sinapse-ai 1.8.0 → 1.9.1
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/hooks/mind-clone-governance.py +212 -212
- package/.claude/hooks/read-protection.py +152 -152
- package/.claude/hooks/slug-validation.py +175 -175
- package/.claude/hooks/sql-governance.py +183 -183
- package/.claude/rules/documentation-first.md +1 -1
- package/.claude/rules/hook-governance.md +1 -1
- package/.claude/rules/mandatory-delegation.md +1 -1
- package/.claude/rules/project-intelligence.md +1 -1
- package/.codex/agents/analyst.md +4 -371
- package/.codex/agents/animations-orqx.md +4 -57
- package/.codex/agents/architect.md +4 -560
- package/.codex/agents/brand-orqx.md +4 -95
- package/.codex/agents/claude-mastery-chief.md +4 -0
- package/.codex/agents/cloning-orqx.md +4 -70
- package/.codex/agents/commercial-orqx.md +4 -67
- package/.codex/agents/config-engineer.md +2 -2
- package/.codex/agents/content-orqx.md +4 -77
- package/.codex/agents/copy-orqx.md +4 -65
- package/.codex/agents/cost-optimizer.md +4 -0
- package/.codex/agents/council-orqx.md +4 -68
- package/.codex/agents/courses-orqx.md +4 -64
- package/.codex/agents/cro-persuasion.md +4 -0
- package/.codex/agents/cyber-orqx.md +4 -67
- package/.codex/agents/data-engineer.md +4 -542
- package/.codex/agents/design-orqx.md +4 -65
- package/.codex/agents/design-system.md +4 -210
- package/.codex/agents/developer.md +4 -666
- package/.codex/agents/devops.md +4 -668
- package/.codex/agents/finance-orqx.md +4 -57
- package/.codex/agents/fiscal-compliance-br.md +4 -0
- package/.codex/agents/forecast-strategist.md +4 -0
- package/.codex/agents/growth-orqx.md +4 -75
- package/.codex/agents/hooks-architect.md +2 -2
- package/.codex/agents/mcp-integrator.md +2 -2
- package/.codex/agents/paidmedia-orqx.md +4 -67
- package/.codex/agents/platform-aesthetic-director.md +4 -0
- package/.codex/agents/premium-packaging-strategist.md +4 -0
- package/.codex/agents/product-lead.md +4 -371
- package/.codex/agents/product-orqx.md +4 -57
- package/.codex/agents/product-surface-director.md +4 -0
- package/.codex/agents/project-integrator.md +2 -2
- package/.codex/agents/project-lead.md +4 -414
- package/.codex/agents/quality-gate.md +4 -547
- package/.codex/agents/research-orqx.md +4 -67
- package/.codex/agents/roadmap-sentinel.md +2 -2
- package/.codex/agents/skill-craftsman.md +2 -2
- package/.codex/agents/snps-orqx.md +4 -684
- package/.codex/agents/sop-extractor.md +4 -61
- package/.codex/agents/sprint-lead.md +4 -324
- package/.codex/agents/squad-creator.md +4 -402
- package/.codex/agents/storytelling-orqx.md +4 -65
- package/.codex/agents/swarm-orqx.md +4 -64
- package/.codex/agents/ux-design-expert.md +4 -532
- package/.codex/agents/ux-designer.md +4 -124
- package/.codex/command-registry.json +9 -9
- package/.codex/delegation-matrix.json +375 -839
- package/.codex/delegation-parity.json +658 -0
- package/.codex/handoff-packet.parity.schema.json +148 -0
- package/.codex/handoff-packet.template.json +26 -0
- package/.codex/instructions.md +8 -8
- package/.codex/scripts/resolve-codex-agent.js +482 -0
- package/.codex/scripts/resolve-codex-command.js +75 -12
- package/.codex/scripts/resolve-codex-delegation.js +131 -92
- package/.codex/skills/sinapse-claude/SKILL.md +3 -3
- package/.codex/skills/sinapse-po/SKILL.md +1 -1
- package/.codex/tasks/resolve-sinapse-conflict.md +1 -1
- package/.sinapse-ai/constitution.md +5 -5
- package/.sinapse-ai/core/doctor/checks/git-hooks.js +163 -19
- package/.sinapse-ai/core/events/dashboard-emitter.js +30 -9
- package/.sinapse-ai/core/execution/subagent-dispatcher.js +1 -1
- package/.sinapse-ai/core/synapse/engine.js +15 -0
- package/.sinapse-ai/core/ui/observability-panel.js +240 -0
- package/.sinapse-ai/core-config.yaml +0 -20
- package/.sinapse-ai/data/entity-registry.yaml +185 -236
- package/.sinapse-ai/development/agents/snps-orqx.md +16 -26
- package/.sinapse-ai/development/tasks/build-autonomous.md +11 -1
- package/.sinapse-ai/development/tasks/build-resume.md +8 -0
- package/.sinapse-ai/development/tasks/build-status.md +8 -0
- package/.sinapse-ai/development/tasks/build.md +8 -0
- package/.sinapse-ai/development/tasks/cleanup-worktrees.md +8 -1
- package/.sinapse-ai/development/tasks/gotcha.md +8 -0
- package/.sinapse-ai/development/tasks/gotchas.md +8 -0
- package/.sinapse-ai/development/tasks/ids-health.md +14 -6
- package/.sinapse-ai/development/tasks/list-mcps.md +15 -0
- package/.sinapse-ai/development/tasks/merge-worktree.md +8 -1
- package/.sinapse-ai/development/tasks/qa-review-build.md +18 -0
- package/.sinapse-ai/development/tasks/remove-mcp.md +8 -1
- package/.sinapse-ai/development/tasks/validate-agents.md +26 -14
- package/.sinapse-ai/development/templates/service-template/README.md.hbs +159 -159
- package/.sinapse-ai/development/templates/service-template/__tests__/index.test.ts.hbs +238 -238
- package/.sinapse-ai/development/templates/service-template/client.ts.hbs +404 -404
- package/.sinapse-ai/development/templates/service-template/errors.ts.hbs +183 -183
- package/.sinapse-ai/development/templates/service-template/index.ts.hbs +121 -121
- package/.sinapse-ai/development/templates/service-template/package.json.hbs +88 -88
- package/.sinapse-ai/development/templates/service-template/types.ts.hbs +146 -146
- package/.sinapse-ai/development/templates/squad-template/LICENSE +22 -22
- package/.sinapse-ai/git-hooks/lib/framework-guard.js +258 -0
- package/.sinapse-ai/git-hooks/lib/secret-scanner-core.js +355 -0
- package/.sinapse-ai/git-hooks/lib/staged-secret-scan.js +179 -0
- package/.sinapse-ai/git-hooks/lib/staged-sql-guard.js +204 -0
- package/.sinapse-ai/git-hooks/post-commit +28 -0
- package/.sinapse-ai/git-hooks/pre-commit +81 -0
- package/.sinapse-ai/git-hooks/pre-push +83 -0
- package/.sinapse-ai/hooks/ids-post-commit.js +13 -11
- package/.sinapse-ai/hooks/ids-pre-push.js +9 -7
- package/.sinapse-ai/infrastructure/scripts/codex-parity/resolve.js +161 -0
- package/.sinapse-ai/infrastructure/scripts/dashboard-status-writer.js +6 -2
- package/.sinapse-ai/infrastructure/scripts/ide-sync/index.js +65 -68
- package/.sinapse-ai/infrastructure/scripts/sync-codex-local-first.js +156 -1
- package/.sinapse-ai/infrastructure/scripts/validate-codex-delegation.js +1 -4
- package/.sinapse-ai/infrastructure/scripts/validate-codex-integration.js +41 -5
- package/.sinapse-ai/infrastructure/templates/coderabbit.yaml.template +280 -280
- package/.sinapse-ai/infrastructure/templates/config/env.example +16 -16
- package/.sinapse-ai/infrastructure/templates/config/gitignore-additions.tmpl +59 -59
- package/.sinapse-ai/infrastructure/templates/github/CODEOWNERS.template +12 -12
- package/.sinapse-ai/infrastructure/templates/github-workflows/ci.yml.template +170 -170
- package/.sinapse-ai/infrastructure/templates/github-workflows/pr-automation.yml.template +331 -331
- package/.sinapse-ai/infrastructure/templates/github-workflows/release.yml.template +197 -197
- package/.sinapse-ai/infrastructure/templates/gitignore/gitignore-brownfield-merge.tmpl +19 -19
- package/.sinapse-ai/infrastructure/templates/gitignore/gitignore-node.tmpl +86 -86
- package/.sinapse-ai/infrastructure/templates/gitignore/gitignore-python.tmpl +146 -146
- package/.sinapse-ai/infrastructure/templates/gitignore/gitignore-sinapse-base.tmpl +64 -64
- package/.sinapse-ai/infrastructure/templates/safe-collab/CODEOWNERS.template +16 -16
- package/.sinapse-ai/infrastructure/templates/sinapse-sync.yaml.template +183 -183
- package/.sinapse-ai/install-manifest.yaml +112 -164
- package/.sinapse-ai/local-config.yaml.template +65 -65
- package/.sinapse-ai/product/templates/adr.hbs +126 -126
- package/.sinapse-ai/product/templates/dbdr.hbs +242 -242
- package/.sinapse-ai/product/templates/epic.hbs +213 -213
- package/.sinapse-ai/product/templates/ide-rules/codex-rules.md +30 -0
- package/.sinapse-ai/product/templates/pmdr.hbs +187 -187
- package/.sinapse-ai/product/templates/prd-v2.0.hbs +217 -217
- package/.sinapse-ai/product/templates/prd.hbs +202 -202
- package/.sinapse-ai/product/templates/statusline/statusline-script.js +31 -8
- package/.sinapse-ai/product/templates/statusline/track-agent-clear.cjs +79 -0
- package/.sinapse-ai/product/templates/statusline/track-agent.cjs +218 -0
- package/.sinapse-ai/product/templates/story.hbs +264 -264
- package/.sinapse-ai/product/templates/task.hbs +171 -171
- package/.sinapse-ai/product/templates/tmpl-comment-on-examples.sql +159 -159
- package/.sinapse-ai/product/templates/tmpl-migration-script.sql +92 -92
- package/.sinapse-ai/product/templates/tmpl-rls-granular-policies.sql +105 -105
- package/.sinapse-ai/product/templates/tmpl-rls-kiss-policy.sql +11 -11
- package/.sinapse-ai/product/templates/tmpl-rls-roles.sql +136 -136
- package/.sinapse-ai/product/templates/tmpl-rls-simple.sql +78 -78
- package/.sinapse-ai/product/templates/tmpl-rls-tenant.sql +153 -153
- package/.sinapse-ai/product/templates/tmpl-rollback-script.sql +78 -78
- package/.sinapse-ai/product/templates/tmpl-seed-data.sql +141 -141
- package/.sinapse-ai/product/templates/tmpl-smoke-test.sql +17 -17
- package/.sinapse-ai/product/templates/tmpl-staging-copy-merge.sql +140 -140
- package/.sinapse-ai/product/templates/tmpl-stored-proc.sql +141 -141
- package/.sinapse-ai/product/templates/tmpl-trigger.sql +153 -153
- package/.sinapse-ai/product/templates/tmpl-view-materialized.sql +134 -134
- package/.sinapse-ai/product/templates/tmpl-view.sql +178 -178
- package/AGENTS.md +193 -0
- package/CHANGELOG.md +1247 -0
- package/LICENSE +63 -63
- package/README.en.md +17 -18
- package/README.md +18 -19
- package/bin/cli.js +1 -1
- package/bin/commands/install.js +194 -22
- package/bin/commands/status.js +14 -1
- package/bin/commands/uninstall.js +2 -2
- package/bin/commands/update.js +52 -0
- package/bin/lib/setup-statusline.js +191 -0
- package/bin/sinapse-init.js +11 -83
- package/bin/utils/framework-guard.js +17 -4
- package/bin/utils/secret-scanner-core.js +109 -7
- package/bin/utils/staged-sql-guard.js +204 -0
- package/bin/utils/validate-publish.js +63 -0
- package/docs/agent-reference-guide.md +5 -7
- package/docs/framework/agent-prefix-convention.md +58 -0
- package/docs/framework/architecture-overview.md +4 -4
- package/docs/framework/collaboration-activation.md +45 -0
- package/docs/framework/guiding-principles.md +9 -9
- package/docs/getting-started.md +1 -1
- package/docs/guides/agent-reference.md +1 -1
- package/docs/guides/codex-config.md +4 -5
- package/docs/pt/architecture/sub-orqx-pattern.md +20 -18
- package/docs/security/overview.md +1 -1
- package/package.json +16 -12
- package/packages/installer/src/index.js +26 -0
- package/packages/installer/src/installer/git-hooks-installer.js +211 -47
- package/packages/installer/src/installer/sinapse-ai-installer.js +71 -0
- package/packages/installer/src/wizard/feedback.js +1 -1
- package/packages/installer/src/wizard/ide-config-generator.js +26 -26
- package/packages/installer/src/wizard/index.js +53 -4
- package/packages/sinapse-install/bin/edmcp.js +0 -0
- package/packages/sinapse-install/bin/sinapse-install.js +0 -0
- package/scripts/audit-tasks.cjs +112 -91
- package/scripts/check-markdown-links.py +352 -352
- package/scripts/prepare-hooks.js +58 -0
- package/scripts/regenerate-orqx-stubs.ps1 +2 -3
- package/scripts/sync-counts.js +10 -2
- package/scripts/sync-squad-yaml-components.js +108 -6
- package/scripts/validate-agents-md.js +128 -0
- package/scripts/validate-all.js +1 -0
- package/scripts/validate-squad-orqx.js +19 -9
- package/sinapse/agents/sinapse-orqx.md +16 -26
- package/sinapse/agents/snps-orqx.md +15 -25
- package/sinapse/knowledge-base/routing-catalog.md +1 -1
- package/sinapse/tasks/diagnose-and-route.md +1 -1
- package/sinapse/tasks/squad-status-report.md +1 -1
- package/squads/claude-code-mastery/agents/claude-mastery-chief.md +1 -1
- package/squads/claude-code-mastery/agents/hooks-architect.md +60 -68
- package/squads/claude-code-mastery/knowledge-base/swarm-orchestration-patterns.md +1 -1
- package/squads/claude-code-mastery/squad.yaml +8 -0
- package/squads/claude-code-mastery/tasks/audit-setup.md +1 -1
- package/squads/claude-code-mastery/workflows/optimization-cycle.yaml +4 -4
- package/squads/claude-code-mastery/workflows/project-setup-cycle.yaml +4 -4
- package/squads/squad-animations/README.md +1 -1
- package/squads/squad-animations/squad.yaml +1 -1
- package/squads/squad-brand/squad.yaml +1 -1
- package/squads/squad-cloning/README.md +1 -1
- package/squads/squad-cloning/squad.yaml +1 -1
- package/squads/squad-commercial/README.md +1 -1
- package/squads/squad-commercial/squad.yaml +2 -3
- package/squads/squad-content/README.md +1 -1
- package/squads/squad-content/squad.yaml +1 -1
- package/squads/squad-copy/README.md +1 -1
- package/squads/squad-copy/squad.yaml +2 -3
- package/squads/squad-council/README.md +1 -1
- package/squads/squad-courses/README.md +1 -1
- package/squads/squad-courses/squad.yaml +1 -1
- package/squads/squad-cybersecurity/README.md +1 -1
- package/squads/squad-cybersecurity/squad.yaml +2 -3
- package/squads/squad-design/README.md +1 -1
- package/squads/{squad-artdir → squad-design}/agents/cro-persuasion.md +1 -1
- package/squads/{squad-artdir → squad-design}/agents/platform-aesthetic-director.md +2 -2
- package/squads/{squad-artdir → squad-design}/agents/premium-packaging-strategist.md +2 -2
- package/squads/{squad-artdir → squad-design}/agents/product-surface-director.md +3 -3
- package/squads/squad-design/squad.yaml +6 -3
- package/squads/squad-finance/README.md +1 -1
- package/squads/squad-finance/squad.yaml +7 -1
- package/squads/squad-growth/README.md +1 -1
- package/squads/squad-growth/squad.yaml +1 -1
- package/squads/squad-paidmedia/README.md +1 -1
- package/squads/squad-paidmedia/squad.yaml +2 -3
- package/squads/squad-product/README.md +1 -1
- package/squads/squad-product/squad.yaml +1 -1
- package/squads/squad-research/README.md +1 -1
- package/squads/squad-research/squad.yaml +2 -3
- package/squads/squad-storytelling/README.md +1 -1
- package/squads/squad-storytelling/squad.yaml +2 -3
- package/.codex/agents/brad-frost.md +0 -46
- package/.codex/agents/claude-orqx.md +0 -72
- package/.codex/agents/copy-chief.md +0 -162
- package/.codex/agents/cyber-chief.md +0 -169
- package/.codex/agents/dan-mall.md +0 -43
- package/.codex/agents/data-chief.md +0 -198
- package/.codex/agents/dave-malouf.md +0 -43
- package/.codex/agents/db-sage.md +0 -152
- package/.codex/agents/design-chief.md +0 -226
- package/.codex/agents/dev.md +0 -102
- package/.codex/agents/legal-chief.md +0 -199
- package/.codex/agents/nano-banana-generator.md +0 -42
- package/.codex/agents/pm.md +0 -81
- package/.codex/agents/po.md +0 -85
- package/.codex/agents/qa.md +0 -98
- package/.codex/agents/sm.md +0 -77
- package/.codex/agents/squad-chief.md +0 -1553
- package/.codex/agents/squad.md +0 -66
- package/.codex/agents/story-chief.md +0 -180
- package/.codex/agents/tools-orqx.md +0 -219
- package/.codex/agents/traffic-masters-chief.md +0 -211
- package/.sinapse-ai/core/memory/__tests__/active-modules.verify.js +0 -265
- package/.sinapse-ai/core/permissions/__tests__/permission-mode.test.js +0 -293
- package/.sinapse-ai/data/registry-update-log.jsonl +0 -158
- package/.sinapse-ai/infrastructure/scripts/ide-sync/gemini-commands.js +0 -298
- package/.sinapse-ai/infrastructure/scripts/ide-sync/transformers/antigravity.js +0 -121
- package/.sinapse-ai/infrastructure/scripts/ide-sync/transformers/cursor.js +0 -119
- package/.sinapse-ai/infrastructure/scripts/ide-sync/transformers/github-copilot.js +0 -191
- package/.sinapse-ai/infrastructure/scripts/ide-sync/transformers/kimi.js +0 -448
- package/.sinapse-ai/infrastructure/tests/project-status-loader.test.js +0 -569
- package/.sinapse-ai/infrastructure/tests/regression-suite-v2.md +0 -622
- package/.sinapse-ai/infrastructure/tests/validate-module.js +0 -98
- package/.sinapse-ai/infrastructure/tests/worktree-manager.test.js +0 -620
- package/.sinapse-ai/monitor/hooks/lib/__init__.py +0 -2
- package/.sinapse-ai/monitor/hooks/lib/enrich.py +0 -59
- package/.sinapse-ai/monitor/hooks/lib/send_event.py +0 -48
- package/.sinapse-ai/monitor/hooks/notification.py +0 -30
- package/.sinapse-ai/monitor/hooks/post_tool_use.py +0 -46
- package/.sinapse-ai/monitor/hooks/pre_compact.py +0 -30
- package/.sinapse-ai/monitor/hooks/pre_tool_use.py +0 -41
- package/.sinapse-ai/monitor/hooks/stop.py +0 -30
- package/.sinapse-ai/monitor/hooks/subagent_stop.py +0 -30
- package/.sinapse-ai/monitor/hooks/user_prompt_submit.py +0 -39
- package/.sinapse-ai/product/templates/statusline/track-agent.sh +0 -69
- package/.sinapse-ai/workflow-intelligence/__tests__/confidence-scorer.test.js +0 -335
- package/.sinapse-ai/workflow-intelligence/__tests__/integration.test.js +0 -340
- package/.sinapse-ai/workflow-intelligence/__tests__/suggestion-engine.test.js +0 -438
- package/.sinapse-ai/workflow-intelligence/__tests__/wave-analyzer.test.js +0 -448
- package/.sinapse-ai/workflow-intelligence/__tests__/workflow-registry.test.js +0 -303
- package/bin/sinapse-graph.js +0 -19
- package/docs/codex-integration-process.md +0 -22
- package/docs/codex-parity-program.md +0 -27
- package/packages/installer/src/__tests__/performance-benchmark.js +0 -383
- package/packages/installer/tests/integration/environment-configuration.test.js +0 -332
- package/packages/installer/tests/integration/wizard-detection.test.js +0 -352
- package/packages/installer/tests/unit/artifact-copy-pipeline/artifact-copy-pipeline.test.js +0 -383
- package/packages/installer/tests/unit/claude-md-template-v5/claude-md-template-v5.test.js +0 -193
- package/packages/installer/tests/unit/config-validator.test.js +0 -315
- package/packages/installer/tests/unit/detection/detect-project-type.test.js +0 -539
- package/packages/installer/tests/unit/doctor/doctor-checks.test.js +0 -636
- package/packages/installer/tests/unit/doctor/doctor-orchestrator.test.js +0 -192
- package/packages/installer/tests/unit/entity-registry-bootstrap.test.js +0 -186
- package/packages/installer/tests/unit/env-template.test.js +0 -187
- package/packages/installer/tests/unit/generate-settings-json/generate-settings-json.test.js +0 -310
- package/packages/installer/tests/unit/git-hooks-installer.test.js +0 -262
- package/packages/installer/tests/unit/ide-sync-integration/ide-sync-integration.test.js +0 -231
- package/packages/installer/tests/unit/merger/env-merger.test.js +0 -191
- package/packages/installer/tests/unit/merger/markdown-merger.test.js +0 -262
- package/packages/installer/tests/unit/merger/strategies.test.js +0 -154
- package/packages/installer/tests/unit/merger/yaml-merger.test.js +0 -328
- package/packages/sinapse-install/tests/unit/chrome-brain.smoke.test.js +0 -66
- package/scripts/install-monitor-hooks.sh +0 -82
- package/squads/squad-artdir/README.md +0 -90
- package/squads/squad-artdir/agents/accessibility-guardian.md +0 -184
- package/squads/squad-artdir/agents/artdir-orqx.md +0 -222
- package/squads/squad-artdir/agents/color-psychologist.md +0 -166
- package/squads/squad-artdir/agents/design-system-architect.md +0 -100
- package/squads/squad-artdir/agents/ia-architect.md +0 -169
- package/squads/squad-artdir/agents/interaction-designer.md +0 -162
- package/squads/squad-artdir/agents/layout-engineer.md +0 -163
- package/squads/squad-artdir/agents/motion-architect.md +0 -185
- package/squads/squad-artdir/agents/type-systemist.md +0 -138
- package/squads/squad-artdir/agents/visual-strategist.md +0 -127
- package/squads/squad-artdir/checklists/seven-pillars-validation-checklist.md +0 -172
- package/squads/squad-artdir/knowledge-base/case-nyo-ia-reference.md +0 -289
- package/squads/squad-artdir/knowledge-base/deliverables-templates.md +0 -457
- package/squads/squad-artdir/knowledge-base/motion-technique-catalog.md +0 -247
- package/squads/squad-artdir/knowledge-base/premium-packaging-principles.md +0 -133
- package/squads/squad-artdir/knowledge-base/psychological-toolkit.md +0 -229
- package/squads/squad-artdir/knowledge-base/saas-art-direction-canon.md +0 -242
- package/squads/squad-artdir/knowledge-base/seven-pillars-framework.md +0 -289
- package/squads/squad-artdir/knowledge-base/ten-pillars-framework.md +0 -221
- package/squads/squad-artdir/package.json +0 -20
- package/squads/squad-artdir/squad.yaml +0 -299
- package/squads/squad-artdir/tasks/audit-conversion.md +0 -97
- package/squads/squad-artdir/tasks/audit-drift-multi-surface.md +0 -55
- package/squads/squad-artdir/tasks/consult-saas-canon.md +0 -54
- package/squads/squad-artdir/tasks/create-art-direction-brief.md +0 -110
- package/squads/squad-artdir/tasks/create-premium-packaging-brief.md +0 -61
- package/squads/squad-artdir/tasks/create-wireflow.md +0 -84
- package/squads/squad-artdir/tasks/design-color-system.md +0 -81
- package/squads/squad-artdir/tasks/design-product-surface.md +0 -60
- package/squads/squad-artdir/tasks/design-token-system.md +0 -58
- package/squads/squad-artdir/tasks/diagnose-visual-language.md +0 -92
- package/squads/squad-artdir/tasks/first-5-minutes-choreography.md +0 -65
- package/squads/squad-artdir/tasks/specify-motion-system.md +0 -84
- package/squads/squad-artdir/tasks/validate-against-pillars.md +0 -143
- package/squads/squad-artdir/templates/art-direction-brief-template.md +0 -215
- package/squads/squad-artdir/workflows/conversion-audit-cycle.yaml +0 -142
- package/squads/squad-artdir/workflows/full-art-direction-cycle.yaml +0 -179
- package/squads/squad-artdir/workflows/saas-platform-art-direction-cycle.yaml +0 -338
- package/squads/squad-commercial/agents/legal-chief.md +0 -199
- package/squads/squad-copy/agents/copy-chief.md +0 -162
- package/squads/squad-cybersecurity/agents/cyber-chief.md +0 -169
- package/squads/squad-design/agents/design-chief.md +0 -226
- package/squads/squad-paidmedia/agents/traffic-masters-chief.md +0 -211
- package/squads/squad-research/agents/data-chief.md +0 -198
- package/squads/squad-storytelling/agents/story-chief.md +0 -180
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework Guard — Pre-commit hook that blocks commits to L1/L2 protected paths.
|
|
3
|
+
*
|
|
4
|
+
* Reads ALL configuration from core-config.yaml (single source of truth):
|
|
5
|
+
* - boundary.frameworkProtection: toggle (true/false)
|
|
6
|
+
* - boundary.protected: L1/L2 blocked glob patterns
|
|
7
|
+
* - boundary.exceptions: L3 allowed glob patterns
|
|
8
|
+
*
|
|
9
|
+
* When frameworkProtection is true (default), blocks staged changes to protected paths.
|
|
10
|
+
* When false, acts as a no-op (contributor mode).
|
|
11
|
+
*
|
|
12
|
+
* Story: BM-3 (Epic: Boundary Mapping & Framework-Project Separation)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const { execSync } = require('child_process');
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
|
|
21
|
+
// Hardcoded fallbacks — used ONLY if core-config.yaml is missing or malformed.
|
|
22
|
+
// The canonical source is core-config.yaml boundary.protected/exceptions.
|
|
23
|
+
const FALLBACK_PROTECTED = [
|
|
24
|
+
'.sinapse-ai/core/**',
|
|
25
|
+
'.sinapse-ai/development/tasks/**',
|
|
26
|
+
'.sinapse-ai/development/templates/**',
|
|
27
|
+
'.sinapse-ai/development/checklists/**',
|
|
28
|
+
'.sinapse-ai/development/workflows/**',
|
|
29
|
+
'.sinapse-ai/infrastructure/**',
|
|
30
|
+
'.sinapse-ai/constitution.md',
|
|
31
|
+
'bin/sinapse.js',
|
|
32
|
+
'bin/sinapse-init.js',
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const FALLBACK_EXCEPTIONS = [
|
|
36
|
+
'.sinapse-ai/data/**',
|
|
37
|
+
'.sinapse-ai/development/agents/*/MEMORY.md',
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Convert a glob pattern to a RegExp.
|
|
42
|
+
* Supports: ** (any depth), * (single segment), literal dots.
|
|
43
|
+
* @param {string} glob
|
|
44
|
+
* @returns {RegExp}
|
|
45
|
+
*/
|
|
46
|
+
function globToRegex(glob) {
|
|
47
|
+
// 1. Replace ** with placeholder before processing
|
|
48
|
+
let pattern = glob.replace(/\*\*/g, '\u0000');
|
|
49
|
+
|
|
50
|
+
// 2. Escape all regex-special chars (dots, plus, etc.) — but NOT * or placeholder
|
|
51
|
+
pattern = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
|
52
|
+
|
|
53
|
+
// 3. Convert remaining single * to single-segment matcher
|
|
54
|
+
pattern = pattern.replace(/\*/g, '[^/]+');
|
|
55
|
+
|
|
56
|
+
// 4. Restore ** placeholder to any-depth matcher
|
|
57
|
+
pattern = pattern.replace(/\u0000/g, '.+');
|
|
58
|
+
|
|
59
|
+
// If pattern ends with .+ (was **), match prefix
|
|
60
|
+
if (glob.endsWith('**')) {
|
|
61
|
+
return new RegExp('^' + pattern);
|
|
62
|
+
}
|
|
63
|
+
// Exact file match
|
|
64
|
+
return new RegExp('^' + pattern + '$');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Read the raw content of core-config.yaml.
|
|
69
|
+
*
|
|
70
|
+
* Resolution order (so this works both as the in-repo hook AND when bundled
|
|
71
|
+
* into .sinapse-ai/git-hooks/lib/ by the installer, where __dirname no longer
|
|
72
|
+
* sits two levels under the project root):
|
|
73
|
+
* 1. `<cwd>/.sinapse-ai/core-config.yaml` — git runs hooks from the repo root,
|
|
74
|
+
* so cwd is the project root in the pre-commit path.
|
|
75
|
+
* 2. `<__dirname>/../../.sinapse-ai/core-config.yaml` — the original in-repo
|
|
76
|
+
* location (bin/utils -> repo root), kept for direct/test invocation.
|
|
77
|
+
* @returns {string|null}
|
|
78
|
+
*/
|
|
79
|
+
function readConfigContent() {
|
|
80
|
+
const candidates = [
|
|
81
|
+
path.resolve(process.cwd(), '.sinapse-ai/core-config.yaml'),
|
|
82
|
+
path.resolve(__dirname, '../../.sinapse-ai/core-config.yaml'),
|
|
83
|
+
];
|
|
84
|
+
for (const configPath of candidates) {
|
|
85
|
+
if (fs.existsSync(configPath)) {
|
|
86
|
+
return fs.readFileSync(configPath, 'utf8');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Parse a YAML list under a given key using simple line-based parsing.
|
|
94
|
+
* Avoids js-yaml dependency for speed.
|
|
95
|
+
* @param {string} content - YAML file content
|
|
96
|
+
* @param {string} parentKey - Parent key (e.g., 'boundary')
|
|
97
|
+
* @param {string} listKey - List key (e.g., 'protected')
|
|
98
|
+
* @returns {string[]}
|
|
99
|
+
*/
|
|
100
|
+
function parseYamlList(content, parentKey, listKey) {
|
|
101
|
+
const lines = content.split('\n');
|
|
102
|
+
const items = [];
|
|
103
|
+
let inParent = false;
|
|
104
|
+
let inList = false;
|
|
105
|
+
let parentIndent = -1;
|
|
106
|
+
let listIndent = -1;
|
|
107
|
+
|
|
108
|
+
for (const line of lines) {
|
|
109
|
+
const trimmed = line.trimStart();
|
|
110
|
+
const indent = line.length - trimmed.length;
|
|
111
|
+
|
|
112
|
+
// Find parent key (e.g., "boundary:")
|
|
113
|
+
if (trimmed === parentKey + ':' || trimmed.startsWith(parentKey + ':')) {
|
|
114
|
+
inParent = true;
|
|
115
|
+
parentIndent = indent;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// If we're past the parent section (back to same or lower indent)
|
|
120
|
+
if (inParent && indent <= parentIndent && trimmed.length > 0 && !trimmed.startsWith('#')) {
|
|
121
|
+
inParent = false;
|
|
122
|
+
inList = false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!inParent) continue;
|
|
126
|
+
|
|
127
|
+
// Find list key within parent (e.g., "protected:")
|
|
128
|
+
if (trimmed === listKey + ':' || trimmed.startsWith(listKey + ':')) {
|
|
129
|
+
inList = true;
|
|
130
|
+
listIndent = indent;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// If we're past the list (back to same or lower indent within parent)
|
|
135
|
+
if (inList && indent <= listIndent && trimmed.length > 0 && !trimmed.startsWith('#') && !trimmed.startsWith('-')) {
|
|
136
|
+
inList = false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Collect list items
|
|
140
|
+
if (inList && trimmed.startsWith('- ')) {
|
|
141
|
+
items.push(trimmed.slice(2).trim());
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return items;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Read boundary config from core-config.yaml.
|
|
150
|
+
* @returns {{ enabled: boolean, protected: string[], exceptions: string[] }}
|
|
151
|
+
*/
|
|
152
|
+
function readBoundaryConfig() {
|
|
153
|
+
const content = readConfigContent();
|
|
154
|
+
if (!content) {
|
|
155
|
+
return { enabled: true, protected: FALLBACK_PROTECTED, exceptions: FALLBACK_EXCEPTIONS };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Read toggle
|
|
159
|
+
const toggleMatch = content.match(/frameworkProtection:\s*(true|false)/);
|
|
160
|
+
const enabled = toggleMatch ? toggleMatch[1] === 'true' : true;
|
|
161
|
+
|
|
162
|
+
// Read lists
|
|
163
|
+
const protectedPaths = parseYamlList(content, 'boundary', 'protected');
|
|
164
|
+
const exceptionPaths = parseYamlList(content, 'boundary', 'exceptions');
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
enabled,
|
|
168
|
+
protected: protectedPaths.length > 0 ? protectedPaths : FALLBACK_PROTECTED,
|
|
169
|
+
exceptions: exceptionPaths.length > 0 ? exceptionPaths : FALLBACK_EXCEPTIONS,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Check if a file path matches any pattern in the list.
|
|
175
|
+
* @param {string} filePath
|
|
176
|
+
* @param {RegExp[]} patterns
|
|
177
|
+
* @returns {boolean}
|
|
178
|
+
*/
|
|
179
|
+
function matchesAny(filePath, patterns) {
|
|
180
|
+
return patterns.some((pattern) => pattern.test(filePath));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Get list of staged files from git.
|
|
185
|
+
* @returns {string[]}
|
|
186
|
+
*/
|
|
187
|
+
function getStagedFiles() {
|
|
188
|
+
try {
|
|
189
|
+
const output = execSync('git diff --cached --name-only', { encoding: 'utf8' });
|
|
190
|
+
return output
|
|
191
|
+
.split('\n')
|
|
192
|
+
.map((f) => f.trim())
|
|
193
|
+
.filter(Boolean);
|
|
194
|
+
} catch {
|
|
195
|
+
return [];
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function main() {
|
|
200
|
+
// Step 1: Read config (single source of truth)
|
|
201
|
+
const config = readBoundaryConfig();
|
|
202
|
+
|
|
203
|
+
if (!config.enabled) {
|
|
204
|
+
process.exit(0);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Step 2: Compile glob patterns to regex
|
|
208
|
+
const blockedPatterns = config.protected.map(globToRegex);
|
|
209
|
+
const allowedPatterns = config.exceptions.map(globToRegex);
|
|
210
|
+
|
|
211
|
+
// Step 3: Get staged files
|
|
212
|
+
const stagedFiles = getStagedFiles();
|
|
213
|
+
if (stagedFiles.length === 0) {
|
|
214
|
+
process.exit(0);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Step 4: Check each staged file
|
|
218
|
+
const blockedFiles = [];
|
|
219
|
+
for (const file of stagedFiles) {
|
|
220
|
+
// Normalize to forward slashes (Windows compat)
|
|
221
|
+
const normalized = file.replace(/\\/g, '/');
|
|
222
|
+
if (matchesAny(normalized, blockedPatterns) && !matchesAny(normalized, allowedPatterns)) {
|
|
223
|
+
blockedFiles.push(normalized);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Step 5: Report
|
|
228
|
+
if (blockedFiles.length > 0) {
|
|
229
|
+
console.error('');
|
|
230
|
+
console.error('Framework Guard: Commit blocked!');
|
|
231
|
+
console.error('');
|
|
232
|
+
console.error('The following framework files are protected (L1/L2):');
|
|
233
|
+
for (const file of blockedFiles) {
|
|
234
|
+
console.error(` - ${file}`);
|
|
235
|
+
}
|
|
236
|
+
console.error('');
|
|
237
|
+
console.error('These files are read-only in project mode (boundary.frameworkProtection: true).');
|
|
238
|
+
console.error('');
|
|
239
|
+
console.error('To bypass (framework contributors only):');
|
|
240
|
+
console.error(' git commit --no-verify');
|
|
241
|
+
console.error('');
|
|
242
|
+
console.error('To disable permanently (contributors):');
|
|
243
|
+
console.error(' Set boundary.frameworkProtection: false in core-config.yaml');
|
|
244
|
+
console.error('');
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
process.exit(0);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Export for testing
|
|
252
|
+
module.exports = { readBoundaryConfig, globToRegex, matchesAny, getStagedFiles, FALLBACK_PROTECTED, FALLBACK_EXCEPTIONS };
|
|
253
|
+
|
|
254
|
+
// Run when executed directly
|
|
255
|
+
if (require.main === module) {
|
|
256
|
+
main();
|
|
257
|
+
}
|
|
258
|
+
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Secret Scanner Core — shared detection logic.
|
|
5
|
+
*
|
|
6
|
+
* Single source of truth for secret detection, imported by BOTH:
|
|
7
|
+
* - .claude/hooks/secret-scanning.cjs (Claude Code PreToolUse, Write/Edit content)
|
|
8
|
+
* - bin/utils/staged-secret-scan.js (git pre-commit, staged diff content)
|
|
9
|
+
*
|
|
10
|
+
* Hardening (Frente 4.2 — Stream A):
|
|
11
|
+
* - All 20+ named patterns preserved (no regression).
|
|
12
|
+
* - Shannon entropy: flags high-entropy tokens (>= 20 contiguous chars,
|
|
13
|
+
* >= 4.5 bits/char) as suspected secrets, BEYOND the named patterns.
|
|
14
|
+
* Lockfile-hash context + code-identifier band are allowlisted to avoid
|
|
15
|
+
* false positives on integrity hashes and long camelCase symbols.
|
|
16
|
+
* - Allowlist: placeholders (your-key-here, xxx, CHANGEME, REPLACE_ME, <...>,
|
|
17
|
+
* example.com, etc.) and obvious fakes PASS.
|
|
18
|
+
* - Redaction: matched secrets are never printed in full.
|
|
19
|
+
* - Designed for fail-CLOSED callers (this module never silently allows).
|
|
20
|
+
*
|
|
21
|
+
* @module bin/utils/secret-scanner-core
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
// Private-key PEM headers are built from fragments so this source file holds no
|
|
25
|
+
// literal "BEGIN ... PRIVATE KEY" marker (avoids self-tripping secret scanners).
|
|
26
|
+
const PK = 'PRIVATE KEY';
|
|
27
|
+
const BEGIN = '-----BEGIN ';
|
|
28
|
+
const END5 = '-----';
|
|
29
|
+
|
|
30
|
+
const NAMED_PATTERNS = [
|
|
31
|
+
// API Keys & Tokens
|
|
32
|
+
{ name: 'AWS Access Key', pattern: /AKIA[0-9A-Z]{16}/ },
|
|
33
|
+
{ name: 'AWS Secret Key', pattern: /(?:aws_secret_access_key|secret_key)\s*[=:]\s*['"]?[A-Za-z0-9/+=]{40}['"]?/i, lowConfidence: true },
|
|
34
|
+
{ name: 'GitHub Token', pattern: /gh[ps]_[A-Za-z0-9_]{36,}/ },
|
|
35
|
+
{ name: 'GitHub OAuth', pattern: /gho_[A-Za-z0-9_]{36,}/ },
|
|
36
|
+
{ name: 'GitHub Fine-Grained Token', pattern: /github_pat_[A-Za-z0-9_]{22,}/ },
|
|
37
|
+
{ name: 'Slack Token', pattern: /xox[bpors]-[0-9]{10,}-[A-Za-z0-9-]+/ },
|
|
38
|
+
{ name: 'Stripe Key', pattern: /[sr]k_(live|test)_[A-Za-z0-9]{20,}/ },
|
|
39
|
+
// OpenAI project-scoped keys (`sk-proj-…`) contain hyphens/underscores, so the
|
|
40
|
+
// generic `sk-[A-Za-z0-9]{20,}` rule below stops at the first hyphen and never
|
|
41
|
+
// matches the body. This explicit rule is CONCLUSIVE on shape (the `sk-proj-`
|
|
42
|
+
// prefix is unambiguous) and is NOT entropy-gated, so a leaked project key is
|
|
43
|
+
// caught even when its tail happens to score low on Shannon entropy.
|
|
44
|
+
{ name: 'OpenAI Project Key', pattern: /sk-proj-[A-Za-z0-9_-]{40,}/ },
|
|
45
|
+
// Other OpenAI key families that also carry separators (svcacct / admin).
|
|
46
|
+
{ name: 'OpenAI Service/Admin Key', pattern: /sk-(?:svcacct|admin)-[A-Za-z0-9_-]{20,}/ },
|
|
47
|
+
// Legacy/classic OpenAI keys (`sk-` + contiguous alnum). Entropy-gated to avoid
|
|
48
|
+
// tripping on `sk-` prefixed identifiers; threshold lowered to 3.0 (see note).
|
|
49
|
+
{ name: 'OpenAI Key', pattern: new RegExp('sk-[A-Za-z0-9]{20,}'), entropyGated: true },
|
|
50
|
+
{ name: 'Anthropic Key', pattern: new RegExp('sk-ant-[A-Za-z0-9-]{20,}') },
|
|
51
|
+
{ name: 'Supabase Key', pattern: /eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\.[A-Za-z0-9_-]{50,}/ },
|
|
52
|
+
{ name: 'Google API Key', pattern: /AIza[0-9A-Za-z_-]{35}/ },
|
|
53
|
+
{ name: 'Vercel Token', pattern: /vercel_[A-Za-z0-9]{20,}/ },
|
|
54
|
+
|
|
55
|
+
// Private Keys (built dynamically; no literal header in source)
|
|
56
|
+
{ name: 'RSA Private Key', pattern: new RegExp(BEGIN + 'RSA ' + PK + END5) },
|
|
57
|
+
{ name: 'SSH Private Key', pattern: new RegExp(BEGIN + 'OPENSSH ' + PK + END5) },
|
|
58
|
+
{ name: 'PGP Private Key', pattern: new RegExp(BEGIN + 'PGP ' + PK + ' BLOCK' + END5) },
|
|
59
|
+
{ name: 'EC Private Key', pattern: new RegExp(BEGIN + 'EC ' + PK + END5) },
|
|
60
|
+
{ name: 'DSA Private Key', pattern: new RegExp(BEGIN + 'DSA ' + PK + END5) },
|
|
61
|
+
{ name: 'Generic Private Key', pattern: new RegExp(BEGIN + PK + END5) },
|
|
62
|
+
|
|
63
|
+
// Connection Strings
|
|
64
|
+
// user/password are restricted to non-whitespace so the negated classes can't
|
|
65
|
+
// span newlines and match a bogus region across a whole file (the host part of
|
|
66
|
+
// a URL followed by an unrelated colon and at-sign on later lines). Real
|
|
67
|
+
// credentials never contain whitespace, so true detection is unaffected.
|
|
68
|
+
{ name: 'DB Connection String', pattern: /(?:postgres|mysql|mongodb|redis):\/\/[^:\s]+:[^@\s]+@[^/\s]+/i, credentialPlaceholderGated: true },
|
|
69
|
+
{ name: 'Supabase DB URL', pattern: /postgresql:\/\/postgres\.[A-Za-z0-9]+:[^@\s]+@/i, credentialPlaceholderGated: true },
|
|
70
|
+
|
|
71
|
+
// Generic Patterns (broader, lower confidence — placeholder-allowlisted)
|
|
72
|
+
{ name: 'Hardcoded Password', pattern: /(?:password|passwd|pwd)\s*[=:]\s*['"][^'"]{8,}['"]/i, lowConfidence: true },
|
|
73
|
+
{ name: 'Bearer Token', pattern: /[Bb]earer\s+[A-Za-z0-9_\-.]{20,}/, entropyGated: true, lowConfidence: true },
|
|
74
|
+
{ name: 'Basic Auth', pattern: /[Bb]asic\s+[A-Za-z0-9+/=]{20,}/, lowConfidence: true },
|
|
75
|
+
|
|
76
|
+
// Long pure-hex runs (32–64 chars). The 16-symbol hex alphabet caps Shannon
|
|
77
|
+
// entropy at ~4.0, BELOW ENTROPY_THRESHOLD (4.5), so the generic entropy
|
|
78
|
+
// backstop never sees them — Twilio auth tokens (32-hex), webhook signing
|
|
79
|
+
// secrets and SHA-style 64-hex digests slip through. This explicit rule closes
|
|
80
|
+
// that gap. It is `hashContextGated`: a hex run sitting in an integrity /
|
|
81
|
+
// checksum / lockfile / git-sha context (covered by HASH_CONTEXT_PATTERN) is a
|
|
82
|
+
// legitimate hash, NOT a secret, and is allowlisted. `lowConfidence` keeps it
|
|
83
|
+
// OUT of the release publish gate (entropy:false there would otherwise match
|
|
84
|
+
// every documented digest); the diff-scoped commit hook still enforces it.
|
|
85
|
+
{ name: 'Long Hex Token', pattern: /\b[a-f0-9]{32,64}\b/i, hashContextGated: true, lowConfidence: true },
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
const PLACEHOLDER_TOKENS = [
|
|
89
|
+
'your-key', 'your_key', 'yourkey', 'your-secret', 'your_secret',
|
|
90
|
+
'your-token', 'your_token', 'your-api', 'your_api',
|
|
91
|
+
'xxx', 'changeme', 'change-me', 'change_me',
|
|
92
|
+
'replace_me', 'replace-me', 'replaceme', 'replace_with',
|
|
93
|
+
'placeholder', 'example', 'sample', 'dummy', 'fake',
|
|
94
|
+
'todo', 'tbd', 'redacted', 'insert', 'put-your', 'put_your',
|
|
95
|
+
'my-secret', 'mysecret', 'my-password', 'mypassword',
|
|
96
|
+
'supersecret', 'super-secret', 'secret123', 'password123',
|
|
97
|
+
'notarealkey', 'not-a-real', 'test-key', 'test_key', 'testkey',
|
|
98
|
+
'abcdef', '123456', '000000', 'aaaaaa',
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
const PLACEHOLDER_PATTERNS = [
|
|
102
|
+
/^<.*>$/, // <sua-chave>, <token>, <REPLACE>
|
|
103
|
+
/^\[.*\]$/, // [CHANGE_ME], [your key]
|
|
104
|
+
/^\{\{?.*\}?\}$/, // {token}, {{secret}}
|
|
105
|
+
/^\$\{.*\}$/, // ${TOKEN}
|
|
106
|
+
// SCREAMING_SNAKE env-var name used as value. Requires an underscore so raw
|
|
107
|
+
// all-caps secrets (e.g. AWS access keys) are NOT allowlisted by this rule.
|
|
108
|
+
/^[A-Z][A-Z0-9]*_[A-Z0-9_]*$/,
|
|
109
|
+
/^(x+|y+|z+|n+|0+|a+|\.+|-+|_+|\*+)$/i, // xxxx, 0000, ----, ....
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
const EXAMPLE_HOST_PATTERN = /(?:example\.(?:com|org|net)|localhost|127\.0\.0\.1|host\b|your-host|placeholder)/i;
|
|
113
|
+
|
|
114
|
+
// A keyword placeholder only allowlists when it DOMINATES the value: once every
|
|
115
|
+
// placeholder occurrence is removed, the TOTAL alphanumeric length that remains
|
|
116
|
+
// is too short to be a secret on its own (< ENTROPY_MIN_LEN). A bare
|
|
117
|
+
// `lower.includes(token)` was a trivial bypass — ANY real secret that happened to
|
|
118
|
+
// contain "abcdef" / "123456" / "example" anywhere in its body got silently
|
|
119
|
+
// allowlisted (e.g. Xq9Zk2…abcdef…Fg5 passed). Measuring the *total* remainder
|
|
120
|
+
// (not just the longest contiguous run) closes that hole even when the buried
|
|
121
|
+
// placeholder splits the secret into two sub-threshold runs: the legitimate
|
|
122
|
+
// cases (your-key-here, placeholder123456, test-key-example, CHANGEME) strip down
|
|
123
|
+
// to nothing, while a 35-char random token minus a 6-char placeholder still has
|
|
124
|
+
// ~29 alphanumeric chars left and is NOT allowlisted.
|
|
125
|
+
function placeholderDominates(lower) {
|
|
126
|
+
let stripped = lower;
|
|
127
|
+
let matchedAny = false;
|
|
128
|
+
for (const token of PLACEHOLDER_TOKENS) {
|
|
129
|
+
if (stripped.includes(token)) {
|
|
130
|
+
matchedAny = true;
|
|
131
|
+
// Replace with a separator so adjacent runs aren't fused into a longer one.
|
|
132
|
+
stripped = stripped.split(token).join(' ');
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (!matchedAny) return false;
|
|
136
|
+
// Total alphanumeric chars left after removing every placeholder occurrence.
|
|
137
|
+
const remaining = (stripped.match(/[a-z0-9]/g) || []).length;
|
|
138
|
+
return remaining < ENTROPY_MIN_LEN;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function isAllowlistPlaceholder(value) {
|
|
142
|
+
if (value === null || value === undefined) return false;
|
|
143
|
+
const v = String(value).trim();
|
|
144
|
+
if (v.length === 0) return true;
|
|
145
|
+
|
|
146
|
+
// Structural placeholders are always safe: <...>, [...], {token}, ${VAR},
|
|
147
|
+
// SCREAMING_SNAKE env-var names, and repeated single-symbol fillers.
|
|
148
|
+
for (const re of PLACEHOLDER_PATTERNS) {
|
|
149
|
+
if (re.test(v)) return true;
|
|
150
|
+
}
|
|
151
|
+
if (/^(.)\1{5,}$/.test(v)) return true; // repeated single char
|
|
152
|
+
|
|
153
|
+
// Keyword placeholders: word-boundary anchored + dominance, NOT raw includes().
|
|
154
|
+
if (placeholderDominates(v.toLowerCase())) return true;
|
|
155
|
+
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function shannonEntropy(str) {
|
|
160
|
+
if (!str || str.length === 0) return 0;
|
|
161
|
+
const freq = Object.create(null);
|
|
162
|
+
for (const ch of str) {
|
|
163
|
+
freq[ch] = (freq[ch] || 0) + 1;
|
|
164
|
+
}
|
|
165
|
+
const len = str.length;
|
|
166
|
+
let entropy = 0;
|
|
167
|
+
for (const ch in freq) {
|
|
168
|
+
const p = freq[ch] / len;
|
|
169
|
+
entropy -= p * Math.log2(p);
|
|
170
|
+
}
|
|
171
|
+
return entropy;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// 4.5 bits/char: long camelCase/snake identifiers in source top out around
|
|
175
|
+
// ~4.2 (dictionary words have repeated letters), while real random secrets —
|
|
176
|
+
// base64 of random bytes (~5.5), mixed alphanumeric tokens (~5.0), even base64
|
|
177
|
+
// of text (~4.5) — clear it. Set above the identifier band to kill false
|
|
178
|
+
// positives on code symbols; named patterns still catch branded low-entropy keys.
|
|
179
|
+
const ENTROPY_THRESHOLD = 4.5;
|
|
180
|
+
const ENTROPY_MIN_LEN = 20;
|
|
181
|
+
const TOKEN_SHAPE = /[A-Za-z0-9_\-/+=]{20,}/g;
|
|
182
|
+
|
|
183
|
+
const LOCKFILE_PATH_PATTERN = /(?:^|\/)(?:package-lock\.json|yarn\.lock|pnpm-lock\.yaml|composer\.lock|Cargo\.lock|poetry\.lock|Gemfile\.lock|bun\.lockb)$/i;
|
|
184
|
+
const HASH_CONTEXT_PATTERN = /(?:sha512-|sha384-|sha256-|sha1-|integrity"?\s*[:=]|"resolved"|"checksum"|"hash"|[a-f0-9]{40}|[a-f0-9]{64})/i;
|
|
185
|
+
|
|
186
|
+
function isLockfilePath(filePath) {
|
|
187
|
+
if (!filePath) return false;
|
|
188
|
+
return LOCKFILE_PATH_PATTERN.test(String(filePath).replace(/\\/g, '/'));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function redactMatch(value, keep = 4) {
|
|
192
|
+
const v = String(value == null ? '' : value);
|
|
193
|
+
if (v.length === 0) return '[REDACTED]';
|
|
194
|
+
if (v.length <= keep) return '[REDACTED]';
|
|
195
|
+
const prefix = v.slice(0, keep);
|
|
196
|
+
return prefix + '...[REDACTED ' + (v.length - keep) + ' chars]';
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function scanContent(content, options = {}) {
|
|
200
|
+
const text = String(content == null ? '' : content);
|
|
201
|
+
const filePath = options.filePath || '';
|
|
202
|
+
const entropyEnabled = options.entropy !== false;
|
|
203
|
+
const findings = [];
|
|
204
|
+
|
|
205
|
+
if (text.length === 0) return findings;
|
|
206
|
+
|
|
207
|
+
// 1) Named patterns
|
|
208
|
+
// Structural patterns (AWS access key, Stripe, Google, PEM headers,
|
|
209
|
+
// connection strings, JWT) are CONCLUSIVE on shape — they are NOT
|
|
210
|
+
// placeholder-allowlisted (a real AKIA… key that happens to contain
|
|
211
|
+
// "123456" is still a leak). Only `lowConfidence` value-bearing patterns
|
|
212
|
+
// (password/secret assignments, Bearer/Basic headers) get the placeholder
|
|
213
|
+
// allowlist, applied to the captured *value* portion.
|
|
214
|
+
for (const descriptor of NAMED_PATTERNS) {
|
|
215
|
+
const m = text.match(descriptor.pattern);
|
|
216
|
+
if (!m) continue;
|
|
217
|
+
const matched = m[0];
|
|
218
|
+
|
|
219
|
+
if (descriptor.lowConfidence) {
|
|
220
|
+
// Isolate the value side of `key = "<value>"` / `Bearer <value>`.
|
|
221
|
+
const valuePart = (matched.match(/(?:[=:]\s*['"]?|\s+)([^'"]+)['"]?\s*$/) || [null, matched])[1] || matched;
|
|
222
|
+
if (isAllowlistPlaceholder(valuePart)) continue;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (descriptor.credentialPlaceholderGated) {
|
|
226
|
+
// A connection string carries a structured user:password@host. When the
|
|
227
|
+
// password slot is a placeholder/interpolation (${VAR}, <pass>, a
|
|
228
|
+
// SCREAMING_SNAKE env-var name, your-password…) it is a template/example,
|
|
229
|
+
// NOT a leaked credential — and is the idiomatic, safe way to document
|
|
230
|
+
// one. A real leaked password (random/literal) is not allowlisted and is
|
|
231
|
+
// still flagged.
|
|
232
|
+
const cred = (matched.match(/:\/\/[^:/\s]+:([^@\s]+)@/) || [null, ''])[1];
|
|
233
|
+
if (cred && isAllowlistPlaceholder(cred)) continue;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (descriptor.entropyGated) {
|
|
237
|
+
const tail = (matched.match(/[A-Za-z0-9_\-/+=]{16,}$/) || [matched])[0];
|
|
238
|
+
if (isAllowlistPlaceholder(tail)) continue;
|
|
239
|
+
// Threshold lowered 2.5 -> 2.0: a short real token (e.g. a 20-char legacy
|
|
240
|
+
// key with a narrow alphabet) can dip just under 2.5 and would have been a
|
|
241
|
+
// false-NEGATIVE (leaked secret allowed through). 2.0 still rejects obvious
|
|
242
|
+
// non-random fixtures (a single repeated char ~ 0.0, repeated words ~1.5)
|
|
243
|
+
// while no longer dropping legitimately-shaped keys.
|
|
244
|
+
if (shannonEntropy(tail) < 2.0) continue; // clearly non-random
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (descriptor.hashContextGated) {
|
|
248
|
+
// Long hex runs are flagged as suspected secrets (Twilio token, webhook
|
|
249
|
+
// signing secret, SHA-style digest) EXCEPT when they sit in an integrity /
|
|
250
|
+
// checksum / lockfile / git-sha context — those are legitimate hashes, not
|
|
251
|
+
// leaked credentials. Reuse HASH_CONTEXT_PATTERN over a window around the
|
|
252
|
+
// match so package-lock integrity:, "resolved" tarball #sha, "checksum",
|
|
253
|
+
// and 40/64-hex git shas are NOT false-positived. A fully repeated/obvious
|
|
254
|
+
// placeholder hex (deadbeef…, 000…) is also allowlisted.
|
|
255
|
+
if (isAllowlistPlaceholder(matched)) continue;
|
|
256
|
+
if (isLockfilePath(filePath)) continue;
|
|
257
|
+
// Canonical digest lengths — git SHA-1 (40) and SHA-256 (64) — are
|
|
258
|
+
// overwhelmingly legitimate hashes (commit refs, file checksums, content
|
|
259
|
+
// digests) and appear bare in changelogs/docs/lockfiles. Treating every
|
|
260
|
+
// isolated 40/64-hex run as a leak would false-positive on the entire git
|
|
261
|
+
// ecosystem, so these exact lengths are hash-shaped by default. The
|
|
262
|
+
// headline real-secret case (Twilio auth token = 32-hex) and other
|
|
263
|
+
// non-digest lengths (33–39, 41–63) are NOT standard digests and stay
|
|
264
|
+
// flagged. A leaked literal hash secret of EXACTLY 40/64 hex is the
|
|
265
|
+
// accepted blind spot of this length-based heuristic (documented tradeoff
|
|
266
|
+
// favouring zero false-positives on git/checksum hashes).
|
|
267
|
+
const hexLen = matched.length;
|
|
268
|
+
if (hexLen === 40 || hexLen === 64) continue;
|
|
269
|
+
// For non-canonical lengths, allowlist only when the SURROUNDING context
|
|
270
|
+
// carries a hash marker (integrity:, sha256-, "resolved", "checksum"). The
|
|
271
|
+
// window is widened on the left so a marker earlier on the line
|
|
272
|
+
// (e.g. `"resolved": "https://…/x.tgz#<hex>"`) is still seen.
|
|
273
|
+
const mIdx = text.indexOf(matched);
|
|
274
|
+
const before = text.slice(Math.max(0, mIdx - 64), mIdx);
|
|
275
|
+
const after = text.slice(mIdx + matched.length, mIdx + matched.length + 4);
|
|
276
|
+
if (HASH_CONTEXT_PATTERN.test(before + ' ' + after)) continue;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
findings.push({ name: descriptor.name, redacted: redactMatch(matched), kind: 'pattern' });
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// 2) Generic high-entropy detector (beyond named patterns)
|
|
283
|
+
if (entropyEnabled && !isLockfilePath(filePath)) {
|
|
284
|
+
const seen = new Set();
|
|
285
|
+
let match;
|
|
286
|
+
TOKEN_SHAPE.lastIndex = 0;
|
|
287
|
+
while ((match = TOKEN_SHAPE.exec(text)) !== null) {
|
|
288
|
+
const token = match[0];
|
|
289
|
+
if (token.length < ENTROPY_MIN_LEN) continue;
|
|
290
|
+
if (seen.has(token)) continue;
|
|
291
|
+
seen.add(token);
|
|
292
|
+
if (isAllowlistPlaceholder(token)) continue;
|
|
293
|
+
|
|
294
|
+
// Real high-entropy secrets are a long *contiguous* alphanumeric body.
|
|
295
|
+
// File paths (a/b/c), kebab/snake IDs (PROP-20260507-slug) and dotted
|
|
296
|
+
// slugs split into short segments at separators — they only look
|
|
297
|
+
// "high-entropy" as a whole. Require a contiguous run >= ENTROPY_MIN_LEN
|
|
298
|
+
// so example identifiers and paths in prose/docs stop tripping the net,
|
|
299
|
+
// while branded tokens (caught by NAMED_PATTERNS) and raw base64/hex
|
|
300
|
+
// bodies still qualify.
|
|
301
|
+
const longestRun = (token.match(/[A-Za-z0-9]+/g) || [])
|
|
302
|
+
.reduce((max, seg) => Math.max(max, seg.length), 0);
|
|
303
|
+
if (longestRun < ENTROPY_MIN_LEN) continue;
|
|
304
|
+
|
|
305
|
+
// For KEY=value tokens (e.g. DATABASE_URL=your-db-url) also check whether
|
|
306
|
+
// the value-side alone is a placeholder. The token shape regex includes '='
|
|
307
|
+
// so a whole assignment can be captured as one long token; if the value part
|
|
308
|
+
// looks like a placeholder the whole thing is safe.
|
|
309
|
+
const eqIdx = token.indexOf('=');
|
|
310
|
+
if (eqIdx > 0) {
|
|
311
|
+
const valueSide = token.slice(eqIdx + 1);
|
|
312
|
+
if (isAllowlistPlaceholder(valueSide)) continue;
|
|
313
|
+
// Generic "your-*" / "my-*" prefix heuristic covers cases not in the list.
|
|
314
|
+
const vsLower = valueSide.toLowerCase();
|
|
315
|
+
if (vsLower.startsWith('your-') || vsLower.startsWith('your_') ||
|
|
316
|
+
vsLower.startsWith('my-') || vsLower.startsWith('my_') ||
|
|
317
|
+
vsLower.startsWith('insert-') || vsLower.startsWith('put-')) continue;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const ent = shannonEntropy(token);
|
|
321
|
+
if (ent < ENTROPY_THRESHOLD) continue;
|
|
322
|
+
|
|
323
|
+
const ctxStart = Math.max(0, match.index - 24);
|
|
324
|
+
const context = text.slice(ctxStart, match.index + token.length + 4);
|
|
325
|
+
if (HASH_CONTEXT_PATTERN.test(context)) continue;
|
|
326
|
+
|
|
327
|
+
findings.push({
|
|
328
|
+
name: 'High-entropy string (suspected secret)',
|
|
329
|
+
redacted: redactMatch(token),
|
|
330
|
+
entropy: Number(ent.toFixed(2)),
|
|
331
|
+
kind: 'entropy',
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return findings;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function hasSecret(content, options = {}) {
|
|
340
|
+
return scanContent(content, options).length > 0;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
module.exports = {
|
|
344
|
+
NAMED_PATTERNS,
|
|
345
|
+
PLACEHOLDER_TOKENS,
|
|
346
|
+
EXAMPLE_HOST_PATTERN,
|
|
347
|
+
ENTROPY_THRESHOLD,
|
|
348
|
+
ENTROPY_MIN_LEN,
|
|
349
|
+
shannonEntropy,
|
|
350
|
+
isAllowlistPlaceholder,
|
|
351
|
+
isLockfilePath,
|
|
352
|
+
redactMatch,
|
|
353
|
+
scanContent,
|
|
354
|
+
hasSecret,
|
|
355
|
+
};
|