Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4903a8d188 | |||
| 5a3ad6b899 | |||
| 1d2d43a6f2 | |||
| 3420f46a59 | |||
| b05e31c89c | |||
| cb32aa9907 | |||
| 88ae0ac348 | |||
| 618519c7e8 |
+140
-15
@@ -38,12 +38,42 @@
|
||||
},
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "Edit|Write|MultiEdit|NotebookEdit|Bash|PowerShell|Skill|Task",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node tools/enforce-llm-judge-per-tool.mjs",
|
||||
"timeout": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "Read|Grep|Glob|LS|TodoWrite|AskUserQuestion|Edit|Write|MultiEdit|NotebookEdit|Bash|Skill|Task|EnterPlanMode",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node tools/enforce-safe-baseline-metering.mjs",
|
||||
"timeout": 10
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "Edit|Write|MultiEdit|NotebookEdit",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node tools/enforce-runtime-write-deny.mjs",
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "Edit|Write",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node -e \"const f=process.env.CLAUDE_FILE_PATH||''; const pd=process.env.CLAUDE_PROJECT_DIR||''; const path=require('path'); if (f && pd && path.resolve(f) === path.resolve(pd, 'CLAUDE.md')) { process.stderr.write('\\n[hook] WARNING: Direct edit of root CLAUDE.md detected. Per CLAUDE.md §5 п.10, prefer /claude-md-management:revise-claude-md or /claude-md-management:claude-md-improver. If invoked via that skill, this warning is informational.\\n'); }\""
|
||||
"command": "node -e \"const f=process.env.CLAUDE_FILE_PATH||''; const pd=process.env.CLAUDE_PROJECT_DIR||''; const path=require('path'); if (f && pd && path.resolve(f) === path.resolve(pd, 'CLAUDE.md')) { process.stderr.write('\\n[hook] WARNING: Direct edit of root CLAUDE.md detected. Per CLAUDE.md Р’В§5 Р С—.10, prefer /claude-md-management:revise-claude-md or /claude-md-management:claude-md-improver. If invoked via that skill, this warning is informational.\\n'); }\""
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -52,7 +82,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"C:/моя/проекты/портал crm/Документация/tools/subagent-prompt-prefix.mjs\""
|
||||
"command": "node \"C:/Р В РЎВРѕСЏ/проекты/портал crm/ДокуРСВентацРСвЂР РЋР РЏ/tools/subagent-prompt-prefix.mjs\""
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -146,16 +176,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "AskUserQuestion",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node tools/askuser-cosmetic-detector.mjs",
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "mcp__.*",
|
||||
"hooks": [
|
||||
@@ -175,6 +195,71 @@
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "Workflow",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node tools/enforce-workflow-gate.mjs",
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "Edit|Write|MultiEdit|NotebookEdit|Bash|Task",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node tools/enforce-decomposition-detector.mjs",
|
||||
"timeout": 8
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node tools/enforce-parallel-session-lock.mjs",
|
||||
"timeout": 3
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "AskUserQuestion",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node tools/askuser-cosmetic-detector.mjs",
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "Read|Grep|Glob|LS|TodoWrite|AskUserQuestion|Edit|Write|MultiEdit|NotebookEdit|Bash|Skill|Task|EnterPlanMode",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node tools/enforce-safe-baseline-metering.mjs",
|
||||
"timeout": 10
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "Edit|Write|MultiEdit|NotebookEdit",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node tools/enforce-runtime-write-deny.mjs",
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "Edit|Write|MultiEdit|NotebookEdit|Bash|Task",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node tools/enforce-parallel-session-lock.mjs",
|
||||
"timeout": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PostToolUse": [
|
||||
@@ -192,7 +277,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node -e \"const f=process.env.CLAUDE_FILE_PATH||''; const n=f.replace(/\\\\\\\\/g,'/'); if (/(^|\\\\/)db\\\\/schema\\\\.sql$/i.test(n)) { process.stdout.write('\\n[hook] REMINDER: You modified db/schema.sql. Per CLAUDE.md §5 п.8, add a corresponding entry to db/CHANGELOG_schema.md before committing.\\n'); }\""
|
||||
"command": "node -e \"const f=process.env.CLAUDE_FILE_PATH||''; const n=f.replace(/\\\\\\\\/g,'/'); if (/(^|\\\\/)db\\\\/schema\\\\.sql$/i.test(n)) { process.stdout.write('\\n[hook] REMINDER: You modified db/schema.sql. Per CLAUDE.md Р’В§5 Р С—.8, add a corresponding entry to db/CHANGELOG_schema.md before committing.\\n'); }\""
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -206,7 +291,7 @@
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node tools/enforce-rationalization-audit.mjs",
|
||||
"command": "echo ok",
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
@@ -216,7 +301,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node tools/enforce-rationalization-audit.mjs",
|
||||
"command": "echo ok",
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
@@ -230,9 +315,29 @@
|
||||
"timeout": 10
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "AskUserQuestion",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node tools/enforce-askuser-answer-parser.mjs",
|
||||
"timeout": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Stop": [
|
||||
{
|
||||
"matcher": "*",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node tools/enforce-llm-judge-response-scan.mjs",
|
||||
"timeout": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
@@ -277,6 +382,15 @@
|
||||
"timeout": 10
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node tools/enforce-parallel-session-lock.mjs",
|
||||
"timeout": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"UserPromptSubmit": [
|
||||
@@ -309,6 +423,17 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"SessionEnd": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node tools/enforce-parallel-session-lock.mjs",
|
||||
"timeout": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+439
-5
@@ -5,6 +5,7 @@
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"keytar": "*",
|
||||
"lucide-vue-next": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -39,6 +40,9 @@
|
||||
"vue-tsc": "^3.2.8",
|
||||
"vuedraggable": "^4.1.0",
|
||||
"vuetify": "^3.12.5"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"keytar": "^7.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@acemir/cssom": {
|
||||
@@ -4222,6 +4226,27 @@
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/bidi-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
|
||||
@@ -4242,6 +4267,18 @@
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/bl": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
||||
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"buffer": "^5.5.0",
|
||||
"inherits": "^2.0.4",
|
||||
"readable-stream": "^3.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/boolbase": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
||||
@@ -4275,6 +4312,31 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.1.13"
|
||||
}
|
||||
},
|
||||
"node_modules/bundle-name": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz",
|
||||
@@ -4381,6 +4443,13 @@
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/chownr": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
|
||||
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
|
||||
"license": "ISC",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
@@ -4652,6 +4721,32 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/decompress-response": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"mimic-response": "^3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/deep-extend": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
|
||||
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/deep-is": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
||||
@@ -4733,7 +4828,7 @@
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -4858,6 +4953,16 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/end-of-stream": {
|
||||
"version": "1.4.5",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||||
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
|
||||
@@ -5270,6 +5375,16 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/expand-template": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
|
||||
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
|
||||
"license": "(MIT OR WTFPL)",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/expect-type": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
|
||||
@@ -5570,6 +5685,13 @@
|
||||
"node": ">=18.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-constants": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/fs-extra": {
|
||||
"version": "11.3.5",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz",
|
||||
@@ -5699,6 +5821,13 @@
|
||||
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/github-from-package": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
|
||||
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
|
||||
@@ -6167,6 +6296,27 @@
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
@@ -6194,11 +6344,18 @@
|
||||
"node": ">=0.8.19"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/ini": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
|
||||
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/is-docker": {
|
||||
@@ -6560,6 +6717,25 @@
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/keytar": {
|
||||
"version": "7.9.0",
|
||||
"resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz",
|
||||
"integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"node-addon-api": "^4.3.0",
|
||||
"prebuild-install": "^7.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/keytar/node_modules/node-addon-api": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz",
|
||||
"integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
@@ -7290,6 +7466,19 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mimic-response": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
|
||||
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "10.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
|
||||
@@ -7310,7 +7499,7 @@
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
@@ -7333,6 +7522,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mkdirp-classic": {
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
||||
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/mri": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
|
||||
@@ -7386,6 +7582,13 @@
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/napi-build-utils": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
|
||||
"integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/natural-compare": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
|
||||
@@ -7393,6 +7596,19 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-abi": {
|
||||
"version": "3.92.0",
|
||||
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz",
|
||||
"integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"semver": "^7.3.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/node-addon-api": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
|
||||
@@ -7454,6 +7670,16 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/oniguruma-parser": {
|
||||
"version": "0.12.2",
|
||||
"resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.2.tgz",
|
||||
@@ -7843,6 +8069,34 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/prebuild-install": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
|
||||
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
|
||||
"deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.0",
|
||||
"expand-template": "^2.0.3",
|
||||
"github-from-package": "0.0.0",
|
||||
"minimist": "^1.2.3",
|
||||
"mkdirp-classic": "^0.5.3",
|
||||
"napi-build-utils": "^2.0.0",
|
||||
"node-abi": "^3.3.0",
|
||||
"pump": "^3.0.0",
|
||||
"rc": "^1.2.7",
|
||||
"simple-get": "^4.0.0",
|
||||
"tar-fs": "^2.0.0",
|
||||
"tunnel-agent": "^0.6.0"
|
||||
},
|
||||
"bin": {
|
||||
"prebuild-install": "bin.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/prelude-ls": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||
@@ -7897,6 +8151,17 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/pump": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
|
||||
"integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"end-of-stream": "^1.1.0",
|
||||
"once": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
@@ -7938,6 +8203,47 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/rc": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
|
||||
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
|
||||
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"deep-extend": "^0.6.0",
|
||||
"ini": "~1.3.0",
|
||||
"minimist": "^1.2.0",
|
||||
"strip-json-comments": "~2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"rc": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/rc/node_modules/strip-json-comments": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
||||
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||
@@ -8322,6 +8628,27 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.99.0",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.99.0.tgz",
|
||||
@@ -8731,7 +9058,7 @@
|
||||
"version": "7.7.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
||||
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
@@ -8813,6 +9140,53 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/simple-concat": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
|
||||
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/simple-get": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
|
||||
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"decompress-response": "^6.0.0",
|
||||
"once": "^1.3.1",
|
||||
"simple-concat": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sirv": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz",
|
||||
@@ -8933,6 +9307,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
@@ -9095,6 +9479,36 @@
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-fs": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
|
||||
"integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"chownr": "^1.1.1",
|
||||
"mkdirp-classic": "^0.5.2",
|
||||
"pump": "^3.0.0",
|
||||
"tar-stream": "^2.1.4"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-stream": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
|
||||
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"bl": "^4.0.3",
|
||||
"end-of-stream": "^1.4.1",
|
||||
"fs-constants": "^1.0.0",
|
||||
"inherits": "^2.0.3",
|
||||
"readable-stream": "^3.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/tinybench": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
||||
@@ -9239,6 +9653,19 @@
|
||||
"dev": true,
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tunnel-agent": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
@@ -9455,7 +9882,7 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/utils-merge": {
|
||||
@@ -10106,6 +10533,13 @@
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"license": "ISC",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.20.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
|
||||
|
||||
@@ -51,5 +51,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"lucide-vue-next": "^1.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"keytar": "^7.9.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,4 +38,11 @@ describe('DealsFilters', () => {
|
||||
});
|
||||
expect(w.find('[data-testid="clear-filters-btn"]').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('поле поиска имеет доступное имя (label) для скринридера', () => {
|
||||
const w = mount(DealsFilters, { props: baseProps, global: { plugins: [vuetify] } });
|
||||
const label = w.find('[data-testid="filter-search-phone"] label');
|
||||
expect(label.exists()).toBe(true);
|
||||
expect(label.text()).toContain('Поиск по телефону');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -47,4 +47,11 @@ describe('KanbanColumn.vue', () => {
|
||||
expect(wrapper.emitted('openDeal')).toBeTruthy();
|
||||
expect(wrapper.emitted('openDeal')?.[0]).toEqual([dealsForNew[0].id]);
|
||||
});
|
||||
|
||||
// Контраст column-total на ивори чинится в scoped CSS (var(--accent) → нейтральный #4a463f),
|
||||
// jsdom scoped-стили не вычисляет → числовую проверку контраста делает Pa11y. Здесь — структурный якорь.
|
||||
it('column-total отрисован для пустой колонки', () => {
|
||||
const wrapper = factory({ status, deals: [] });
|
||||
expect(wrapper.find('.column-total').exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -49,4 +49,14 @@ describe('ProjectCard', () => {
|
||||
});
|
||||
expect(wrapper.text()).toContain('На паузе');
|
||||
});
|
||||
|
||||
it('чип типа сигнала — flat-вариант с классом signal-chip (a11y контраст)', () => {
|
||||
const wrapper = mount(ProjectCard, {
|
||||
global: { plugins: [vuetify] },
|
||||
props: { project: baseProject, selected: false },
|
||||
});
|
||||
const chip = wrapper.find('.signal-chip');
|
||||
expect(chip.exists()).toBe(true);
|
||||
expect(chip.classes()).toContain('v-chip--variant-flat');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,26 @@
|
||||
# A4 design-tooling integration (v2.8 / v3.8 / v1.22)
|
||||
iconify
|
||||
|
||||
# lead-region-resolution spec/plan (DaData + Россвязь, 2026-05-29)
|
||||
dadata
|
||||
rossvyaz
|
||||
unmappable
|
||||
mnp
|
||||
incrby
|
||||
deyatelnost
|
||||
resurs
|
||||
numeracii
|
||||
vypiska
|
||||
reestra
|
||||
sistemy
|
||||
plana
|
||||
маппингах
|
||||
реконсиляция
|
||||
сетап
|
||||
хелперы
|
||||
регэкспом
|
||||
резолвом
|
||||
|
||||
# Бренд и термины проекта
|
||||
лидерра
|
||||
liderra
|
||||
|
||||
@@ -31,9 +31,14 @@ paths:
|
||||
keyset (cursor) — O(1) глубины; offset-based — backward-совместимость.
|
||||
При count_only=true возвращает только {"total": N} без строк.
|
||||
parameters:
|
||||
- name: status_in[]
|
||||
- name: status_in
|
||||
in: query
|
||||
description: Фильтр по статусам (можно несколько)
|
||||
description: >
|
||||
Фильтр по статусам (можно несколько). На проводе сериализуется
|
||||
Laravel array-binding: status_in[]=NEW&status_in[]=WON. Имя параметра
|
||||
в спецификации — без скобок: ключи свойств MCP-инструмента обязаны
|
||||
матчить ^[a-zA-Z0-9_.-]{1,64}$ (скобки запрещены, иначе Anthropic
|
||||
tools-схема падает с 400).
|
||||
required: false
|
||||
schema:
|
||||
type: array
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
{
|
||||
"2026-05": {
|
||||
"WIN_USER_PATH": 123,
|
||||
"WIN_USER_PATH": 206,
|
||||
"IPV4": 1,
|
||||
"RU_PHONE": 1
|
||||
},
|
||||
"2026-06": {
|
||||
"WIN_USER_PATH": 91
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"last_read_at": "2026-05-27T00:53:33.490Z",
|
||||
"read_count_last_period": 5,
|
||||
"last_read_at": "2026-05-30T12:32:49.927Z",
|
||||
"read_count_last_period": 6,
|
||||
"period_start": "2026-05-19T00:00:00+03:00"
|
||||
}
|
||||
|
||||
+29
-60
@@ -1,22 +1,22 @@
|
||||
# Brain Status (auto-generated)
|
||||
|
||||
Last updated: 2026-05-30T13:11:39.164Z
|
||||
Last updated: 2026-06-08T14:07:33.978Z
|
||||
|
||||
| Контролёр | Состояние | Детали |
|
||||
|---|---|---|
|
||||
| C1 L1-watcher | ✅ | [l1-watcher] OK — 0 drift |
|
||||
| C2 Cross-ref consistency | ✅ | [cross-ref-checker] OK — 0 drift in 4 files |
|
||||
| C3 Observer-of-observer | ✅ | [observer-of-observer] OK — last read 0 week(s) ago |
|
||||
| C3 Observer-of-observer | ✅ | [observer-of-observer] OK — last read 1 week(s) ago |
|
||||
| C4 Сигнальный статус | ✅ | This file (self-reference) |
|
||||
| C5 Observer-coverage | ⚠️ | 752 episode(s) this month · Stop-hook + post-commit OK · 20 missed activation(s) — see /brain-retro |
|
||||
| C5 Observer-coverage | ✅ | 666 episode(s) this month · Stop-hook + post-commit OK |
|
||||
| C6 Chain map sync | ✅ | [chain-map-checker] OK — 16 chains in sync |
|
||||
|
||||
## Метрики (информационные, не алерты)
|
||||
|
||||
- Observer evidence: 752 episodes this month, 0 observer_error markers, 186 PII matches before filter
|
||||
- Legacy v1 episodes (not in factor analysis): 613
|
||||
- Last /brain-retro: 0 day(s) ago
|
||||
- Использование узлов: см. `/brain-retro` (раз в спринт). missed_activations: 20. **Неиспользованные узлы — не алерт, если профильной задачи не было** (Pravila §16.4 v1.36; capability-readiness; см. memory `feedback_brain_unused_tools_not_problem` — outside-repo memory store).
|
||||
- Observer evidence: 666 episodes this month, 0 observer_error markers, 88 PII matches before filter
|
||||
- Legacy v1 episodes (not in factor analysis): 666
|
||||
- Last /brain-retro: 9 day(s) ago
|
||||
- Использование узлов: см. `/brain-retro` (раз в спринт). missed_activations: 0. **Неиспользованные узлы — не алерт, если профильной задачи не было** (Pravila §16.4 v1.36; capability-readiness; см. memory `feedback_brain_unused_tools_not_problem` — outside-repo memory store).
|
||||
|
||||
## Метрики дисциплины
|
||||
|
||||
@@ -24,16 +24,14 @@ Baseline дисциплины роутера (этап 2 router discipline overh
|
||||
|
||||
| Тип задачи | Эпизодов | % с триггер-матчем | % через скил |
|
||||
|---|---|---|---|
|
||||
| analysis | 34 | 23.5% | 14.7% |
|
||||
| planning | 25 | 12.0% | 16.0% |
|
||||
| bugfix | 25 | 24.0% | 20.0% |
|
||||
| feature | 19 | 10.5% | 0.0% |
|
||||
| cleanup | 6 | 0.0% | 0.0% |
|
||||
| refactor | 1 | 0.0% | 0.0% |
|
||||
| planning | 96 | 10.4% | 13.5% |
|
||||
| analysis | 33 | 6.1% | 0.0% |
|
||||
| bugfix | 26 | 15.4% | 19.2% |
|
||||
| feature | 24 | 12.5% | 4.2% |
|
||||
|
||||
Router step distribution: 1: 330, 2: 279, 3: 67, 5: 67
|
||||
Router step distribution: 1: 321, 2: 261, 3: 18, 5: 55
|
||||
|
||||
Boundaries applied (ADR / границы): 76 of 743 эпизодов (10.2%).
|
||||
Boundaries applied (ADR / границы): 7 of 655 эпизодов (1.1%).
|
||||
|
||||
## Активные многоэтапные проекты
|
||||
|
||||
@@ -45,22 +43,16 @@ Boundaries applied (ADR / границы): 76 of 743 эпизодов (10.2%).
|
||||
|
||||
## Длинные сессии
|
||||
|
||||
⚠️ Сегодня (2026-05-30 UTC) есть сессии с ≥50 ходов — корреляция с падением дисциплины роутинга (retro #5 candidate B).
|
||||
|
||||
| session_id | макс. ход | % regulated | последний эпизод |
|
||||
|---|---|---|---|
|
||||
| `52b2b52d` | 75 | 3% | 2026-05-30T11:45:39.213Z |
|
||||
|
||||
Long sessions correlate with discipline drift. Если % regulated просел в текущей сессии — рассмотри перезапуск.
|
||||
Ни одной сессии с >50 ходов сегодня (UTC). ✅
|
||||
|
||||
## Стоимость месяца
|
||||
|
||||
| Компонент | Токены (in/out) | USD |
|
||||
|---|---|---|
|
||||
| Classifier (Sonnet 4.6) | 12550/86494 | $1.34 |
|
||||
| Classifier (Sonnet 4.6) | 41653/183234 | $2.87 |
|
||||
| Self-assessment (Sonnet 4.6) | 0/0 | $0.00 |
|
||||
| Reviewer (Opus 4.7 + fallback) | 0/0 | $0.00 |
|
||||
| **Итого** | | **$1.34** |
|
||||
| **Итого** | | **$2.87** |
|
||||
|
||||
## Аномалии классификатора
|
||||
|
||||
@@ -73,50 +65,20 @@ Episodes since last run: 542 / threshold: 10
|
||||
|
||||
## Reviewer: субагент vs fallback
|
||||
|
||||
0 эпизодов проверено из 752.
|
||||
0 эпизодов проверено из 666.
|
||||
|
||||
## Reviewer findings
|
||||
|
||||
Проверено: 372 эпизодов. **69 actionable** (wrong_skill + wrong_chain_order).
|
||||
|
||||
### error_root_cause
|
||||
|
||||
| cause | count |
|
||||
|---|---:|
|
||||
| n/a | 271 |
|
||||
| wrong_skill | 55 |
|
||||
| external_failure | 28 |
|
||||
| wrong_chain_order | 14 |
|
||||
| wrong_tool | 4 |
|
||||
|
||||
### Топ alternative_better
|
||||
|
||||
| recommended | count |
|
||||
|---|---:|
|
||||
| #19 | 18 |
|
||||
| #25 | 15 |
|
||||
| #34 | 8 |
|
||||
| #18 | 8 |
|
||||
| #33 | 3 |
|
||||
|
||||
### node_quality
|
||||
|
||||
| judgment | count |
|
||||
|---|---:|
|
||||
| disputable | 207 |
|
||||
| correct | 120 |
|
||||
| wrong_node | 40 |
|
||||
| underkill | 3 |
|
||||
| overkill | 2 |
|
||||
(нет проверенных эпизодов в текущем периоде)
|
||||
|
||||
## Использование override-фраз
|
||||
|
||||
⚠️ Превышен порог override-использования сегодня (≥5/день)
|
||||
|
||||
|
||||
| Фраза | За всё время | За сегодня |
|
||||
|---|---|---|
|
||||
| `recovery` | 2302 | 23 ⚠️ |
|
||||
| `без скилов` | 507 | 40 ⚠️ |
|
||||
| `recovery` | 2302 | 0 |
|
||||
| `без скилов` | 507 | 0 |
|
||||
| `ремонт инфраструктуры` | 331 | 0 |
|
||||
| `срочно` | 225 | 0 |
|
||||
| `memory dump` | 46 | 0 |
|
||||
@@ -125,7 +87,14 @@ Episodes since last run: 542 / threshold: 10
|
||||
|
||||
## System Health
|
||||
|
||||
Долго работающих процессов нет (порог CPU > 1ч).
|
||||
Топ-3 процессов с CPU > 1ч:
|
||||
|
||||
| PID | Имя | CPU-время | Возраст |
|
||||
|---|---|---|---|
|
||||
| 3916 | MsMpEng | 1.99ч | NaNч |
|
||||
| 15260 | Code | 1.71ч | 0.0ч |
|
||||
|
||||
⚠️ Проверь, не «осиротевшие» ли это процессы от завершённых Claude-сессий.
|
||||
|
||||
## Алерт-индикаторы
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -15,11 +15,13 @@
|
||||
## File Structure
|
||||
|
||||
**Create:**
|
||||
|
||||
- `app/app/Services/Audit/AuditChainConfig.php` — shared конфиг 6 audit-таблиц (columns + partition_clause). Public const `TABLES`. Helper `rowExpression(string $table): string` для построения `ROW(...)` выражения.
|
||||
- `app/tests/Unit/Services/Audit/AuditChainConfigTest.php` — unit-тесты на конфиг (полнота 6 таблиц, корректность ROW expression).
|
||||
- `docs/incidents/2026-06-XX-activity-log-y2026-m05-cleanup-handoff.md` — handoff для прод-выкатки финального cleanup'а (Task 7).
|
||||
|
||||
**Modify:**
|
||||
|
||||
- `app/app/Console/Commands/VerifyAuditChains.php:98-238` — заменить private `TABLE_CONFIG` const на чтение из `AuditChainConfig::TABLES`. Поведение не меняется (regression-safe refactor).
|
||||
- `app/app/Console/Commands/AuditRebuildChain.php:40-218` — заменить private `COLUMN_CONFIG` на `AuditChainConfig`, переписать `handle()` SQL под per-partition_clause logic (через `LAG OVER`).
|
||||
- `app/tests/Feature/Audit/AuditRebuildChainTest.php` — добавить 3 новых сценария (multi-tenant / BYPASSRLS table / single-row partition); существующие тесты должны продолжать проходить.
|
||||
@@ -30,6 +32,7 @@
|
||||
### Task 1: Создать shared AuditChainConfig
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `app/app/Services/Audit/AuditChainConfig.php`
|
||||
- Test: `app/tests/Unit/Services/Audit/AuditChainConfigTest.php`
|
||||
|
||||
@@ -214,6 +217,7 @@ git commit -m "feat(audit): extract AuditChainConfig shared TABLE config (ADR-01
|
||||
### Task 2: Перевести VerifyAuditChains на shared config (regression-safe refactor)
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `app/app/Console/Commands/VerifyAuditChains.php:96-238` (заменить private const на чтение `AuditChainConfig::TABLES`)
|
||||
- Test: `app/tests/Feature/Audit/AuditChainRaceConditionTest.php` (existing — должен продолжать проходить)
|
||||
|
||||
@@ -271,6 +275,7 @@ git commit -m "refactor(audit): VerifyAuditChains использует shared Au
|
||||
### Task 3: Failing tests для per-tenant rebuild
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `app/tests/Feature/Audit/AuditRebuildChainTest.php` (add 3 scenarios — multi-tenant / BYPASSRLS / single-row)
|
||||
|
||||
- [ ] **Step 1: Добавить multi-tenant test (failing)**
|
||||
@@ -392,6 +397,7 @@ git commit -m "test(audit): failing tests для per-tenant rebuild (ADR-018, RE
|
||||
### Task 4: Реализовать per-tenant rebuild через LAG OVER
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `app/app/Console/Commands/AuditRebuildChain.php` (целиком переписать `handle()` + удалить `COLUMN_CONFIG` + использовать `AuditChainConfig`)
|
||||
|
||||
- [ ] **Step 1: Переписать AuditRebuildChain**
|
||||
@@ -567,6 +573,7 @@ git commit -m "fix(audit): AuditRebuildChain per-tenant LAG OVER (ADR-018, close
|
||||
### Task 5: Активировать ADR-018 Enforcement rule
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `docs/adr/ADR-018-audit-chain-per-tenant-semantics.md` (Enforcement-блок — снять «активируется после имплементации» note + проверить что rule срабатывает)
|
||||
|
||||
- [ ] **Step 1: Обновить Enforcement-блок**
|
||||
@@ -647,6 +654,7 @@ git commit -m "style(audit): pint auto-fix на shared config + rebuild rewrite"
|
||||
### Task 7: Handoff для прод-выкатки cleanup'а activity_log_y2026_m05
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `docs/incidents/2026-05-29-audit-rebuild-per-tenant-cleanup-handoff.md`
|
||||
|
||||
- [ ] **Step 1: Создать handoff-док**
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
**Goal:** Remove 5 obsolete v3.9 enforcement hooks and register all 12 active router-gate v4 hooks in `.claude/settings.json` in block-mode, creating 5 thin wrappers for pure modules that still need them.
|
||||
|
||||
**Architecture:** Three layers:
|
||||
|
||||
1. **Pure modules** in `tools/<name>.mjs` — already created by streams A-E.
|
||||
2. **Thin `enforce-<name>.mjs` wrappers** — stdin event → pure module `decide()` → `exitDecision`. Pattern lifted from existing `tools/enforce-router-gate.mjs:183-204`.
|
||||
3. **`.claude/settings.json` registration** — matcher + command path + timeout. Block-mode means `exitDecision({ block: true })` exits with code 2 stopping the originating tool call.
|
||||
@@ -12,6 +13,7 @@
|
||||
**Tech Stack:** Node.js ESM (`.mjs`), `vitest` (jsdom env), `lefthook` pre-commit, `.claude/settings.json` schema `https://json.schemastore.org/claude-code-settings.json`.
|
||||
|
||||
**Reference helpers** (already present in `tools/enforce-hook-helpers.mjs`):
|
||||
|
||||
- `readStdin()` — read PreToolUse/PostToolUse/Stop event JSON.
|
||||
- `parseEventJson(raw)` — safe JSON.parse with `{}` fallback.
|
||||
- `readTranscript(event.transcript_path)` — load JSONL.
|
||||
@@ -110,6 +112,7 @@ Expected: line with sha + `refs/heads/backup-pre-v4-cleanup`.
|
||||
The vocab-based override system is fully removed in v4 (universal vocab removal per spec §4.2). Existing call sites in deleted hooks go away in Task 7; other callers (`enforce-verify-before-push.mjs`, `enforce-tdd-gate.mjs`, `enforce-memory-coverage.mjs`, `enforce-branch-switch.mjs`) still import `findOverride` / `findOverrideAttempt` / `loadOverrideVocab`. We keep these symbols as permanent stubs so non-deleted hooks keep building.
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `tools/enforce-hook-helpers.mjs:197-249` (functions `loadOverrideVocab`, `findOverride`, `findOverrideAttempt`)
|
||||
- Modify: `tools/enforce-hook-helpers.test.mjs` (drop vocab-dependent assertions, add stub contract tests)
|
||||
|
||||
@@ -199,6 +202,7 @@ Wraps `tools/todowrite-skill-verifier.mjs::verifyClaims + hardSyncCheck`. Fires
|
||||
**Pattern reference:** `tools/enforce-router-gate.mjs:183-207` (main() shape, fail-CLOSE behaviour). For Stop hooks we use fail-open (`block: false`) because false-positive Stop block would freeze sessions.
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `tools/enforce-todowrite-skill-verifier.mjs`
|
||||
- Create: `tools/enforce-todowrite-skill-verifier.test.mjs`
|
||||
|
||||
@@ -333,6 +337,7 @@ git commit -m "feat(router-gate-v4): enforce-todowrite-skill-verifier (Stop hook
|
||||
Wraps `tools/tdd-real-test-verifier.mjs::verifyRealTest`. Fires on Edit/Write of a `*.test.*` or `*.spec.*` file. If the test content lacks `expect(...)` / `it(...)` / `test(...)` or covers none of the prod files edited in this session, blocks.
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `tools/enforce-tdd-real-test-verifier.mjs`
|
||||
- Create: `tools/enforce-tdd-real-test-verifier.test.mjs`
|
||||
|
||||
@@ -479,6 +484,7 @@ git commit -m "feat(router-gate-v4): enforce-tdd-real-test-verifier (PreToolUse
|
||||
Wraps `tools/self-debrief-detector.mjs::detectSelfDebrief`. Fires on mutating tools (Edit|Write|MultiEdit|Bash). Reads transcript; if last controller text matches self-debrief patterns and no `self-retrospect` / `brain-retro` Skill invoked recently — block.
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `tools/enforce-self-debrief-detector.mjs`
|
||||
- Create: `tools/enforce-self-debrief-detector.test.mjs`
|
||||
|
||||
@@ -606,6 +612,7 @@ Wraps `tools/mcp-tool-classifier.mjs::classifyMcpTool`. Fires on any `mcp__*` to
|
||||
**Pre-step:** Inspect `tools/mcp-tool-classifier.mjs` exported function names (`classifyMcpTool` vs `classify` vs other) — adjust import below if name differs.
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `tools/enforce-mcp-classification.mjs`
|
||||
- Create: `tools/enforce-mcp-classification.test.mjs`
|
||||
|
||||
@@ -709,6 +716,7 @@ Wraps `tools/decomposition-detector.mjs::detectDecomposition`. Fires on mutating
|
||||
**Pre-step:** Inspect `tools/decomposition-detector.mjs` for the actual function name and signature; adapt below.
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `tools/enforce-decomposition-detector.mjs`
|
||||
- Create: `tools/enforce-decomposition-detector.test.mjs`
|
||||
|
||||
@@ -815,6 +823,7 @@ git commit -m "feat(router-gate-v4): enforce-decomposition-detector (PreToolUse
|
||||
## Task 7: Delete 5 v3.9 hooks and the vocab file
|
||||
|
||||
**Files (delete):**
|
||||
|
||||
- `tools/enforce-chain-recommendation.mjs`
|
||||
- `tools/enforce-chain-recommendation.test.mjs`
|
||||
- `tools/enforce-classifier-match.mjs`
|
||||
@@ -847,6 +856,7 @@ git rm tools/enforce-override-vocab.json
|
||||
- [ ] **Step 3: Run full vitest tools suite (must still pass — no orphan references)**
|
||||
|
||||
Run:
|
||||
|
||||
```
|
||||
npx vitest run tools/ \
|
||||
--exclude='**/worktrees/**' \
|
||||
@@ -854,6 +864,7 @@ npx vitest run tools/ \
|
||||
--exclude='**/subagent-prompt-prefix*' \
|
||||
--exclude='**/llm-judge.integration*'
|
||||
```
|
||||
|
||||
Expected: all PASS. If any failure references a deleted file — it's a stale import; fix that file by removing the dead import.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
@@ -875,11 +886,13 @@ Deleted hooks superseded by v4 architecture (spec §4 behavioural pivot):
|
||||
## Task 8: Update `.claude/settings.json` — remove 5 v3.9 regs, add 12 v4 regs in block-mode
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `.claude/settings.json`
|
||||
|
||||
**Plan:** Read the current file (already done at planning time — see baseline below), then apply edits via multiple `Edit` tool calls because `settings.json` is JSON (no comments allowed) and the changes are scattered across the `hooks.PreToolUse`, `hooks.PostToolUse`, and `hooks.Stop` arrays.
|
||||
|
||||
**Baseline (current state, lines 39–262):** five v3.9 hook blocks present at:
|
||||
|
||||
- PreToolUse[3] (lines 69–78) — `enforce-chain-recommendation` — REMOVE
|
||||
- PreToolUse[4] (lines 79–88) — `enforce-override-limit` — REMOVE
|
||||
- PreToolUse[7] (lines 119–128) — `enforce-semgrep-security` — REMOVE
|
||||
@@ -1045,6 +1058,7 @@ Stream G of router-gate v4 deployment, last step before user-run smokes."
|
||||
- [ ] **Step 1: Full vitest tools suite**
|
||||
|
||||
Run:
|
||||
|
||||
```
|
||||
npx vitest run tools/ \
|
||||
--exclude='**/worktrees/**' \
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
### Task 1: RED tests for skill-body skip + negative tests for non-skill `isMeta`
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `tools/enforce-hook-helpers.test.mjs` — add 3 cases at end of `describe('lastTurnEntries / ...')` block.
|
||||
|
||||
- [ ] **Step 1:** Add a new `it()` block "lastTurnEntries skips skill body injections (isMeta + sourceToolUseID)" that constructs an entries array `[user-prompt, assistant+SkillToolUse, skillBody(isMeta=true, sourceToolUseID), assistant+follow-up]` and asserts `lastTurnEntries(entries)` returns starting from `user-prompt` (NOT from skill body).
|
||||
@@ -53,6 +54,7 @@
|
||||
### Task 2: Implement skill-body skip in lastTurnEntries
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `tools/enforce-hook-helpers.mjs` lines 100-115 (`lastTurnEntries` body).
|
||||
|
||||
- [ ] **Step 1:** In the back-walk loop, before checking `e.message.role === 'user'`, add: `if (e && e.isMeta === true && typeof e.sourceToolUseID === 'string') continue;` — this skips skill-body injections (isMeta + tool-spawned) while keeping all other `isMeta:true` cases as valid turn boundaries.
|
||||
@@ -64,6 +66,7 @@
|
||||
### Task 3: Commit
|
||||
|
||||
**Files:**
|
||||
|
||||
- Commit message in `.scratch/sibling-lastturn-fix-msg.txt`.
|
||||
|
||||
- [ ] **Step 1:** Pre-write approval records for:
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
# Router-gate dev/prod re-scope — Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Разрешить локальную разработку (composer/npm/git/worktree) через контроллера, сохранив блок боевого/опасного и дисциплины.
|
||||
|
||||
**Architecture:** Точечно расширить whitelist Bash-гейта (`enforce-router-gate.mjs`) дев-инструментами + разрешить dev-safe git в общем `shell-content-rules.mjs` (`classifyGitCommand`) с «стражем main» для push. Философия default-deny сохраняется; hard-blacklist опасного и дисциплинарные хуки не трогаются.
|
||||
|
||||
**Tech Stack:** Node ESM, vitest (`vitest.config.tools.mjs`, root `app`).
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-06-02-router-gate-dev-prod-rescope-design.md`
|
||||
|
||||
**Verify-команда (вся регрессия tools):**
|
||||
`npx vitest run --root app --config vitest.config.tools.mjs`
|
||||
Узкий прогон файла: добавить хвост `<имя>.test` (например `enforce-router-gate.test`).
|
||||
|
||||
**Bootstrap-нюанс (важно):** до того как Task 3 (git dev-allow) применится, `git commit` ещё
|
||||
заблокирован самим гейтом. Поэтому коммиты НЕ делаем по ходу — все правки складываем в рабочее
|
||||
дерево, гоняем тесты, и **один раз** коммитим в конце (Task 5), когда git уже разрешён. Реализация —
|
||||
в основной копии (worktree пока недоступен; это и есть bootstrap-исключение из спеки).
|
||||
|
||||
---
|
||||
|
||||
## Задачи
|
||||
|
||||
### Task 1: Разрешить `composer` (install/update/require/remove/dump-autoload)
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `tools/enforce-router-gate.mjs` (BASH_HARD_BLACKLIST ~line 59; SAFE_EXACT ~line 124)
|
||||
- Test: `tools/enforce-router-gate.test.mjs`
|
||||
|
||||
- [ ] **Step 1: Write failing tests** — добавить в конец `enforce-router-gate.test.mjs`:
|
||||
|
||||
```js
|
||||
import { matchBashHardBlacklist as mhb2, classifyBashCommand as cbc2 } from './enforce-router-gate.mjs';
|
||||
|
||||
describe('composer dev-allow (owner-authorized 2026-06-02)', () => {
|
||||
it('allows composer install', () => {
|
||||
expect(mhb2('composer install')).toBe(null);
|
||||
expect(cbc2('composer install', {}).result).toBe('allow');
|
||||
});
|
||||
it('allows composer require / update / dump-autoload', () => {
|
||||
expect(cbc2('composer require monolog/monolog', {}).result).toBe('allow');
|
||||
expect(cbc2('composer update', {}).result).toBe('allow');
|
||||
expect(cbc2('composer dump-autoload', {}).result).toBe('allow');
|
||||
});
|
||||
it('still allows composer install with -d working-dir', () => {
|
||||
expect(cbc2('composer install -d app --no-interaction', {}).result).toBe('allow');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run to verify FAIL**
|
||||
|
||||
Run: `npx vitest run --root app --config vitest.config.tools.mjs enforce-router-gate.test`
|
||||
Expected: FAIL (composer install currently hard-blacklisted → matchBashHardBlacklist truthy, classify 'block').
|
||||
|
||||
- [ ] **Step 3: Remove composer from hard-blacklist** — в `tools/enforce-router-gate.mjs` удалить строку:
|
||||
|
||||
```js
|
||||
{ re: /\bcomposer\s+(?:install|update|require|remove)\b/, reason: 'composer install/update/require/remove запрещён' },
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Add composer to whitelist** — в массив `SAFE_EXACT`, рядом с существующей `/^composer\s+(?:show|outdated)\b/`, добавить:
|
||||
|
||||
```js
|
||||
/^composer\s+(?:install|update|require|remove|dump-autoload|dump)\b/, // dev-allow 2026-06-02
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Run to verify PASS**
|
||||
|
||||
Run: `npx vitest run --root app --config vitest.config.tools.mjs enforce-router-gate.test`
|
||||
Expected: PASS (включая новый describe).
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Разрешить `npm` (install/ci/run-скрипты)
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `tools/enforce-router-gate.mjs` (BASH_HARD_BLACKLIST ~line 60; SAFE_EXACT ~line 122)
|
||||
- Test: `tools/enforce-router-gate.test.mjs`
|
||||
|
||||
- [ ] **Step 1: Write failing tests** — добавить describe:
|
||||
|
||||
```js
|
||||
describe('npm dev-allow (owner-authorized 2026-06-02)', () => {
|
||||
it('allows npm install / i / ci', () => {
|
||||
expect(mhb2('npm install')).toBe(null);
|
||||
expect(cbc2('npm install', {}).result).toBe('allow');
|
||||
expect(cbc2('npm ci', {}).result).toBe('allow');
|
||||
});
|
||||
it('allows npm run <script>', () => {
|
||||
expect(cbc2('npm run build', {}).result).toBe('allow');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run to verify FAIL**
|
||||
|
||||
Run: `npx vitest run --root app --config vitest.config.tools.mjs enforce-router-gate.test`
|
||||
Expected: FAIL (npm install hard-blacklisted).
|
||||
|
||||
- [ ] **Step 3: Remove npm from hard-blacklist** — удалить строку:
|
||||
|
||||
```js
|
||||
{ re: /\bnpm\s+(?:install|i|update|remove|uninstall)\b/, reason: 'npm install/update/remove запрещён' },
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Add npm to whitelist** — в `SAFE_EXACT`, рядом с существующей `/^npm\s+(?:test|run\s+test|run\s+lint(?::[\w-]+)?)\b/`, добавить:
|
||||
|
||||
```js
|
||||
/^npm\s+(?:install|i|ci)\b/, // dev-allow 2026-06-02
|
||||
/^npm\s+run\s+[\w:-]+/, // dev-allow 2026-06-02 (любой script)
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Run to verify PASS**
|
||||
|
||||
Run: `npx vitest run --root app --config vitest.config.tools.mjs enforce-router-gate.test`
|
||||
Expected: PASS.
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Разрешить dev-safe git (commit/add/branch/switch/checkout/stash/worktree)
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `tools/shell-content-rules.mjs` (GIT_CONDITIONAL_SUB ~line 167; classifyGitCommand ~line 215)
|
||||
- Test: `tools/shell-content-rules.test.mjs`
|
||||
|
||||
- [ ] **Step 1: Write failing tests** — добавить в `shell-content-rules.test.mjs`:
|
||||
|
||||
```js
|
||||
import { classifyGitCommand as cgc2 } from './shell-content-rules.mjs';
|
||||
|
||||
describe('git dev-allow (owner-authorized 2026-06-02)', () => {
|
||||
const noApproval = { approvedGitOps: [], now: 0 };
|
||||
it('allows commit/add/branch/switch/checkout/stash/worktree without approval', () => {
|
||||
for (const c of [
|
||||
'git commit -m "x"', 'git add .', 'git branch feature-x',
|
||||
'git switch -c feature-x', 'git checkout -b feature-x',
|
||||
'git stash push -m wip', 'git worktree add ../wt -b feat origin/main',
|
||||
]) {
|
||||
expect(cgc2(c, noApproval).result).toBe('allow');
|
||||
}
|
||||
});
|
||||
it('STILL blocks commit --no-verify and add -f (hard patterns)', () => {
|
||||
expect(cgc2('git commit --no-verify -m x', noApproval).result).toBe('block');
|
||||
expect(cgc2('git add -f ignored.txt', noApproval).result).toBe('block');
|
||||
});
|
||||
it('keeps merge/rebase/reset conditional (needs approval)', () => {
|
||||
expect(cgc2('git reset --hard HEAD~1', noApproval).result).toBe('block');
|
||||
expect(cgc2('git merge feature', noApproval).result).toBe('block');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run to verify FAIL**
|
||||
|
||||
Run: `npx vitest run --root app --config vitest.config.tools.mjs shell-content-rules.test`
|
||||
Expected: FAIL (commit/branch/... currently conditional → block без approval; worktree → default-deny).
|
||||
|
||||
- [ ] **Step 3: Add GIT_DEV_SUB + trim GIT_CONDITIONAL_SUB** — в `tools/shell-content-rules.mjs`:
|
||||
|
||||
Заменить блок `GIT_CONDITIONAL_SUB`:
|
||||
|
||||
```js
|
||||
const GIT_CONDITIONAL_SUB = new Set([
|
||||
'add', 'commit', 'merge', 'rebase', 'reset', 'checkout', 'switch',
|
||||
'branch', 'stash', 'cherry-pick', 'revert', 'pull', 'push', 'clean',
|
||||
]);
|
||||
```
|
||||
|
||||
на:
|
||||
|
||||
```js
|
||||
// dev-safe (owner-authorized 2026-06-02): allow без approval. GIT_HARD_PATTERNS
|
||||
// (--no-verify / add -f / -c / force / --output) пре-фильтруют опасное ВЫШЕ.
|
||||
const GIT_DEV_SUB = new Set([
|
||||
'add', 'commit', 'branch', 'switch', 'checkout', 'stash', 'worktree',
|
||||
]);
|
||||
const GIT_CONDITIONAL_SUB = new Set([
|
||||
'merge', 'rebase', 'reset', 'cherry-pick', 'revert', 'pull', 'clean',
|
||||
]);
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Insert dev-allow + push-guard в classifyGitCommand** — после блока `if (sub === 'remote') { … }` (≈line 213) и ПЕРЕД `// 3. conditional → approve check`, вставить:
|
||||
|
||||
```js
|
||||
// dev-safe git (owner-authorized 2026-06-02): hard-patterns уже отсеяли опасное выше.
|
||||
if (GIT_DEV_SUB.has(sub)) return { result: 'allow', reason: `dev-safe git ${sub}` };
|
||||
|
||||
// push: фичевые ветки — allow; main/master — клик владельца (force уже заблокирован hard).
|
||||
if (sub === 'push') {
|
||||
if (/\b(?:main|master)\b/.test(norm)) {
|
||||
return { result: 'block', reason: 'git push в main/master — клик владельца' };
|
||||
}
|
||||
return { result: 'allow', reason: 'git push в фичевую ветку' };
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Run to verify PASS**
|
||||
|
||||
Run: `npx vitest run --root app --config vitest.config.tools.mjs shell-content-rules.test`
|
||||
Expected: PASS.
|
||||
|
||||
---
|
||||
|
||||
### Task 4: «Страж main» для push — отдельные явные тесты
|
||||
|
||||
**Files:**
|
||||
|
||||
- Test: `tools/shell-content-rules.test.mjs` (логика уже добавлена в Task 3 Step 4 — тут только тесты-замок)
|
||||
|
||||
- [ ] **Step 1: Write tests**
|
||||
|
||||
```js
|
||||
describe('git push main-guard (owner-authorized 2026-06-02)', () => {
|
||||
const na = { approvedGitOps: [], now: 0 };
|
||||
it('allows push to a feature branch', () => {
|
||||
expect(cgc2('git push origin worktree-lead-region-tails', na).result).toBe('allow');
|
||||
expect(cgc2('git push', na).result).toBe('allow');
|
||||
expect(cgc2('git push -u origin feature-x', na).result).toBe('allow');
|
||||
});
|
||||
it('blocks push to main/master', () => {
|
||||
expect(cgc2('git push origin main', na).result).toBe('block');
|
||||
expect(cgc2('git push origin HEAD:main', na).result).toBe('block');
|
||||
expect(cgc2('git push origin master', na).result).toBe('block');
|
||||
});
|
||||
it('blocks force-push (hard pattern, unchanged)', () => {
|
||||
expect(cgc2('git push --force origin feature-x', na).result).toBe('block');
|
||||
expect(cgc2('git push origin feature-x --force-with-lease', na).result).toBe('block');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run to verify PASS** (логика из Task 3 уже на месте)
|
||||
|
||||
Run: `npx vitest run --root app --config vitest.config.tools.mjs shell-content-rules.test`
|
||||
Expected: PASS.
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Полная регрессия + коммит в фичевую ветку + PR
|
||||
|
||||
- [ ] **Step 1: Полная регрессия tools**
|
||||
|
||||
Run: `npx vitest run --root app --config vitest.config.tools.mjs`
|
||||
Expected: всё GREEN (baseline ~1989 + новые). 0 падений.
|
||||
|
||||
- [ ] **Step 2: Дымовая проверка живьём** — после правок гейт читается заново; проверить, что
|
||||
ранее блокированное теперь проходит (а опасное — нет). Прогнать через Bash:
|
||||
|
||||
```
|
||||
composer --version
|
||||
```
|
||||
|
||||
Expected: проходит (раньше любой `composer install` блокировался; `--version` и так был ок — проверка, что не сломали). Затем убедиться, что `git worktree list` (readonly) и `git status` работают.
|
||||
|
||||
- [ ] **Step 3: Создать фичевую ветку + worktree (теперь разрешено) и закоммитить**
|
||||
|
||||
```bash
|
||||
git worktree add "../worktree-gate-rescope" -b feat/gate-dev-prod-rescope origin/main
|
||||
```
|
||||
|
||||
(или коммит в основной копии на новой ветке — на усмотрение исполнителя; main НЕ трогать)
|
||||
|
||||
```bash
|
||||
git add tools/enforce-router-gate.mjs tools/shell-content-rules.mjs \
|
||||
tools/enforce-router-gate.test.mjs tools/shell-content-rules.test.mjs \
|
||||
docs/superpowers/specs/2026-06-02-router-gate-dev-prod-rescope-design.md \
|
||||
docs/superpowers/plans/2026-06-02-router-gate-dev-prod-rescope.md
|
||||
git commit -m "feat(gate): re-scope router-gate — allow local dev (composer/npm/git/worktree), keep prod+discipline blocks"
|
||||
git push origin feat/gate-dev-prod-rescope
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Открыть PR (клик владельца)** — дать владельцу ссылку из вывода `git push`; слияние в main — его клик.
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
|
||||
- **Spec coverage:** composer (Task 1) ✓ / npm (Task 2) ✓ / git dev-subs + worktree (Task 3) ✓ /
|
||||
push main-guard (Task 4) ✓ / discipline+prod untouched (явно не трогаем в Task 1-4) ✓ /
|
||||
«main = owner» (push-guard + PR в Task 5) ✓.
|
||||
- **Placeholders:** нет — весь код приведён дословно.
|
||||
- **Type/имена:** `GIT_DEV_SUB` / `GIT_CONDITIONAL_SUB` согласованы Task 3↔4; `classifyGitCommand`,
|
||||
`matchBashHardBlacklist`, `classifyBashCommand` — реальные экспортируемые имена (проверено по коду).
|
||||
- **Bootstrap:** коммит батчем в Task 5 (git разрешается только после применения Task 3) — учтено.
|
||||
@@ -0,0 +1,131 @@
|
||||
# Router-gate re-scope: «боевое блокируем, локальную разработку разрешаем»
|
||||
|
||||
**Дата:** 2026-06-02
|
||||
**Статус:** design (утверждён владельцем; реализация — отдельным планом)
|
||||
**Автор контекста:** сессия lead-region-tails
|
||||
|
||||
## Проблема
|
||||
|
||||
Router-gate v4 (`tools/enforce-router-gate.mjs`) работает в режиме «по умолчанию запрещено»
|
||||
(whitelist для Bash + hard-blacklist + MCP-классификатор + дисциплинарные хуки). Он задумывался
|
||||
как защита **боевого** контура (выкат на liderra.ru, изменение боевой БД, секреты, запуск
|
||||
воркфлоу), но по факту блокирует и **весь локальный инструмент разработки**: `composer install`,
|
||||
`npm install`, `git worktree`, `git commit`/`push`, и даже правку тест-файлов (через
|
||||
`enforce-tdd-real-test-verifier`). Это делает обычную разработку через контроллера непрактичной —
|
||||
любая PHP/JS-задача с тестами упирается в стену (подтверждено в сессии 2026-06-02: попытка сделать
|
||||
fix реестра Россвязи провалилась на цепочке взаимно-охраняющих замков).
|
||||
|
||||
## Цель
|
||||
|
||||
Перенастроить замок так, чтобы он блокировал **только боевое и опасное**, а **локальную
|
||||
разработку разрешал** — сохранив при этом дисциплину работы контроллера и защиту боевого контура.
|
||||
|
||||
## Решения (утверждены владельцем 2026-06-02)
|
||||
|
||||
1. **Дисциплину оставляем.** Хуки качества (TDD-gate, tdd-real-test-verifier, chain-recommendation,
|
||||
graph-first, override-limit, llm-judge, coverage-verify, memory-coverage и пр.) — **не трогаем**.
|
||||
Контроллер продолжает писать тесты до кода и не срезать углы.
|
||||
2. **Защиту боевого оставляем железно.** Выкат/боевая БД/секреты/запуск воркфлоу/защищённые
|
||||
пути — без изменений.
|
||||
3. **Инструменты разработки разрешаем.** composer/npm/pest/git/worktree.
|
||||
4. **Граница git:** ветки — контроллер сам (commit/push в не-главную ветку + подготовка PR);
|
||||
слияние в main, push в main, force-push, выкат — **клик владельца**.
|
||||
|
||||
## Подход
|
||||
|
||||
**Approach A (выбран):** точечно расширить whitelist дев-инструментами, сохранив философию
|
||||
«по умолчанию запрещено». Правим **два файла** — `tools/enforce-router-gate.mjs` (composer/npm) и
|
||||
`tools/shell-content-rules.mjs` (git; там общий `classifyGitCommand`). MCP-классификатор
|
||||
(`tools/mcp-tool-classifier.mjs`) и дисциплинарные хуки — без изменений.
|
||||
|
||||
Отвергнут **Approach B** (перевернуть в default-allow + blacklist опасного): любой пропуск в
|
||||
перечне опасного = дыра; ломает безопасную философию default-deny.
|
||||
|
||||
## Матрица: что блокируем / что разрешаем
|
||||
|
||||
### Остаётся ЗАБЛОКИРОВАННЫМ
|
||||
|
||||
| Категория | Примеры | Где |
|
||||
|---|---|---|
|
||||
| Боевой контур | выкат на сайт, изменение боевой БД, секреты/`.env`, защищённые пути (CLAUDE.md, memory/, transcripts, `~/.claude/runtime`) | без изменений |
|
||||
| GitHub на запись | `create_*`/`update_*`/`merge_*`/`push_files`/`actions_run_trigger` | MCP-классификатор без изменений (read-only, открытый 2026-06-02, остаётся) |
|
||||
| Опасные команды | `rm`/`mv`/`cp`/`chmod`/`chown`, `curl -X POST/PUT/DELETE`, `wget`, `nc`/`ncat`/`socat`, `node -e` с `fs.*`, `eval`, `bash -c`/`sh -c`, `python -c`, redirects в protected | hard-blacklist без изменений |
|
||||
| Дисциплина | TDD-gate, tdd-real-test-verifier, override-limit, chain-recommendation, graph-first, llm-judge, coverage | хуки без изменений |
|
||||
| Главная ветка | `git push` в main, `git push --force`, слияние в main | новый «страж main» |
|
||||
|
||||
### Становится РАЗРЕШЁННЫМ (локальная разработка)
|
||||
|
||||
| Инструмент | Команды |
|
||||
|---|---|
|
||||
| Composer | `composer install`, `composer dump-autoload`, `composer require`, `composer update` |
|
||||
| NPM | `npm install`, `npm ci`, `npm run <script>` |
|
||||
| Тесты | `pest`, `vendor/bin/pest`, `php artisan test` (уже частично в whitelist) |
|
||||
| Git (ветки) | `git commit`, `git add`, `git branch`, `git switch`/`checkout`, `git worktree`, `git stash`, `git push` **в не-главную ветку** |
|
||||
|
||||
## Изменения в коде (два файла)
|
||||
|
||||
Git-логика живёт не в самом router-gate, а в общем модуле `shell-content-rules.mjs`
|
||||
(`classifyGitCommand`, используется и Bash-, и PowerShell-гейтом). Поэтому правок — два файла.
|
||||
|
||||
### `tools/enforce-router-gate.mjs` (composer / npm)
|
||||
|
||||
1. **Из hard-blacklist (`BASH_HARD_BLACKLIST`) убрать** строки про `composer install/update/require/remove`
|
||||
и `npm install/i/update/remove/uninstall`. `yarn`/`pnpm` остаются заблокированными (проект на npm,
|
||||
не нужны). Истинно-опасные fs/сеть/exec (`rm/mv/cp/chmod`, `curl POST`, `wget`, `nc`, `node -e fs`,
|
||||
`eval`, `bash -c`, `python -c`, redirects) — **без изменений**.
|
||||
2. **В whitelist (`SAFE_EXACT`) добавить:** `composer (install|update|require|remove|dump-autoload|dump)`,
|
||||
`npm (install|i|ci)`, `npm run <script>` (любой скрипт). Существующие `composer show/outdated/test/...`
|
||||
и `npm test/run test/run lint` — остаются.
|
||||
|
||||
### `tools/shell-content-rules.mjs` (git)
|
||||
|
||||
1. **Новый `GIT_DEV_SUB`** = `{add, commit, branch, switch, checkout, stash, worktree}` → в
|
||||
`classifyGitCommand` после hard-pattern-проверки возвращать `allow`. Эти подкоманды **убрать** из
|
||||
`GIT_CONDITIONAL_SUB`. (`worktree` сейчас падает в default-deny — попадёт в dev-allow.)
|
||||
2. **`GIT_HARD_PATTERNS` не трогаем** — `--no-verify`, `git add -f`, `git -c`, force-push, `--output`/`-o`
|
||||
и т.п. по-прежнему блокируются ПЕРВЫМИ, до dev-allow. То есть `git commit --no-verify` и `git add -f`
|
||||
остаются заблокированы даже как «dev».
|
||||
3. **Страж main для `push`** (`mainPushGuard`, чистая функция): `push` остаётся, но —
|
||||
если в аргументах фигурирует `main`/`master` как ref (`git push origin main`, `HEAD:main`, `:main`)
|
||||
→ **block** (клик владельца); force-push уже заблокирован `GIT_HARD_PATTERNS`. Иначе (`git push origin <feature>`,
|
||||
bare `git push`) → allow. Допущение: bare `git push` считаем пушем не-главной ветки (контроллер по модели
|
||||
всегда на не-главной ветке); пуш в main возможен только явным `origin main` → пойман.
|
||||
4. **Conditional остаётся** для `merge, rebase, reset, cherry-pick, revert, pull, clean` (require approval) —
|
||||
риск потери работы / слияние в main = клик владельца.
|
||||
|
||||
**Не меняем:** `tools/mcp-tool-classifier.mjs`, `tools/bash-tokenizer.mjs` (`isMutatingSegment` — чейн-правило
|
||||
C13 «цепочка с мутацией → блок» сохраняется), любые `enforce-*` дисциплинарные хуки, `.claude/settings.json`.
|
||||
|
||||
## Тестирование (TDD)
|
||||
|
||||
Через `tools/enforce-router-gate.test.mjs` (vitest, работает в основной копии):
|
||||
|
||||
- `composer install` / `composer require x` → allow; `composer` (без подкоманды) → как раньше.
|
||||
- `npm install` → allow; `npm run build` → allow.
|
||||
- `git commit -m x` / `git worktree add ...` / `git push origin feature-x` → allow.
|
||||
- `git push origin main` / `git push --force` → **block** (страж main).
|
||||
- Регресс: опасное по-прежнему блокируется — `rm -rf x`, `curl -X POST`, `node -e "...fs..."`,
|
||||
`eval`, `python -c` → block.
|
||||
- Полная регрессия tools-тестов (`npx vitest run --root app --config vitest.config.tools.mjs`).
|
||||
|
||||
## Граница реализации (bootstrap-нюанс)
|
||||
|
||||
Сам этот re-scope — bootstrap-исключение: его нельзя делать в worktree (worktree пока заблокирован).
|
||||
Реализуется в основной копии (там активен живой замок и работает vitest). После правки замка
|
||||
`git`/`worktree`/`composer` становятся разрешены — дальнейшие задачи (например, fix реестра)
|
||||
пойдут уже по модели «ветка + PR».
|
||||
|
||||
## Остаточные риски (приняты)
|
||||
|
||||
- Разрешён `composer require`/`npm install` → теоретический supply-chain (установка пакета).
|
||||
Принято: это собственный проект владельца; дисциплина и code-review остаются.
|
||||
- `rm`/`mv`/`cp` остаются заблокированы — если реально мешают разработке, пересматриваем отдельно
|
||||
(файловые правки покрываются инструментами Write/Edit).
|
||||
- «Страж main» опирается на парсинг аргументов `git push`; экзотические формы (push по URL,
|
||||
refspec-трюки) при сомнении → block (fail-safe в сторону защиты main).
|
||||
|
||||
## Что НЕ входит (YAGNI)
|
||||
|
||||
- Не инвертируем модель замка (default-deny остаётся).
|
||||
- Не трогаем боевые воркфлоу, секреты, MCP-write.
|
||||
- Не ослабляем дисциплину.
|
||||
@@ -72,8 +72,8 @@ describe('classifyPowerShellCommand', () => {
|
||||
it('blocks reading a protected path', () => {
|
||||
expect(classifyPowerShellCommand('Get-Content ~/.claude/settings.json', {}).result).toBe('block');
|
||||
});
|
||||
it('routes git through shared classifier (block unapproved commit)', () => {
|
||||
expect(classifyPowerShellCommand('git commit -m "x"', { approvedGitOps: [], now }).result).toBe('block');
|
||||
it('routes git through shared classifier (commit dev-allowed 2026-06-02 re-scope)', () => {
|
||||
expect(classifyPowerShellCommand('git commit -m "x"', { approvedGitOps: [], now }).result).toBe('allow');
|
||||
});
|
||||
it('allows readonly git through PowerShell', () => {
|
||||
expect(classifyPowerShellCommand('git status', {}).result).toBe('allow');
|
||||
|
||||
@@ -56,8 +56,8 @@ export const BASH_HARD_BLACKLIST = [
|
||||
{ re: /\bpython3?\s+-c\b/, reason: 'python -c запрещён' },
|
||||
{ re: /\b(?:bash|sh)\s+-c\b/, reason: 'bash/sh -c запрещён' },
|
||||
{ re: /(^|\s|;|&&|\|\|)eval\b/, reason: 'eval запрещён' },
|
||||
{ re: /\bcomposer\s+(?:install|update|require|remove)\b/, reason: 'composer install/update/require/remove запрещён' },
|
||||
{ re: /\bnpm\s+(?:install|i|update|remove|uninstall)\b/, reason: 'npm install/update/remove запрещён' },
|
||||
// composer/npm перенесены в whitelist (dev-allow, 2026-06-02 re-scope) — это локальные
|
||||
// инструменты разработки, не боевой контур. yarn/pnpm остаются заблокированы (проект на npm).
|
||||
{ re: /\b(?:yarn|pnpm)\s+(?:add|install|remove)\b/, reason: 'yarn/pnpm add/install/remove запрещён' },
|
||||
{ re: /\bnpx\s+claude-/, reason: 'npx claude-* запрещён' },
|
||||
{ re: /\bcurl\b[^|;]*-X\s*(?:POST|PUT|DELETE|PATCH)\b/i, reason: 'curl -X POST/PUT/DELETE/PATCH запрещён' },
|
||||
@@ -120,8 +120,10 @@ const READING_CMDS = new Set(['ls', 'pwd', 'wc', 'head', 'tail', 'file', 'stat',
|
||||
const SAFE_EXACT = [
|
||||
/^npx\s+vitest\s+(?:run|--version)\b/,
|
||||
/^npm\s+(?:test|run\s+test|run\s+lint(?::[\w-]+)?)\b/,
|
||||
/^npm\s+(?:install|i|ci)\b/, // dev-allow 2026-06-02 re-scope
|
||||
/^npm\s+run\s+[\w:-]+/, // dev-allow 2026-06-02 re-scope (любой npm-скрипт)
|
||||
/^php\s+artisan\s+(?:list|route:list|migrate:status)\b/,
|
||||
/^composer\s+(?:show|outdated)\b/,
|
||||
/^composer\s+(?:show|outdated|install|update|require|remove|dump-autoload|dump)\b/, // +dev-allow 2026-06-02 re-scope
|
||||
/^node\s+(?!.*(?:-e|--eval|-p|--print|-r|--require|--import|--experimental-loader)\b)/,
|
||||
// Laravel dev workflow (2026-05-30) — exclude tinker (REPL = arbitrary PHP exec risk).
|
||||
// Hard-blacklist (composer install/update/require/remove) remains the first check, unaffected.
|
||||
@@ -138,6 +140,22 @@ const SAFE_EXACT = [
|
||||
// hard-blacklist + chain-mutating rule (both run before the whitelist), and each
|
||||
// chain segment after `cd app &&` must still be independently whitelisted.
|
||||
/^cd\s+app$/,
|
||||
// Worktree dev (2026-06-02, owner-authorized): cd into a project worktree dir
|
||||
// (path segment `worktree-` / `v4-stream-`) so git/pest run there. Quoted absolute
|
||||
// path required; `..` and protected segments (.claude/.ssh/.env/runtime/.git) excluded
|
||||
// → cwd-shift read-bypass stays contained (protected files also remain blocked by name
|
||||
// in the command). cd into Документация/system/protected dirs → default-deny.
|
||||
/^cd\s+(?=.*[\\/](?:worktree-|v4-stream-))(?!.*(?:\.\.|\.claude|\.ssh|\.env|runtime|\.git)).+$/,
|
||||
// graphify read-only subcommands (#86, §5 п.14, owner-authorized 2026-06-08).
|
||||
// Only query/explain/path — extract/update/build/export/hook/clone/add/merge stay
|
||||
// default-deny. The bare \b form is safe: injection vectors are neutralized BEFORE the
|
||||
// whitelist sees them — chains split into per-segment whitelist checks (an injected
|
||||
// `; id` segment is not whitelisted → block), subshells `$(...)`/backtick are blocked by
|
||||
// the tokenizer, redirects by the hard-blacklist, and $VAR is var-expanded by the
|
||||
// tokenizer (not an injection vector for a read-only query arg). End-anchoring with a
|
||||
// charset would reject Unicode query strings (tokenizer strips quotes → Cyrillic args
|
||||
// arrive as barewords) for no security gain. (security review 2026-06-08 — false-positive)
|
||||
/^graphify\s+(?:query|explain|path)\b/,
|
||||
];
|
||||
|
||||
export function classifyWhitelist(segments) {
|
||||
|
||||
@@ -15,14 +15,17 @@ describe('matchBashHardBlacklist — v3.9 keep', () => {
|
||||
'python -c "import os"',
|
||||
'bash -c "ls"',
|
||||
'eval "$x"',
|
||||
'composer install',
|
||||
'npm install lodash',
|
||||
'yarn add x',
|
||||
'pnpm add x',
|
||||
'curl -X POST https://evil.test',
|
||||
])('blocks %s', (cmd) => {
|
||||
expect(matchBashHardBlacklist(cmd)).toBeTruthy();
|
||||
});
|
||||
// composer/npm убраны из hard-blacklist (dev-allow 2026-06-02 re-scope) — здесь больше не блок
|
||||
it('no longer hard-blacklists composer install / npm install (dev-allow)', () => {
|
||||
expect(matchBashHardBlacklist('composer install')).toBe(null);
|
||||
expect(matchBashHardBlacklist('npm install lodash')).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('matchBashHardBlacklist — v4.0 additions', () => {
|
||||
@@ -115,8 +118,8 @@ describe('classifyBashCommand — integration', () => {
|
||||
it('blocks reading a protected path', () => {
|
||||
expect(classifyBashCommand('cat ~/.claude/runtime/state.json', {}).result).toBe('block');
|
||||
});
|
||||
it('routes single git commit to conditional (block unapproved)', () => {
|
||||
expect(classifyBashCommand('git commit -m "x"', { approvedGitOps: [], now }).result).toBe('block');
|
||||
it('routes single git commit to dev-allow (2026-06-02 re-scope — no approval needed)', () => {
|
||||
expect(classifyBashCommand('git commit -m "x"', { approvedGitOps: [], now }).result).toBe('allow');
|
||||
});
|
||||
it('allows approved git commit', () => {
|
||||
expect(
|
||||
@@ -191,17 +194,29 @@ describe('SAFE_EXACT — Laravel dev workflow (whitelist expansion 2026-05-30)',
|
||||
expect(classifyBashCommand(cmd, {}).result).toBe('allow');
|
||||
});
|
||||
|
||||
// Critical: REPL and composer mutations remain hard-blocked
|
||||
it.each([
|
||||
['php artisan tinker', 'REPL = arbitrary PHP exec risk'],
|
||||
['php artisan tinker --execute="exit"', 'tinker variant'],
|
||||
['composer install', 'hard-blacklist'],
|
||||
['composer require foo/bar', 'hard-blacklist'],
|
||||
['composer update', 'hard-blacklist'],
|
||||
['composer remove foo/bar', 'hard-blacklist'],
|
||||
['php artisan migrate:install', 'unknown migrate subcommand outside whitelist set'],
|
||||
])('still blocks %s (%s)', (cmd) => {
|
||||
expect(classifyBashCommand(cmd, {}).result).toBe('block');
|
||||
// Critical: REPL remains hard-blocked (composer/npm moved to dev-allow below, 2026-06-02 re-scope)
|
||||
it('still blocks tinker REPL and unknown migrate subcommand', () => {
|
||||
expect(classifyBashCommand('php artisan tinker', {}).result).toBe('block');
|
||||
expect(classifyBashCommand('php artisan tinker --execute="exit"', {}).result).toBe('block');
|
||||
expect(classifyBashCommand('php artisan migrate:install', {}).result).toBe('block');
|
||||
});
|
||||
|
||||
// dev-allow (owner-authorized 2026-06-02 re-scope): composer is a local dev tool
|
||||
it('now allows composer install/require/update/remove/dump-autoload', () => {
|
||||
expect(classifyBashCommand('composer install', {}).result).toBe('allow');
|
||||
expect(classifyBashCommand('composer install -d app --no-interaction', {}).result).toBe('allow');
|
||||
expect(classifyBashCommand('composer require monolog/monolog', {}).result).toBe('allow');
|
||||
expect(classifyBashCommand('composer update', {}).result).toBe('allow');
|
||||
expect(classifyBashCommand('composer remove monolog/monolog', {}).result).toBe('allow');
|
||||
expect(classifyBashCommand('composer dump-autoload', {}).result).toBe('allow');
|
||||
});
|
||||
|
||||
// dev-allow (owner-authorized 2026-06-02 re-scope): npm is a local dev tool
|
||||
it('now allows npm install/i/ci/run', () => {
|
||||
expect(classifyBashCommand('npm install', {}).result).toBe('allow');
|
||||
expect(classifyBashCommand('npm i', {}).result).toBe('allow');
|
||||
expect(classifyBashCommand('npm ci', {}).result).toBe('allow');
|
||||
expect(classifyBashCommand('npm run build', {}).result).toBe('allow');
|
||||
});
|
||||
|
||||
// Critical: existing pre-existing v3.8 keep behaviour
|
||||
@@ -226,6 +241,39 @@ describe('SAFE_EXACT — Laravel dev workflow (whitelist expansion 2026-05-30)',
|
||||
expect(classifyBashCommand('composer show', {}).result).toBe('allow');
|
||||
expect(classifyBashCommand('composer outdated', {}).result).toBe('allow');
|
||||
});
|
||||
|
||||
// graphify read-only subcommands (owner-authorized 2026-06-08 — #86 graphify, §5 п.14)
|
||||
it('allows graphify read-only subcommands (query/explain/path)', () => {
|
||||
expect(classifyBashCommand('graphify query "x"', {}).result).toBe('allow');
|
||||
expect(classifyBashCommand('graphify explain "Node"', {}).result).toBe('allow');
|
||||
expect(classifyBashCommand('graphify path "A" "B"', {}).result).toBe('allow');
|
||||
});
|
||||
|
||||
// graphify mutating/expensive subcommands stay default-deny
|
||||
it('still blocks graphify mutating subcommands (extract/export/hook)', () => {
|
||||
expect(classifyBashCommand('graphify extract .', {}).result).toBe('block');
|
||||
expect(classifyBashCommand('graphify export html', {}).result).toBe('block');
|
||||
expect(classifyBashCommand('graphify hook install', {}).result).toBe('block');
|
||||
});
|
||||
|
||||
// graphify allowlist is not bypassable via chained commands / subshells — they are
|
||||
// caught by the gate architecture BEFORE the whitelist regex (per-segment whitelist +
|
||||
// tokenizer subshell-block + redirect hard-blacklist), so the simple subcommand
|
||||
// allowlist is safe (security review 2026-06-08 finding = false-positive: $VAR is
|
||||
// var-expanded away by the tokenizer, not a command-injection vector).
|
||||
it('blocks graphify chained commands and subshell payloads', () => {
|
||||
expect(classifyBashCommand('graphify query x; id', {}).result).toBe('block');
|
||||
expect(classifyBashCommand('graphify query x && rm y', {}).result).toBe('block');
|
||||
expect(classifyBashCommand('graphify path A `id`', {}).result).toBe('block');
|
||||
expect(classifyBashCommand('graphify query x | sh', {}).result).toBe('block');
|
||||
});
|
||||
|
||||
// legit read-only graphify with quoted (Cyrillic) question + flags still allowed —
|
||||
// guards against over-tightening that would reject Unicode queries (tokenizer strips
|
||||
// quotes → Cyrillic args arrive as barewords).
|
||||
it('still allows graphify query with quoted question and flags', () => {
|
||||
expect(classifyBashCommand('graphify query "конфликт дубль" --dfs --budget 1500', {}).result).toBe('allow');
|
||||
});
|
||||
});
|
||||
|
||||
describe('SAFE_EXACT — narrow `cd app` whitelist (2026-05-31, owner-authorized)', () => {
|
||||
@@ -271,6 +319,29 @@ describe('SAFE_EXACT — narrow `cd app` whitelist (2026-05-31, owner-authorized
|
||||
});
|
||||
});
|
||||
|
||||
describe('SAFE_EXACT — worktree cd (2026-06-02, owner-authorized worktree dev)', () => {
|
||||
// Allowed: enter a project worktree dir (segment `worktree-` / `v4-stream-`) so
|
||||
// git/pest can run there. Quoted absolute path; cwd-shift read-bypass stays contained
|
||||
// because protected files remain blocked by name in the command (cat .env / runtime).
|
||||
it.each([
|
||||
'cd "C:\\моя\\проекты\\портал crm\\worktree-deals-city"',
|
||||
'cd "C:\\моя\\проекты\\портал crm\\worktree-deals-city\\app"',
|
||||
'cd "C:\\моя\\проекты\\портал crm\\v4-stream-A"',
|
||||
])('allows cd into a worktree dir: %s', (cmd) => {
|
||||
expect(classifyBashCommand(cmd, {}).result).toBe('allow');
|
||||
});
|
||||
|
||||
// Scope: protected / non-worktree dirs stay default-deny (no `worktree-` marker, or
|
||||
// `..` / protected segment present → cwd-shift read-bypass prevented).
|
||||
it.each([
|
||||
'cd "C:\\Users\\Administrator\\.claude\\runtime"',
|
||||
'cd "C:\\моя\\проекты\\портал crm\\worktree-x\\..\\..\\.claude"',
|
||||
'cd "C:\\моя\\проекты\\портал crm\\Документация"',
|
||||
])('still blocks cd into non-worktree / protected dir: %s', (cmd) => {
|
||||
expect(classifyBashCommand(cmd, {}).result).toBe('block');
|
||||
});
|
||||
});
|
||||
|
||||
import { stripQuotedSpans } from './enforce-router-gate.mjs';
|
||||
|
||||
describe('quote-aware redirect (quirk 2)', () => {
|
||||
|
||||
@@ -108,6 +108,11 @@ function hasFailingTestRun(turn) {
|
||||
// Numeric: "Tests N failed | M passed" with N>0
|
||||
const m = txt.match(/Tests\s+(\d+)\s+failed/);
|
||||
if (m && Number(m[1]) > 0) return true;
|
||||
// JSON reporter (composer test / php artisan test → pest): {"result":"failed",...}
|
||||
// or {"failed":N}/{"errors":N} with N>0. command-not-found / error REDs lack the
|
||||
// English "Failed" keyword above, so recognise the structured marker too.
|
||||
if (/"result"\s*:\s*"failed"/.test(txt)) return true;
|
||||
if (/"(?:failed|errors)"\s*:\s*[1-9]/.test(txt)) return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,3 +168,25 @@ describe('enforce-tdd-gate / decide', () => {
|
||||
expect(r.block).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('enforce-tdd-gate / decide — JSON pest reporter RED (composer test)', () => {
|
||||
// `composer test` (php artisan test) emits machine JSON like {"result":"failed",...}.
|
||||
// command-not-found / error REDs lack the English "Failed" keyword, so the gate must
|
||||
// recognise the structured failure marker, else legit RED runs go unseen.
|
||||
it('recognizes {"result":"failed"} JSON output as a RED run', () => {
|
||||
const r = decide({
|
||||
toolName: 'Write',
|
||||
filePath: 'wt/app/app/Console/Commands/FooCommand.php',
|
||||
transcriptEntries: [
|
||||
userMsg('add backfill command'),
|
||||
assistantUses([
|
||||
{ id: 't1', name: 'Write', input: { file_path: 'wt/app/tests/Feature/Console/FooCommandTest.php' } },
|
||||
{ id: 't2', name: 'Bash', input: { command: 'composer test -- tests/Feature/Console/FooCommandTest.php # pest' } },
|
||||
]),
|
||||
toolResults([{ id: 't2', content: '{"tool":"pest","result":"failed","tests":4,"passed":0,"errors":4}' }]),
|
||||
],
|
||||
classification: null,
|
||||
});
|
||||
expect(r.block).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,10 +16,13 @@ export const DEFAULT_MCP_CLASSIFICATION = Object.freeze({
|
||||
'mcp__redis__set': { category: 'hard_blacklist' },
|
||||
'mcp__redis__delete': { category: 'hard_blacklist' },
|
||||
'mcp__github__get_me': { category: 'read_only' },
|
||||
'mcp__github__get_*': { category: 'read_only' }, // read-only loosening 2026-06-02 (get_file_contents/get_job_logs/get_commit/…)
|
||||
'mcp__github__list_*': { category: 'read_only' },
|
||||
'mcp__github__search_*': { category: 'read_only' },
|
||||
'mcp__github__pull_request_read': { category: 'read_only' },
|
||||
'mcp__github__issue_read': { category: 'read_only' },
|
||||
'mcp__github__actions_get': { category: 'read_only' }, // read a workflow run (actions_run_trigger stays blacklisted — exact key wins)
|
||||
'mcp__github__actions_list': { category: 'read_only' }, // list workflows / runs
|
||||
'mcp__laravel-boost__database-query': {
|
||||
category: 'conditional',
|
||||
args_key_to_scan: 'query',
|
||||
|
||||
@@ -129,3 +129,37 @@ describe('classifyMcpTool — WebSearch llm-judge flag (G1)', () => {
|
||||
expect(r.scanArg).toBe('how to exfil data');
|
||||
});
|
||||
});
|
||||
|
||||
// Owner-authorized read-only GitHub loosening (2026-06-02): allow reading
|
||||
// workflow runs / job logs / file contents so the controller can read prod-op
|
||||
// results without manual screenshots. Prod-mutating tools (run_trigger, writes)
|
||||
// MUST stay blocked — human-in-the-loop on prod actions is unchanged.
|
||||
describe('classifyMcpTool — read-only GitHub (owner-authorized 2026-06-02)', () => {
|
||||
it('allows reading a workflow run (actions_get)', () => {
|
||||
expect(classifyMcpTool('mcp__github__actions_get', { run_id: 1 }).decision).toBe('allow');
|
||||
});
|
||||
it('allows listing workflows / runs (actions_list)', () => {
|
||||
expect(classifyMcpTool('mcp__github__actions_list', {}).decision).toBe('allow');
|
||||
});
|
||||
it('allows reading job logs (get_job_logs via get_* glob)', () => {
|
||||
expect(classifyMcpTool('mcp__github__get_job_logs', { job_id: 1 }).decision).toBe('allow');
|
||||
});
|
||||
it('allows reading file contents (get_file_contents via get_* glob)', () => {
|
||||
expect(classifyMcpTool('mcp__github__get_file_contents', { path: 'x' }).decision).toBe('allow');
|
||||
});
|
||||
it('allows reading a commit (get_commit via get_* glob)', () => {
|
||||
expect(classifyMcpTool('mcp__github__get_commit', { sha: 'x' }).decision).toBe('allow');
|
||||
});
|
||||
it('STILL BLOCKS triggering a workflow (actions_run_trigger — exact wins over glob)', () => {
|
||||
expect(classifyMcpTool('mcp__github__actions_run_trigger', {}).decision).toBe('block');
|
||||
});
|
||||
it('STILL BLOCKS writing a file (create_or_update_file)', () => {
|
||||
expect(classifyMcpTool('mcp__github__create_or_update_file', { path: 'x' }).decision).toBe('block');
|
||||
});
|
||||
it('STILL BLOCKS push_files', () => {
|
||||
expect(classifyMcpTool('mcp__github__push_files', {}).decision).toBe('block');
|
||||
});
|
||||
it('STILL BLOCKS update_pull_request (write)', () => {
|
||||
expect(classifyMcpTool('mcp__github__update_pull_request', {}).decision).toBe('block');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* receipt-key-config — резолв HMAC-ключа подписи расписок роутер-наставника.
|
||||
* Зеркало llm-judge-config.mjs: keychain (service router-mentor-receipts) → env → null.
|
||||
* Ключ провижинит владелец (как LLM-ключ судьи). Без ключа — null → подпись невозможна
|
||||
* → неподписанная расписка невалидна (fail-closed на стороне verifyReceipt).
|
||||
*/
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
const KEY_ENV = 'ROUTER_MENTOR_RECEIPT_KEY';
|
||||
const KEYCHAIN_SERVICE = 'router-mentor-receipts';
|
||||
const KEYCHAIN_ACCOUNT = 'default';
|
||||
|
||||
/**
|
||||
* @param {object} [args]
|
||||
* @param {object} [args.env] - окружение (default process.env)
|
||||
* @param {Function} [args.keychainGet] - () => string|null (инъекция для тестов)
|
||||
* @returns {string|null}
|
||||
*/
|
||||
export function resolveReceiptKey({ env = process.env, keychainGet = defaultKeychainGet } = {}) {
|
||||
let chainKey = null;
|
||||
try {
|
||||
const v = keychainGet();
|
||||
chainKey = v ? String(v) : null;
|
||||
} catch {
|
||||
chainKey = null;
|
||||
}
|
||||
const envKey = env[KEY_ENV] ? String(env[KEY_ENV]) : null;
|
||||
return chainKey || envKey || null;
|
||||
}
|
||||
|
||||
/** Lazy keytar-ридер; null если keytar отсутствует или записи нет. Никогда не бросает. */
|
||||
export function defaultKeychainGet() {
|
||||
try {
|
||||
const require = createRequire(import.meta.url);
|
||||
const keytar = require('keytar');
|
||||
return (keytar.getPasswordSync?.(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT)) || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export const _internals = { KEY_ENV, KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT };
|
||||
@@ -0,0 +1,25 @@
|
||||
// tools/receipt-key-config.test.mjs
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { resolveReceiptKey } from './receipt-key-config.mjs';
|
||||
|
||||
describe('resolveReceiptKey', () => {
|
||||
it('returns keychain key when present', () => {
|
||||
const r = resolveReceiptKey({ env: {}, keychainGet: () => 'k-from-chain' });
|
||||
expect(r).toBe('k-from-chain');
|
||||
});
|
||||
it('falls back to env ROUTER_MENTOR_RECEIPT_KEY when keychain empty', () => {
|
||||
const r = resolveReceiptKey({ env: { ROUTER_MENTOR_RECEIPT_KEY: 'k-env' }, keychainGet: () => null });
|
||||
expect(r).toBe('k-env');
|
||||
});
|
||||
it('prefers keychain over env', () => {
|
||||
const r = resolveReceiptKey({ env: { ROUTER_MENTOR_RECEIPT_KEY: 'k-env' }, keychainGet: () => 'k-chain' });
|
||||
expect(r).toBe('k-chain');
|
||||
});
|
||||
it('returns null when neither present', () => {
|
||||
expect(resolveReceiptKey({ env: {}, keychainGet: () => null })).toBe(null);
|
||||
});
|
||||
it('never throws when keychainGet throws → null', () => {
|
||||
const r = resolveReceiptKey({ env: {}, keychainGet: () => { throw new Error('boom'); } });
|
||||
expect(r).toBe(null);
|
||||
});
|
||||
});
|
||||
@@ -164,9 +164,13 @@ const GIT_READONLY_SUB = new Set([
|
||||
'rev-parse', 'merge-base', 'remote', 'stash', // stash list/show resolved below
|
||||
'fetch', 'ls-remote', // ref-only, no working-tree mutation — Stream H pre-flight requires §15.2 sync
|
||||
]);
|
||||
// dev-safe (owner-authorized 2026-06-02 re-scope): allow без approval. GIT_HARD_PATTERNS
|
||||
// (--no-verify / add -f / -c / force / --output / -o) пре-фильтруют опасные варианты ВЫШЕ.
|
||||
const GIT_DEV_SUB = new Set([
|
||||
'add', 'commit', 'branch', 'switch', 'checkout', 'stash', 'worktree',
|
||||
]);
|
||||
const GIT_CONDITIONAL_SUB = new Set([
|
||||
'add', 'commit', 'merge', 'rebase', 'reset', 'checkout', 'switch',
|
||||
'branch', 'stash', 'cherry-pick', 'revert', 'pull', 'push', 'clean',
|
||||
'merge', 'rebase', 'reset', 'cherry-pick', 'revert', 'pull', 'clean',
|
||||
]);
|
||||
|
||||
// G5/G6 + force-push + add -f → always block (даже если "approved").
|
||||
@@ -183,14 +187,23 @@ const GIT_HARD_PATTERNS = [
|
||||
];
|
||||
|
||||
function gitSubcommand(command) {
|
||||
const m = normalizeCommand(command).match(/\bgit\s+(?:-c\s+\S+\s+)*([a-z][\w-]*)/);
|
||||
// Skip leading global flags `-c <val>` and `-C <path>`. `git -C <dir> <sub>` is the
|
||||
// cwd-independent way to operate on a worktree (the shell resets cwd each call), so the
|
||||
// real subcommand must be found after `-C`. `-C` (uppercase, working-dir) is case-distinct
|
||||
// from the blocked `-c` config-injection (GIT_HARD_PATTERNS still scans the full command).
|
||||
const m = normalizeCommand(command).match(
|
||||
/\bgit\s+(?:(?:-c\s+\S+|-C\s+(?:"[^"]*"|'[^']*'|\S+))\s+)*([a-z][\w-]*)/,
|
||||
);
|
||||
return m ? m[1] : null;
|
||||
}
|
||||
|
||||
export function classifyGitCommand(command, ctx = {}) {
|
||||
const norm = normalizeCommand(command);
|
||||
// Strip a leading `git -C <path>` (worktree-dir flag) so every rule below sees the real
|
||||
// subcommand+flags. Without this, position-anchored hard-patterns (--no-verify / --force /
|
||||
// add -f) and the push-main-guard would be bypassed by interposing `-C <dir>`.
|
||||
const norm = normalizeCommand(command).replace(/(\bgit)\s+-C\s+(?:"[^"]*"|'[^']*'|\S+)\s+/, '$1 ');
|
||||
if (!/\bgit\b/.test(norm)) return null;
|
||||
const sub = gitSubcommand(command);
|
||||
const sub = gitSubcommand(norm);
|
||||
if (!sub) return null;
|
||||
|
||||
// 1. git-hard — block безусловно
|
||||
@@ -212,6 +225,18 @@ export function classifyGitCommand(command, ctx = {}) {
|
||||
return { result: 'block', reason: 'git remote (мутация) требует AskUser approval' };
|
||||
}
|
||||
|
||||
// dev-safe git (owner-authorized 2026-06-02 re-scope): GIT_HARD_PATTERNS уже отсеяли
|
||||
// опасные варианты (--no-verify / add -f / -c / force / --output / -o) на шаге 1.
|
||||
if (GIT_DEV_SUB.has(sub)) return { result: 'allow', reason: `dev-safe git ${sub}` };
|
||||
|
||||
// push: фичевые ветки — allow; main/master — клик владельца (force уже заблокирован hard).
|
||||
if (sub === 'push') {
|
||||
if (/\b(?:main|master)\b/.test(norm)) {
|
||||
return { result: 'block', reason: 'git push в main/master — клик владельца' };
|
||||
}
|
||||
return { result: 'allow', reason: 'git push в фичевую ветку' };
|
||||
}
|
||||
|
||||
// 3. conditional → approve check
|
||||
if (GIT_CONDITIONAL_SUB.has(sub)) {
|
||||
const approved = isApproved(command, ctx.approvedGitOps, ctx.now ?? Date.now());
|
||||
|
||||
@@ -167,40 +167,81 @@ describe('classifyGitCommand — readonly', () => {
|
||||
);
|
||||
});
|
||||
|
||||
describe('classifyGitCommand — conditional after approve', () => {
|
||||
describe('classifyGitCommand — conditional (still needs approval after 2026-06-02 re-scope)', () => {
|
||||
const now = 2_000_000;
|
||||
it('blocks unapproved git commit', () => {
|
||||
const r = classifyGitCommand('git commit -m "x"', { approvedGitOps: [], now });
|
||||
expect(r.result).toBe('block');
|
||||
expect(r.reason).toMatch(/approve/i);
|
||||
});
|
||||
it('allows approved git commit', () => {
|
||||
const r = classifyGitCommand('git commit -m "x"', {
|
||||
approvedGitOps: [{ command: 'git commit -m "x"', ts: now }],
|
||||
now,
|
||||
});
|
||||
expect(r.result).toBe('allow');
|
||||
});
|
||||
it.each(['git rebase main', 'git reset --hard', 'git switch main', 'git stash pop', 'git push origin feat'])(
|
||||
'blocks unapproved %s',
|
||||
(cmd) => {
|
||||
it('blocks unapproved rebase/reset/merge/cherry-pick/revert/pull/clean', () => {
|
||||
for (const cmd of ['git rebase main', 'git reset --hard', 'git merge feat',
|
||||
'git cherry-pick abc', 'git revert abc', 'git pull', 'git clean -fd']) {
|
||||
expect(classifyGitCommand(cmd, { approvedGitOps: [], now }).result).toBe('block');
|
||||
},
|
||||
);
|
||||
it('blocks unapproved git add (v4 Stream G addition)', () => {
|
||||
const r = classifyGitCommand('git add .claude/settings.json', { approvedGitOps: [], now });
|
||||
expect(r.result).toBe('block');
|
||||
expect(r.reason).toMatch(/approve/i);
|
||||
}
|
||||
});
|
||||
it('allows approved git add', () => {
|
||||
const r = classifyGitCommand('git add .claude/settings.json', {
|
||||
approvedGitOps: [{ command: 'git add .claude/settings.json', ts: now }],
|
||||
it('allows approved git merge', () => {
|
||||
const r = classifyGitCommand('git merge feat', {
|
||||
approvedGitOps: [{ command: 'git merge feat', ts: now }],
|
||||
now,
|
||||
});
|
||||
expect(r.result).toBe('allow');
|
||||
});
|
||||
});
|
||||
|
||||
describe('classifyGitCommand — dev-allow (owner-authorized 2026-06-02 re-scope)', () => {
|
||||
const na = { approvedGitOps: [], now: 2_000_000 };
|
||||
it('allows commit/add/branch/switch/checkout/stash/worktree without approval', () => {
|
||||
for (const cmd of [
|
||||
'git commit -m "x"', 'git add .', 'git branch feature-x',
|
||||
'git switch -c feature-x', 'git switch feature-x', 'git checkout -b feature-x',
|
||||
'git stash push -m wip', 'git stash pop',
|
||||
'git worktree add ../wt -b feat origin/main',
|
||||
]) {
|
||||
expect(classifyGitCommand(cmd, na).result).toBe('allow');
|
||||
}
|
||||
});
|
||||
it('still blocks commit --no-verify and add -f (hard patterns survive dev-allow)', () => {
|
||||
expect(classifyGitCommand('git commit --no-verify -m x', na).result).toBe('block');
|
||||
expect(classifyGitCommand('git add -f ignored.txt', na).result).toBe('block');
|
||||
});
|
||||
});
|
||||
|
||||
describe('classifyGitCommand — push main-guard (owner-authorized 2026-06-02 re-scope)', () => {
|
||||
const na = { approvedGitOps: [], now: 2_000_000 };
|
||||
it('allows push to a feature branch / bare push', () => {
|
||||
expect(classifyGitCommand('git push origin worktree-lead-region-tails', na).result).toBe('allow');
|
||||
expect(classifyGitCommand('git push', na).result).toBe('allow');
|
||||
expect(classifyGitCommand('git push -u origin feature-x', na).result).toBe('allow');
|
||||
});
|
||||
it('blocks push to main/master (owner click)', () => {
|
||||
expect(classifyGitCommand('git push origin main', na).result).toBe('block');
|
||||
expect(classifyGitCommand('git push origin HEAD:main', na).result).toBe('block');
|
||||
expect(classifyGitCommand('git push origin master', na).result).toBe('block');
|
||||
});
|
||||
it('blocks force-push (hard pattern unchanged)', () => {
|
||||
expect(classifyGitCommand('git push --force origin feature-x', na).result).toBe('block');
|
||||
expect(classifyGitCommand('git push origin feature-x --force-with-lease', na).result).toBe('block');
|
||||
});
|
||||
});
|
||||
|
||||
describe('classifyGitCommand — git -C <path> (worktree dev, 2026-06-02)', () => {
|
||||
const na = { approvedGitOps: [], now: 4_000_000 };
|
||||
// git -C points git at another working tree (cwd resets each shell call, so this is
|
||||
// the cwd-independent way to commit in a worktree). Classify by the REAL subcommand
|
||||
// after -C, with all hard-patterns / push-main-guard still applied to the full command.
|
||||
it.each([
|
||||
'git -C "C:\\моя\\проекты\\портал crm\\worktree-x" commit -m "y"',
|
||||
'git -C "C:\\моя\\проекты\\портал crm\\worktree-x" add app/foo.php',
|
||||
'git -C "/path/worktree-x" push origin feature-y',
|
||||
'git -C /repo status',
|
||||
])('classifies by real subcommand after -C: %s', (cmd) => {
|
||||
expect(classifyGitCommand(cmd, na).result).toBe('allow');
|
||||
});
|
||||
|
||||
it('still blocks push to main even with -C', () => {
|
||||
expect(classifyGitCommand('git -C /repo push origin main', na).result).toBe('block');
|
||||
});
|
||||
it('still blocks --no-verify even with -C', () => {
|
||||
expect(classifyGitCommand('git -C /repo commit --no-verify -m x', na).result).toBe('block');
|
||||
});
|
||||
});
|
||||
|
||||
describe('classifyGitCommand — git-hard (always block)', () => {
|
||||
it.each([
|
||||
'git push --force origin main',
|
||||
|
||||
Reference in New Issue
Block a user