diff --git a/.deckent/workspace/IDENTITY.md b/.deckent/workspace/IDENTITY.md index ee29d97ab..7a4c6607a 100644 --- a/.deckent/workspace/IDENTITY.md +++ b/.deckent/workspace/IDENTITY.md @@ -17,7 +17,7 @@ Direction (2026-06-29 pivot): Tool-driven, progressive-disclosure, full-control Moat: Deterministik eval-backed orchestration · governance-by-construction · outcome→evidence→routing→promotion→training-trace kapalı öğrenme döngüsü SSOT: `docs/MASTER-PLAN.md` · core-memory: `.deckent/docs/core-memory/MEMORY.md` · yön gerekçesi: `.analysis/hermes-vs-deckent-direction-decisions.md` -Tests: 34,295 descriptors (parsed from tests/**/*.test.ts(x)) +Tests: 34,307 descriptors (parsed from tests/**/*.test.ts(x)) Dashboard Tests: 96 descriptors (parsed from src/dashboard/src/**/*.test.tsx) Coverage: N/A diff --git a/README.md b/README.md index 0125325a5..5b0831d56 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ Deckent's three immutable laws are Dual Lens + Scale, Every Environment, and Nev License: MIT. [Evidence: `package.json:90-91`; `LICENSE`] -[![npm version](https://img.shields.io/npm/v/deckent.svg)](https://www.npmjs.com/package/deckent) [![tests](https://img.shields.io/badge/tests-34295%2B-brightgreen)](https://github.com/VerhexIO/deckent) [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![sprints](https://img.shields.io/badge/sprints-492%2B-teal)](https://github.com/VerhexIO/deckent) [![version](https://img.shields.io/badge/version-v1.0.0--beta.1-orange)](https://github.com/VerhexIO/deckent) [![CI](https://img.shields.io/github/actions/workflow/status/VerhexIO/deckent/ci.yml?label=ci)](https://github.com/VerhexIO/deckent/actions) +[![npm version](https://img.shields.io/npm/v/deckent.svg)](https://www.npmjs.com/package/deckent) [![tests](https://img.shields.io/badge/tests-34307%2B-brightgreen)](https://github.com/VerhexIO/deckent) [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![sprints](https://img.shields.io/badge/sprints-492%2B-teal)](https://github.com/VerhexIO/deckent) [![version](https://img.shields.io/badge/version-v1.0.0--beta.1-orange)](https://github.com/VerhexIO/deckent) [![CI](https://img.shields.io/github/actions/workflow/status/VerhexIO/deckent/ci.yml?label=ci)](https://github.com/VerhexIO/deckent/actions) diff --git a/README.tr.md b/README.tr.md index 81b14b47a..899cd1438 100644 --- a/README.tr.md +++ b/README.tr.md @@ -94,7 +94,7 @@ Deckent'in üç Immutable Law'u Dual Lens + Scale, Every Environment ve Never MV License: MIT. [Kanıt: `package.json:90-91`; `LICENSE`] -[![npm version](https://img.shields.io/npm/v/deckent.svg)](https://www.npmjs.com/package/deckent) [![tests](https://img.shields.io/badge/tests-34295%2B-brightgreen)](https://github.com/VerhexIO/deckent) [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![sprints](https://img.shields.io/badge/sprints-492%2B-teal)](https://github.com/VerhexIO/deckent) [![version](https://img.shields.io/badge/version-v1.0.0--beta.1-orange)](https://github.com/VerhexIO/deckent) [![CI](https://img.shields.io/github/actions/workflow/status/VerhexIO/deckent/ci.yml?label=ci)](https://github.com/VerhexIO/deckent/actions) +[![npm version](https://img.shields.io/npm/v/deckent.svg)](https://www.npmjs.com/package/deckent) [![tests](https://img.shields.io/badge/tests-34307%2B-brightgreen)](https://github.com/VerhexIO/deckent) [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![sprints](https://img.shields.io/badge/sprints-492%2B-teal)](https://github.com/VerhexIO/deckent) [![version](https://img.shields.io/badge/version-v1.0.0--beta.1-orange)](https://github.com/VerhexIO/deckent) [![CI](https://img.shields.io/github/actions/workflow/status/VerhexIO/deckent/ci.yml?label=ci)](https://github.com/VerhexIO/deckent/actions) diff --git a/docs/MASTER-PLAN.md b/docs/MASTER-PLAN.md index bf384f46a..c84fcc746 100644 --- a/docs/MASTER-PLAN.md +++ b/docs/MASTER-PLAN.md @@ -406,6 +406,8 @@ Current receipt register: | `GR-2026-08-09-DRIFT-VISIBILITY-SUPP-01` | RECOVERY-DO-DOGFOOD-001 | G1 | `scripts/lint-test-hermeticity.mjs@530d522c516d68cb84c73c77977e88ab2dca639ac8d4e8319b4dee4c669ce909`; drift-gorunurluk mekanik baseline supp — dilim sprint-spawner ve sprint-utils uretim modullerini ve zaten-izlenen test dosyasini duzenledi, unresolved ve production-inventory digest drift etti, baseline digest-only tazelendi (ayni count), sanctioned mekanik refresh | owner=Alperen; decision=APPROVED; scope=exact one-path mechanical hermeticity baseline refresh for the drift-visibility slice; exclusions=production-code,sprint,provider-call,build,destructive-action,other-files | 2026-08-09T10:46:08Z | `ONE_SHOT`: consumed@2026-08-09T10:46:08Z | | `GR-2026-08-09-STALE-SPRINT-LOCK-01` | RECOVERY-DO-DOGFOOD-001 | G1 | `scripts/clean.mjs@60a9583a9aadb78989f25a8710dd4798a1f05541c96cd4eb3fc0971432f80268`; `tests/scripts/clean-active-execution-guard.test.ts@e97f2f9e599e8cc25860fe95ec26a3de9a74cd03b5ed19f418c45f15eb9649c8`; RECOVERY-DO-DOGFOOD stale-sprint kilidi (owner canli onay 2026-08-09, A-B-C zincirinin kalici C adimi) — olculen kapali dongu: basarisiz spawn sprint-state'i PLANNING SPAWN pid null olarak birakti, clean-authority bunu AKTIF sayip build'i E_CLEAN_ACTIVE_EXECUTION_HOLD ile reddetti, kurtarma araci ise binary-identity guard'a takildi cunku dist bayatti ve dist'i tazelemek icin gereken build zaten bloke idi; yani bir basarisiz kosu arti bir kaynak duzenlemesi her iki kurtarma yolunu da kapatiyor. Owner onayiyla manuel seam kullanildi (clean-siz tsc build, finalize force, cleanup) ve kilit kirildi. Kod-truth clean.mjs sprint yuzeyinde karari YALNIZ state.status'tan veriyor canlilik kanitina hic bakmiyor, oysa ayni rapor run-flow yuzeyinde processProbe ile ucslu siniflandirma yapiyor: alive bloklar, dead bloklamayan STALE_DEAD projeksiyonu uretir, unknown typed bloklar. Bu dilim o yerlesik deseni sprint yuzeyine tasir; gercekten canli sprint bloklamaya devam eder, belirsiz durum typed unknown olarak bloklar, yalnizca kaniti olmayan olu sprint projeksiyona duser. ACIK typed-residual binary-identity guard'in kurtarma komutlarini da kilitlemesi ayri dilimdir | owner=Alperen; decision=APPROVED; scope=exact two-path liveness-aware stale-sprint classification in the clean active-execution authority with regression tests; exclusions=sprint,provider-call,destructive-action,other-files | 2026-08-09T12:22:16Z | `ONE_SHOT`: consumed@2026-08-09T12:47:18Z | | `GR-2026-08-09-STALE-SPRINT-LOCK-SUPP-01` | RECOVERY-DO-DOGFOOD-001 | G1 | `scripts/lint-test-hermeticity.mjs@63daea1a593c5d5263eb419fdd03dd86b2787ee1a6205e9212fb95f7154e6a21`; stale-sprint kilidi mekanik baseline supp — alti yeni liveness fixture'i tmpdir'a sprint-state yazdigi icin unresolved sayaci 12499'dan 12509'a cikti (durustce not edildi) ve clean.mjs degistigi icin production-inventory digest'i kaydi, baseline tazelendi, sanctioned mekanik refresh | owner=Alperen; decision=APPROVED; scope=exact one-path mechanical hermeticity baseline refresh for the stale-sprint liveness slice; exclusions=production-code,sprint,provider-call,build,destructive-action,other-files | 2026-08-09T12:47:18Z | `ONE_SHOT`: consumed@2026-08-09T12:47:18Z | +| `GR-2026-08-09-MODEL-ACTIVATION-01` | MODEL-ACTIVATION-001 | G1 | `src/core/model-activation-store.ts@ABSENT`; `src/core/model-auto-detect.ts@c97d89f22c6e7e87540cc6cb9c62fa43d5ed898a1ff0dbbd0a5d20d65db27df3`; `src/cli/commands/models.ts@30ea562cb60fc9ef9f6c3c05f08aed76e75381c931d762b56756b35d765e6595`; `tests/core/model-activation-store.test.ts@ABSENT`; MODEL-ACTIVATION-001 ilk dilimi (owner Q&A 2026-08-09; store yeri owner secimi proje-kapsamli models db) — tespit-havuzunun ustune aktiflik katmani: yeni model-activation-store SQLite tablosu provider arti model kimligi basina aktif bayragi tutar, kayit yoksa bugunku davranis korunur boylece sessiz davranis degisikligi olmaz, auto-detect kayit yolu pasif modelleri registry'ye almaz, ve deckent models yuzeyi activate ile deactivate alt komutlarini kazanir boylece son-kullanici dosya duzenlemeden yonetir. Brain modeli worker havuzuna girmez. ACIK typed-residual planner atamasinin aktif kumeye karsi dogrulanmasi ve min_tier alaninin olu olmasi ayri dilimlerdir | owner=Alperen; decision=APPROVED; scope=project-scoped model activation store plus auto-detect registration enforcement and CLI activate-deactivate management with tests; exclusions=sprint,provider-call,build,destructive-action,other-files | 2026-08-09T14:56:00Z | `ONE_SHOT`: consumed@2026-08-09T15:41:31Z | +| `GR-2026-08-09-MODEL-ACTIVATION-SUPP-01` | MODEL-ACTIVATION-001 | G1 | `scripts/lint-test-hermeticity.mjs@defab35626b6b0138d5ffbb1fdbb42dc0023fb0b47e799d6ebfd25719f1f31c3`; model-aktiflik mekanik baseline supp — yeni uretim modulu model-activation-store eklendigi icin production-inventory sayaci 1203'ten 1204'e cikti ve digest kaydi, baseline tazelendi, sanctioned mekanik refresh | owner=Alperen; decision=APPROVED; scope=exact one-path mechanical hermeticity baseline refresh for the model activation slice; exclusions=production-code,sprint,provider-call,build,destructive-action,other-files | 2026-08-09T15:41:31Z | `ONE_SHOT`: consumed@2026-08-09T15:41:31Z | ### 3.5 Typed blocker register @@ -1034,6 +1036,7 @@ specification'ını execute eder. Legacy provider adapter'ının varlığı PAEP | 7050 | HUB-001 | ECOSYSTEM-001 | ECOSYSTEM | Production-ready Deckent Hub and signed distribution | P1 | SUPPLY-CHAIN-001, PLUGIN-SANDBOX-001 | G2,G5 | BLOCKED | ~/~/0/?/0/?/? | Real key custody, package signing, verification, tenancy, moderation and rollback | Legacy 503; owner key decision required | 2026-07-26 | | 7060 | TOOL-COMPUTER-001 | ECOSYSTEM-001 | TOOL | Optional computer-use/browser automation pack | P2 | TOOL-AUTHORITY-001, PLUGIN-SANDBOX-001 | G2,G1 | OPEN | 1/~/0/?/0/0/? | Explicit install/approval, isolated permissions, replay-resistant audit and platform truth | Legacy 83 | 2026-07-26 | | 7070 | PROVIDER-EXTENSION-001 | ECOSYSTEM-001 | PROVIDER | OpenRouter and future provider extensions through PAEP | P1 | P02-637, P02-646, P02-647 | G2,G1 | OPEN | 1/~/0/?/0/?/? | No bespoke bypass; exact model/reachability/usage/policy conformance | Legacy 477 | 2026-07-26 | +| 7075 | MODEL-ACTIVATION-001 | ECOSYSTEM-001 | PROVIDER | Owner-managed model activation over the auto-detected pool | P1 | — | G1 | VERIFY | 1/1/1/1/1/0/0 | Owner can activate or deactivate any detected model per provider from a first-class surface without editing files; only active models reach the routing pool; an absent record preserves current behaviour; brain-only models never enter the worker pool | Açılış 2026-08-09: Alperen Q&A kararı (çift-mercek — ürün kullanıcısı model/provider yönetimini kolayca yapabilmeli, dogfood aynı store'u kullanır; store yeri owner seçimi `.deckent/models.db` proje-kapsamlı). Kod-truth: `model-auto-detect.ts` provider artı authMode başına gerçekten sunulan modelleri tespit edip registry'ye kaydeder ve cache'ler, fakat AKTİFLİK kavramı yoktur — tespit edilen her model havuza girer. Ölçüm 2026-08-09: codex oturumunda `o3` `gpt-5.5` `gpt-4.1` `o4-mini` `gpt-5-mini` `gpt-4.1-mini` eski nesilleri `gpt-5.6` ailesiyle yan yana duruyor ve AI-planner ilk dogfood koşusunda economy görevine `gpt-5-mini` atadı. İlişkili iki kusur da ölçüldü ve ayrı residual'dır: `min_tier` config alanı `model-selector.ts` içinde HİÇ okunmuyor (yalnız `haiku_allowed` türetiliyor, oysa şema tersini vaat ediyor) ve tier tabanı planner'ın atamasına uygulanmıyor; `currentGeneration` bayrağı doğru nesilleri işaret etse de planner onu bypass ediyor ; `receipt=GR-2026-08-09-MODEL-ACTIVATION-01`; `receipt=GR-2026-08-09-MODEL-ACTIVATION-SUPP-01`; SETTLEMENT 2026-08-09: aktiflik katmani tespit-havuzunun ustune kuruldu — ModelActivationStore (.deckent/models.db, owner secimi) provider arti model basina owner karari tutar, kaydi olmayan model AKTIF kalir yani kurulum hicbir projede sessiz daralma yapmaz. Enforcement tek kayit otoritesinde: deaktif model executable registry'den UNREGISTER edilir, yalnizca kesif listesini filtrelemek yetmezdi cunku cloud modeller bundled katalogdan zaten kayitliydi ve planner yine secebilirdi. Kullanici yuzeyi deckent models activate/deactivate/activation — dosya duzenlemeden yonetim (KANUN 1 cift-mercek, ayni store dogfood'u da yonetir). Brain modeli ayri kalir worker havuzuna girmez. Gercek-binary kosumu ile owner listesi uygulandi: codex o3 gpt-5.5 gpt-4.1 o4-mini gpt-5-mini gpt-4.1-mini ve claude opus-4-8 kapatildi, aktif havuz gpt-5.6 ailesi arti claude opus-5 sonnet-5 haiku-4-5 olarak kaldi. `proof=model-activation-12-pins-plus-42-green-plus-real-binary-7-decisions`; 4 pin arti 1 mekanik supp. ACIK typed-residual: planner atamasinin aktif kumeye karsi dogrulanmasi, min_tier alaninin model-selector'da olu olmasi, ve cross-platform arti olcek kanitlari ayri dilimlerdir | 2026-08-09 | | 7080 | IDE-ADAPTER-001 | ECOSYSTEM-001 | SURFACE | VS Code, JetBrains and future IDE adapters as non-canonical clients | P2 | APP-SERVICE-001, SURFACE-CONTRACT-001 | G2,G1 | OPEN | 1/~/0/?/0/0/? | Thin adapters, no second engine; capability matrix and honest support lifecycle | Legacy 64; native Deckent remains primary | 2026-07-26 | | 7090 | ORPHAN-WIRE-001 | ECOSYSTEM-001 | TRUTH | Production import graph orphan disposition and wiring | P0 | REPO-CLEANUP-001, SURFACE-CUTOVER-001 | G2,G1 | BLOCKED | 1/~/0/?/0/?/? | Each deliverable wired, intentionally public or owner-disposed; fresh-clone reachability proof | Waits cleanup manifest and surface cutover | 2026-07-26 | | 7100 | DEP-SUPPLY-DEFENSE-001 | ECOSYSTEM-001 | SECURITY | npm dependency supply-chain savunmasını ürün özelliği olarak değerlendir: worker/CI install yollarında install-script guard, lockfile-integrity gate, bilinen-IOC taraması ve editör-hook (workspace-trust) koruması | P1 | — | G2,G1 | OPEN | 0/0/0/?/0/?/? | Owner değerlendirmesi: kapsam ve tasarım kararı verilir; kabul kriterleri, platform matrisi ve enforcement modu (advisory/enforce) değerlendirme sonucunda tiplenir; karar redde de çıkabilir | Açılış: Alperen 2026-08-04 talebi ("değerlendirilecek madde"). Tetikleyici: 2026-08-04 "Shai-Hulud: Here We Go Again" npm worm'u — keyv@6.0.0 başlangıç, ~1.684 zehirli versiyon/420 paket adı, preinstall `setup.mjs` + Bun-derlenmiş payload, repo'lara `.claude/settings.json` SessionStart ve `.vscode/tasks.json` folderOpen hook'ları, `gh-token-monitor` persistence ve dead-man's-switch token izleyicisi. Bu repo aynı gün denetlendi ve TEMİZ: hiçbir lockfile'da etkilenen versiyon yok (desktop `keyv@4.5.4` integrity-pinli, saldırı öncesi sürüm), IOC dosyası/persistence/editör-hook izi yok, o gün install koşmadı. Değerlendirme sorusu (Dual-Lens): deckent worker'ları kullanıcı projelerinde bağımlılık kurabildiği için bu savunma katmanı hem dogfood hem son-kullanıcı ürünü olarak hangi kapsamda inşa edilmeli. Kaynaklar: safedep.io/keyv-npm-supply-chain-compromise, thehackernews.com/2026/08/keyv-linked-npm-worm-poisons-hundreds.html | 2026-08-04 | diff --git a/docs/generated/master-plan-active.json b/docs/generated/master-plan-active.json index c0320fd86..c3bf39ded 100644 --- a/docs/generated/master-plan-active.json +++ b/docs/generated/master-plan-active.json @@ -3,26 +3,26 @@ "generatedFrom": "docs/MASTER-PLAN.md", "sourceDigest": { "algorithm": "sha256(normalized-lf-utf8)", - "value": "21982a53897bb78912eb8c9e7adbb81baec0ad54259048250bebc30e2174ab3f" + "value": "ee2f529f8f9f45605f7cca65e12664d903338e6a2df3df9d276e2dcdb5c40127" }, "summary": { - "total": 397, - "active": 350, + "total": 398, + "active": 351, "terminal": 47, - "receipts": 146, + "receipts": 148, "byState": { "OPEN": 259, "READY": 0, "IN_PROGRESS": 0, "BLOCKED": 67, - "VERIFY": 24, + "VERIFY": 25, "DONE": 47, "DEFERRED": 0, "DISPOSED": 0 }, "byPriority": { "P0": 251, - "P1": 109, + "P1": 110, "P2": 37 }, "byProgram": { @@ -49,7 +49,7 @@ "PAEP": 35, "PRODUCT": 12, "PROMPT": 3, - "PROVIDER": 3, + "PROVIDER": 4, "RELEASE": 4, "REPO": 1, "RESILIENCE": 1, @@ -3925,6 +3925,17 @@ "progressDigest": "3c1ca2cea18a2424ea1917339241f91933cf282e9ab1c1e90ebe5b00ee5d3d98", "terminalClosureDigest": null }, + { + "order": 7075, + "id": "MODEL-ACTIVATION-001", + "program": "PROVIDER", + "priority": "P1", + "state": "VERIFY", + "updated": "2026-08-09", + "definitionDigest": "c71612dea49f09b12bc2f171efafb362999df95ec8af8d9be07b7185e7dfe7c0", + "progressDigest": "d267c01d80072610256b0be908fa998e701078ad00462dde75e8be90e5d7e531", + "terminalClosureDigest": null + }, { "order": 7080, "id": "IDE-ADAPTER-001", @@ -5892,6 +5903,26 @@ "transitionAt": "2026-08-09T12:47:18Z" }, "g7AttemptIdentity": null + }, + { + "id": "GR-2026-08-09-MODEL-ACTIVATION-01", + "authorityDigest": "85191c898227109c1aaec3937605c2cd318143b4d4f7c9235d395abd2ef7a697", + "lifecycle": { + "mode": "ONE_SHOT", + "status": "consumed", + "transitionAt": "2026-08-09T15:41:31Z" + }, + "g7AttemptIdentity": null + }, + { + "id": "GR-2026-08-09-MODEL-ACTIVATION-SUPP-01", + "authorityDigest": "615b55c232600bc539ba146957edfc10558230805ce3a775ca2c0a2586f37c2c", + "lifecycle": { + "mode": "ONE_SHOT", + "status": "consumed", + "transitionAt": "2026-08-09T15:41:31Z" + }, + "g7AttemptIdentity": null } ], "workItems": [ @@ -18495,6 +18526,7 @@ "HUB-001", "TOOL-COMPUTER-001", "PROVIDER-EXTENSION-001", + "MODEL-ACTIVATION-001", "IDE-ADAPTER-001", "ORPHAN-WIRE-001", "DEP-SUPPLY-DEFENSE-001", @@ -18529,6 +18561,7 @@ "HUB-001", "TOOL-COMPUTER-001", "PROVIDER-EXTENSION-001", + "MODEL-ACTIVATION-001", "IDE-ADAPTER-001", "ORPHAN-WIRE-001", "DEP-SUPPLY-DEFENSE-001", @@ -18856,6 +18889,40 @@ "evidence": "Legacy 477", "updated": "2026-07-26" }, + { + "order": 7075, + "id": "MODEL-ACTIVATION-001", + "parent": "ECOSYSTEM-001", + "program": "PROVIDER", + "outcome": "Owner-managed model activation over the auto-detected pool", + "priority": "P1", + "dependsOn": [], + "dependents": [], + "children": [], + "gates": [ + "G1" + ], + "state": "VERIFY", + "truth": { + "C": "1", + "W": "1", + "E": "1", + "H": "1", + "L": "1", + "X": "0", + "S": "0" + }, + "blockerCode": null, + "blockerRemedy": null, + "closureBlockedBy": [], + "evidenceReceipts": [ + "GR-2026-08-09-MODEL-ACTIVATION-01", + "GR-2026-08-09-MODEL-ACTIVATION-SUPP-01" + ], + "acceptance": "Owner can activate or deactivate any detected model per provider from a first-class surface without editing files; only active models reach the routing pool; an absent record preserves current behaviour; brain-only models never enter the worker pool", + "evidence": "Açılış 2026-08-09: Alperen Q&A kararı (çift-mercek — ürün kullanıcısı model/provider yönetimini kolayca yapabilmeli, dogfood aynı store'u kullanır; store yeri owner seçimi `.deckent/models.db` proje-kapsamlı). Kod-truth: `model-auto-detect.ts` provider artı authMode başına gerçekten sunulan modelleri tespit edip registry'ye kaydeder ve cache'ler, fakat AKTİFLİK kavramı yoktur — tespit edilen her model havuza girer. Ölçüm 2026-08-09: codex oturumunda `o3` `gpt-5.5` `gpt-4.1` `o4-mini` `gpt-5-mini` `gpt-4.1-mini` eski nesilleri `gpt-5.6` ailesiyle yan yana duruyor ve AI-planner ilk dogfood koşusunda economy görevine `gpt-5-mini` atadı. İlişkili iki kusur da ölçüldü ve ayrı residual'dır: `min_tier` config alanı `model-selector.ts` içinde HİÇ okunmuyor (yalnız `haiku_allowed` türetiliyor, oysa şema tersini vaat ediyor) ve tier tabanı planner'ın atamasına uygulanmıyor; `currentGeneration` bayrağı doğru nesilleri işaret etse de planner onu bypass ediyor ; `receipt=GR-2026-08-09-MODEL-ACTIVATION-01`; `receipt=GR-2026-08-09-MODEL-ACTIVATION-SUPP-01`; SETTLEMENT 2026-08-09: aktiflik katmani tespit-havuzunun ustune kuruldu — ModelActivationStore (.deckent/models.db, owner secimi) provider arti model basina owner karari tutar, kaydi olmayan model AKTIF kalir yani kurulum hicbir projede sessiz daralma yapmaz. Enforcement tek kayit otoritesinde: deaktif model executable registry'den UNREGISTER edilir, yalnizca kesif listesini filtrelemek yetmezdi cunku cloud modeller bundled katalogdan zaten kayitliydi ve planner yine secebilirdi. Kullanici yuzeyi deckent models activate/deactivate/activation — dosya duzenlemeden yonetim (KANUN 1 cift-mercek, ayni store dogfood'u da yonetir). Brain modeli ayri kalir worker havuzuna girmez. Gercek-binary kosumu ile owner listesi uygulandi: codex o3 gpt-5.5 gpt-4.1 o4-mini gpt-5-mini gpt-4.1-mini ve claude opus-4-8 kapatildi, aktif havuz gpt-5.6 ailesi arti claude opus-5 sonnet-5 haiku-4-5 olarak kaldi. `proof=model-activation-12-pins-plus-42-green-plus-real-binary-7-decisions`; 4 pin arti 1 mekanik supp. ACIK typed-residual: planner atamasinin aktif kumeye karsi dogrulanmasi, min_tier alaninin model-selector'da olu olmasi, ve cross-platform arti olcek kanitlari ayri dilimlerdir", + "updated": "2026-08-09" + }, { "order": 7080, "id": "IDE-ADAPTER-001", @@ -20589,6 +20656,6 @@ ], "registryIntegrity": { "algorithm": "sha256(canonical-json-utf8)", - "value": "8bfc317ed63756395a3990e8c76720ef4f07d43afc75428a0f2ac527645903ad" + "value": "893f95178c252b26fbd9c80325f8e1209880d78fe6cf5d53c166ddd5ae47c50c" } } diff --git a/docs/generated/master-plan-active.md b/docs/generated/master-plan-active.md index c25777c01..6690823dc 100644 --- a/docs/generated/master-plan-active.md +++ b/docs/generated/master-plan-active.md @@ -5,9 +5,9 @@ **Schema:** 3 -**Source digest:** `sha256(normalized-lf-utf8):21982a53897bb78912eb8c9e7adbb81baec0ad54259048250bebc30e2174ab3f` +**Source digest:** `sha256(normalized-lf-utf8):ee2f529f8f9f45605f7cca65e12664d903338e6a2df3df9d276e2dcdb5c40127` -**Rows:** 397 total · 350 active · 47 terminal +**Rows:** 398 total · 351 active · 47 terminal ## State summary @@ -17,7 +17,7 @@ | READY | 0 | | IN_PROGRESS | 0 | | BLOCKED | 67 | -| VERIFY | 24 | +| VERIFY | 25 | | DONE | 47 | | DEFERRED | 0 | | DISPOSED | 0 | @@ -333,6 +333,7 @@ | 7050 | `HUB-001` | BLOCKED | P1 | ECOSYSTEM | `SUPPLY-CHAIN-001`, `PLUGIN-SANDBOX-001` | `OWNER_DECISION_REQUIRED` | Production-ready Deckent Hub and signed distribution | | 7060 | `TOOL-COMPUTER-001` | OPEN | P2 | TOOL | `TOOL-AUTHORITY-001`, `PLUGIN-SANDBOX-001` | — | Optional computer-use/browser automation pack | | 7070 | `PROVIDER-EXTENSION-001` | OPEN | P1 | PROVIDER | `P02-637`, `P02-646`, `P02-647` | — | OpenRouter and future provider extensions through PAEP | +| 7075 | `MODEL-ACTIVATION-001` | VERIFY | P1 | PROVIDER | — | — | Owner-managed model activation over the auto-detected pool | | 7080 | `IDE-ADAPTER-001` | OPEN | P2 | SURFACE | `APP-SERVICE-001`, `SURFACE-CONTRACT-001` | — | VS Code, JetBrains and future IDE adapters as non-canonical clients | | 7090 | `ORPHAN-WIRE-001` | BLOCKED | P0 | TRUTH | `REPO-CLEANUP-001`, `SURFACE-CUTOVER-001` | `DEPENDENCY_UNSATISFIED` | Production import graph orphan disposition and wiring | | 7100 | `DEP-SUPPLY-DEFENSE-001` | OPEN | P1 | SECURITY | — | — | npm dependency supply-chain savunmasını ürün özelliği olarak değerlendir: worker/CI install yollarında install-script guard, lockfile-integrity gate, bilinen-IOC taraması ve editör-hook (workspace-trust) koruması | diff --git a/scripts/lint-test-hermeticity.mjs b/scripts/lint-test-hermeticity.mjs index 7081d9ff5..3ccf9b948 100644 --- a/scripts/lint-test-hermeticity.mjs +++ b/scripts/lint-test-hermeticity.mjs @@ -160,7 +160,7 @@ export const PRODUCTION_INVENTORY_BASELINE = Object.freeze({ // autonomous ingresses all import — every local pendingPath helper deleted so // one resolver remains. Edits to already-inventoried modules only — same 1203 // count, content digest only. - count: 1203, + count: 1204, // 2026-08-08 (TOOL-AUTHORITY filesystem-write-guard): resolveWriteScopeShellEscape // predicate in provider-command-spec + writeScopeShellEscape wiring in sprint-spawner // (both already-inventoried); same 1203 count, content digest only. @@ -173,7 +173,10 @@ export const PRODUCTION_INVENTORY_BASELINE = Object.freeze({ // 2026-08-09 (STALE-SPRINT-LOCK): liveness-aware sprint classification in the // clean active-execution authority (already-inventoried); same 1203 count, // content digest only. - digest: '55047869c9dae3cd966abe7b32afb37ba867e0b36aaa61569e946fdb1006808b', + // 2026-08-09 (MODEL-ACTIVATION-001): +1 REAL production module — + // src/core/model-activation-store.ts (owner model-activation authority), plus + // the auto-detect enforcement and CLI edits. Prior: STALE-SPRINT-LOCK (1203). + digest: '59e9ed31c8606db48b1bf50e42637763a3de176febad8ba0bc760a146a73b0f9', }); const PROTECTED_ROOT_POLICY = new Map([ diff --git a/src/cli/commands/models.ts b/src/cli/commands/models.ts index 133686135..f57f0c90e 100644 --- a/src/cli/commands/models.ts +++ b/src/cli/commands/models.ts @@ -10,6 +10,8 @@ import { } from '../../core/model-catalog.js'; import type { ModelDefinition } from '../../core/model-registry.js'; import { print, printError, color } from '../helpers/output.js'; +import { ModelActivationStore } from '../../core/model-activation-store.js'; +import { resolveProjectRoot } from '../helpers/process.js'; // ─── Tier Display ────────────────────────────────────────────────────────── @@ -138,6 +140,72 @@ export function registerModels(program: Command): void { } }); + // ── deckent models activate|deactivate|activation ──────────────────────── + // MODEL-ACTIVATION-001: detection says what a provider OFFERS; these say what + // the owner ALLOWS. First-class surface so model/provider management never + // requires editing a file (dual-lens: the same store governs dogfood runs). + // A model with NO record is active, so an untouched project is unchanged. + function withStore(fn: (store: ModelActivationStore) => T): T { + const store = new ModelActivationStore(resolveProjectRoot()); + try { + return fn(store); + } finally { + store.close(); + } + } + + models + .command('activate ') + .description('Allow a detected model to enter the routing pool') + .requiredOption('--provider ', 'Provider that serves this model') + .action((model: string, opts: { provider: string }) => { + try { + withStore((store) => store.setActivation(opts.provider, model, true)); + print(` ${color('\x1b[32m', '✓')} ${opts.provider}/${model} activated`); + } catch (err) { + printError(err); + process.exitCode = 1; + } + }); + + models + .command('deactivate ') + .description('Remove a model from the routing pool (detection still sees it)') + .requiredOption('--provider ', 'Provider that serves this model') + .action((model: string, opts: { provider: string }) => { + try { + withStore((store) => store.setActivation(opts.provider, model, false)); + print(` ${color('\x1b[33m', '✓')} ${opts.provider}/${model} deactivated — it will not be routed`); + } catch (err) { + printError(err); + process.exitCode = 1; + } + }); + + models + .command('activation') + .description('Show recorded model activation decisions (unrecorded = active)') + .action(() => { + try { + const records = withStore((store) => store.list()); + if (records.length === 0) { + print('\n No activation decisions recorded — every detected model is active.\n'); + return; + } + print(`\n ${color('\x1b[1m', 'Model Activation')} ${records.length} decision(s)\n`); + for (const r of records) { + const mark = r.active + ? color('\x1b[32m', 'active ') + : color('\x1b[31m', 'inactive'); + print(` ${mark} ${r.provider}/${r.modelId} ${color('\x1b[2m', `(${r.actor}, ${r.updatedAt})`)}`); + } + print(''); + } catch (err) { + printError(err); + process.exitCode = 1; + } + }); + // ── deckent models refresh ─────────────────────────────────────────────── models .command('refresh') diff --git a/src/core/model-activation-store.ts b/src/core/model-activation-store.ts new file mode 100644 index 000000000..5e948ca36 Binary files /dev/null and b/src/core/model-activation-store.ts differ diff --git a/src/core/model-auto-detect.ts b/src/core/model-auto-detect.ts index 38e4cd32d..79a6d7e99 100644 --- a/src/core/model-auto-detect.ts +++ b/src/core/model-auto-detect.ts @@ -17,6 +17,7 @@ import { homedir } from 'node:os'; import { join, dirname } from 'node:path'; import { BUILTIN_MODELS } from './model-registry.js'; import type { ModelRegistry, ModelDefinition, RegistryProviderName } from './model-registry.js'; +import { activationKey, readInactiveModels } from './model-activation-store.js'; import { buildParametricModel, inferProviderFromId } from './model-registry.js'; import { killProcessGroupWithEscalation } from './process-tree-termination.js'; @@ -64,6 +65,16 @@ export interface DetectAndRegisterOptions extends ProbeOptions { ttlMs?: number; /** Now provider for deterministic tests. */ now?: () => number; + /** + * Project root used to resolve the owner's model-activation decisions + * (MODEL-ACTIVATION-001). Detection discovers what a provider OFFERS; the + * activation store records what the owner ALLOWS. Absent → no activation + * filtering at all (every detected model stays eligible), so callers that + * predate the store behave exactly as before. + */ + projectRoot?: string; + /** Override the activation lookup (tests). */ + inactiveModels?: ReadonlySet; } // ─── Constants ─────────────────────────────────────────────────────────────── @@ -327,6 +338,10 @@ export async function detectAndRegisterModels( const cacheDir = opts.cacheDir ?? AUTO_DETECT_CACHE_DIR; const ttl = opts.ttlMs ?? AUTO_DETECT_TTL_MS; const now = opts.now ?? (() => Date.now()); + // Resolved ONCE per sweep: the owner's deactivation set. Fail-safe — an absent + // or unreadable store yields an empty set, so discovery never breaks on it. + const inactiveModels = opts.inactiveModels + ?? (opts.projectRoot ? readInactiveModels(opts.projectRoot) : new Set()); const results: DetectResult[] = []; @@ -376,6 +391,21 @@ export async function detectAndRegisterModels( if (source === 'empty' && discovered.length > 0) source = 'catalog'; } + // MODEL-ACTIVATION-001: detection says what the provider OFFERS; the owner's + // activation store says what may actually be USED. Apply that decision at + // this single registration authority, so every downstream consumer (routing, + // planner, tier equivalence) sees only the allowed pool. A model with no + // record is active, so a project without decisions is unchanged. + const deactivated = discovered.filter( + (id) => inactiveModels.has(activationKey(provider, id)), + ); + if (deactivated.length > 0) { + discovered = discovered.filter((id) => !deactivated.includes(id)); + // Already-registered catalog models must LEAVE the executable registry — + // filtering the discovery list alone would still leave them selectable. + for (const id of deactivated) registry.unregister(id); + } + // A CLI discovery is reachability evidence, not pricing/catalog authority. // Unknown cloud IDs remain outside the executable registry until a priced // catalog producer admits them. Local Ollama tags are the sole zero-cost diff --git a/tests/core/model-activation-store.test.ts b/tests/core/model-activation-store.test.ts new file mode 100644 index 000000000..3b1b14693 --- /dev/null +++ b/tests/core/model-activation-store.test.ts @@ -0,0 +1,207 @@ +// MODEL-ACTIVATION-001 — the owner's allow-decision over the auto-detected pool. +// +// Measured 2026-08-09: `model-auto-detect` registers every model a provider +// offers, with no notion of activation, so old generations (`gpt-5-mini`, +// `gpt-4.1`, `o3`, …) sat beside the current ones and the AI planner picked one. +// These pins hold the two properties that make the store safe to ship: an +// unrecorded model stays ACTIVE (no silent narrowing), and a deactivated model +// actually leaves the executable registry (not just the discovery list). + +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { mkdtempSync, rmSync, existsSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import { + ModelActivationStore, + ModelActivationStoreError, + activationKey, + readInactiveModels, +} from '../../src/core/model-activation-store.js'; +import { detectAndRegisterModels } from '../../src/core/model-auto-detect.js'; +import { ModelRegistry } from '../../src/core/model-registry.js'; + +let root: string; + +beforeEach(() => { + root = mkdtempSync(join(tmpdir(), 'model-activation-')); +}); + +afterEach(() => { + rmSync(root, { recursive: true, force: true }); +}); + +function open(): ModelActivationStore { + return new ModelActivationStore(root, { now: () => '2026-08-09T00:00:00.000Z' }); +} + +describe('ModelActivationStore — default-preserving activation', () => { + it('an UNRECORDED model is active, so installing the store narrows nothing', () => { + const store = open(); + try { + expect(store.isActive('codex', 'gpt-5.6-terra')).toBe(true); + expect(store.isActive('claude', 'anything-at-all')).toBe(true); + expect(store.list()).toEqual([]); + } finally { + store.close(); + } + }); + + it('records a deactivation and reports it', () => { + const store = open(); + try { + store.setActivation('codex', 'gpt-5-mini', false, 'owner'); + expect(store.isActive('codex', 'gpt-5-mini')).toBe(false); + expect(store.list()).toEqual([{ + provider: 'codex', + modelId: 'gpt-5-mini', + active: false, + updatedAt: '2026-08-09T00:00:00.000Z', + actor: 'owner', + }]); + } finally { + store.close(); + } + }); + + it('re-activating overwrites the decision rather than duplicating it', () => { + const store = open(); + try { + store.setActivation('codex', 'gpt-5-mini', false); + store.setActivation('codex', 'gpt-5-mini', true); + expect(store.list()).toHaveLength(1); + expect(store.isActive('codex', 'gpt-5-mini')).toBe(true); + } finally { + store.close(); + } + }); + + it('clearing a decision restores the default (active)', () => { + const store = open(); + try { + store.setActivation('codex', 'o3', false); + expect(store.clearActivation('codex', 'o3')).toBe(true); + expect(store.isActive('codex', 'o3')).toBe(true); + expect(store.clearActivation('codex', 'o3')).toBe(false); + } finally { + store.close(); + } + }); + + it('decisions are per-provider — the same model id under another provider is untouched', () => { + const store = open(); + try { + store.setActivation('codex', 'shared-id', false); + expect(store.isActive('codex', 'shared-id')).toBe(false); + expect(store.isActive('gemini', 'shared-id')).toBe(true); + } finally { + store.close(); + } + }); + + it('refuses empty provider/model input instead of writing a junk row', () => { + const store = open(); + try { + expect(() => store.setActivation('', 'm', false)).toThrowError(ModelActivationStoreError); + expect(() => store.setActivation('codex', ' ', false)).toThrowError(/modelId/u); + expect(store.list()).toEqual([]); + } finally { + store.close(); + } + }); + + it('persists across connections (the decision is durable, not in-memory)', () => { + const first = open(); + try { + first.setActivation('codex', 'gpt-4.1', false); + } finally { + first.close(); + } + expect(existsSync(join(root, '.deckent', 'models.db'))).toBe(true); + const second = open(); + try { + expect(second.isActive('codex', 'gpt-4.1')).toBe(false); + } finally { + second.close(); + } + }); +}); + +describe('readInactiveModels — the registration path lookup', () => { + it('is empty and NEVER throws when no store exists (fail-safe discovery)', () => { + const missing = mkdtempSync(join(tmpdir(), 'model-activation-none-')); + try { + expect(readInactiveModels(missing).size).toBe(0); + } finally { + rmSync(missing, { recursive: true, force: true }); + } + }); + + it('returns exactly the deactivated pairs, keyed by provider+model', () => { + const store = open(); + try { + store.setActivation('codex', 'gpt-5-mini', false); + store.setActivation('codex', 'gpt-5.6-terra', true); + store.setActivation('claude', 'claude-opus-4-8', false); + } finally { + store.close(); + } + + const inactive = readInactiveModels(root); + expect(inactive.has(activationKey('codex', 'gpt-5-mini'))).toBe(true); + expect(inactive.has(activationKey('claude', 'claude-opus-4-8'))).toBe(true); + expect(inactive.has(activationKey('codex', 'gpt-5.6-terra'))).toBe(false); + expect(inactive.size).toBe(2); + }); +}); + +// ═══ Enforcement: a deactivated model leaves the executable registry ═════════ +// Filtering the discovery LIST alone is not enough — cloud models are already in +// the registry from the bundled catalog, so a planner could still name one. +describe('detectAndRegisterModels — activation enforcement', () => { + it('unregisters a deactivated model so nothing downstream can select it', async () => { + const registry = new ModelRegistry(); + const before = registry.getAllModels().filter((m) => m.provider === 'claude'); + expect(before.length).toBeGreaterThan(0); + const victim = before[0]!.id; + + const [result] = await detectAndRegisterModels(registry, { + providers: ['claude'], + offline: true, + cacheDir: root, + inactiveModels: new Set([activationKey('claude', victim)]), + }); + + expect(registry.has(victim)).toBe(false); + expect(result?.discovered).not.toContain(victim); + }); + + it('leaves every model registered when nothing is deactivated (default path)', async () => { + const registry = new ModelRegistry(); + const claudeIds = registry.getAllModels() + .filter((m) => m.provider === 'claude').map((m) => m.id); + + await detectAndRegisterModels(registry, { + providers: ['claude'], + offline: true, + cacheDir: root, + inactiveModels: new Set(), + }); + + for (const id of claudeIds) expect(registry.has(id)).toBe(true); + }); + + it('omitting projectRoot AND inactiveModels keeps the pre-store behaviour', async () => { + const registry = new ModelRegistry(); + const claudeIds = registry.getAllModels() + .filter((m) => m.provider === 'claude').map((m) => m.id); + + await detectAndRegisterModels(registry, { + providers: ['claude'], + offline: true, + cacheDir: root, + }); + + for (const id of claudeIds) expect(registry.has(id)).toBe(true); + }); +});