diff --git a/.github/workflows/cli-ci.yml b/.github/workflows/cli-ci.yml index 15875fe42..1c4a14b1d 100644 --- a/.github/workflows/cli-ci.yml +++ b/.github/workflows/cli-ci.yml @@ -350,6 +350,7 @@ jobs: - name: Install Kilo Code CLI run: npm install -g @kilocode/cli@7.7.5 - run: cd cli && pnpm test:e2e:kilo + - run: cd cli && pnpm test:e2e:kilo:native cli-build: name: cli / Build & Bundle Budget diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/challenge.md b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/challenge.md new file mode 100644 index 000000000..e6c719f98 --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/challenge.md @@ -0,0 +1,31 @@ +My confidence level of correctness now: 90% + +Rapport historique de planification, avant l’autorisation et l’implémentation de phase 1 ; aucune preuve d’implémentation ne se déduit de ce score. + +# Correctness (100%) + +- Capacité installée `aidd-refine:02-challenge` effectivement appliquée dans le caller Codex après production et lecture du plan, des six phases et de la matrice. Référence convenue : seize critères officiels #914, demande utilisateur actualisée, architecture et CONTRIBUTING. Ce pourcentage évalue la justesse du plan révisé, jamais une implémentation inexistante. +- Alignement : les seize lignes reprennent les critères officiels sans regroupement qui en cache un. Aucun code fonctionnel modifié ; plan et phases pending, développement soumis à validation explicite. +- Responsabilité correcte : aidd-context produit les artefacts ; CLI conserve profils et bridge. `02-project-init` obsolète se projette sur mémoire, sans ajout inutile à bootstrap. Aucun développement flat #868 ni hook runtime générique. +- Canonique Claude : templates conservés ; exceptions Kilo ciblées dans rendu/validation, notamment filename agent, choix portable skill et guidance hook. Relance Claude hooks couverte car le code actuel peut dupliquer les entrées. +- Configuration : sélection explicite en ambiguïté, réutilisation owner unique, édit minimal JSONC, conservation des duplications utilisateur. Les doublons sur disque sont un oracle distinct du tableau d’instructions fusionné par Kilo. +- Challenge résolu : le preflight mémoire s’applique avant Upsert, pas seulement avant le sous-processus Fill. Sinon une erreur tardive pouvait laisser un nouveau contexte. Les tests capturent le projet entier à l’entrée de l’action sync, y compris une cible invalide en dernier. +- Challenge résolu : plusieurs rename atomiques ne prouvent pas l’absence d’écriture partielle. Phase règles inclut staging global, contrôles de changement et restauration sur les pannes testées. Ne pas revendiquer une garantie crash-machine universelle. +- Challenge résolu : la CI Kilo existante est Ubuntu-only sur 7.7.5. Phase 6 prévoit de vérifier les plateformes supportées et les contraintes du runner avant une adaptation ciblée du job existant, avec sorties réellement générées/hashées et inférence locale contrôlée. Aucun compte Codex ou modèle gratuit externe supposé disponible en CI. +- Challenge résolu : le harness actuel appelle claude -p authentifié. Le plan prévoit des parcours Codex réels dès chaque phase, puis une extension ciblée du runner ; les sorties Claude sont vérifiables sans abonnement, mais cette preuve n’est pas une exécution de générateur dans Claude authentifié. +- #979 revalidée Draft/open, next ; l’alignement du writer est un gate explicite de phase 2. Pas de mécanisme concurrent conçu en parallèle, pas de modification Git anticipée. Les changements Codex/OpenCode de cette base sont isolés des régressions introduites par Kilo. +- Accès gratuit Kilo observé avec coût final 0 ; absence de credentials enregistrés déclarée. Les tests runtime restent futurs ; la sonde de connectivité ne valide ni découverte ni utilisation d’un artefact #914. +- Trust : fier du plan livré : oui ; confiance dans les décisions conséquentes du plan conditionnel : oui ; réponse au résultat attendu de cette session : oui. Les gates d’implémentation et accès externes ne sont pas dissimulés comme des preuves obtenues. + +- Aucun deal breaker pour la remise du plan révisé. L’absence actuelle du writer #979 dans HEAD bloque le démarrage de la phase 2 tant que la base n’est pas alignée ou le plan explicitement amendé ; elle ne bloque pas les phases indépendantes ni l’approbation de ce plan. +- Runtime Claude authentifié indisponible, OpenCode absent du PATH, Kilo multi-OS non exécuté : validations futures déclarées non acquises. Aucun critère #914 ne requiert spécifiquement un modèle Claude, mais la demande de CONTRIBUTING n’est pas satisfaite par le seul offline. Ne pas annoncer une validation finale complète sans publier cette limite. + +# Deal breakers + +# Suggestions (enhancements only) + +- Si le writer #979 évolue avant intégration, tenir un petit reçu de revalidation head/base/tests dans les preuves de phase 2 ; ne pas copier ses données historiques en les présentant comme nouvelles. +- Pour limiter le coût des évaluations LLM, réserver les répétitions aux chemins à risque : sélection ambiguë, refus tardif, portable et relances. Garder le corpus déterministe pour les invariants byte-level, et les parcours runtime réels pour découverte/usage. +- Garder une PR future cohérente couvrant #914 plutôt que supposer un découpage Git aujourd’hui. L’ordre des phases est une organisation du travail, pas une autorisation de commit ou de PR. + +- Précisions après validation utilisateur : le score historique de 90 % porte uniquement sur le plan. Les ajustements sont ciblés : preuves progressives et statut réel par tâche, phase 1 seule autorisée, gate #979 explicite, JSONC sans moteur générique et extension CI conditionnée au support/runner vérifiés. Aucun critère officiel supprimé ; aucun score de qualité d’implémentation déduit du challenge. diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/cleanup-report.md b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/cleanup-report.md new file mode 100644 index 000000000..80d67b560 --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/cleanup-report.md @@ -0,0 +1,251 @@ +# Nettoyage documentaire de la PR #992 + +Date : 12 octobre 2026. Dossier concerné : cette archive de tâche uniquement. + +## Résultat + +Inventaire initial : 212 fichiers, 2,1 Mo. Après suppression de 157 fichiers, 55 fichiers existants sont conservés ; ce rapport ajoute un 56e fichier. L’objectif était une réduction forte vers 42, pas un quota. Volume final : 472 Ko alloués (`du -sh`), 291 776 octets de taille apparente (`du -sb`). + +Les plans actifs, les six phases, rapports, matrice, décisions importantes, limites et un minimum de replay de phase 4 sont conservés. Les résultats historiques dont les journaux ont été retirés restent identifiés comme historiques ; aucun succès nouveau n’est revendiqué. + +## Provenance et replay + +Le texte retrouvé en lecture seule dans `stash@{0}` indique que le backup local B1 conserve l’état documentaire modifié avant nettoyage, pas un original brut distinct. Aucun manifeste ou hash indépendant du backup B1, ni copie externe des exports bruts, n’a été trouvé dans le dépôt. Ne pas interpréter B1 comme une preuve d’intégrité des captures supprimées. Le snapshot machine-readable antérieur à la phase 1 a été supprimé comme doublon du plan/phases conservés et parce qu’il contenait des chemins locaux et des liens historiques obsolètes ; les décisions utiles sont résumées dans `plan-adjustments.md` et `phase-1-report.md`. + +Le stash n’a pas été appliqué, dépilé ni supprimé. Ses deux nouveaux journaux phase 2 obsolètes n’ont pas été restaurés. Le changement utile de `replay-kilo.sh` a été reporté manuellement : les sorties masquent maintenant le `$HOME` courant en `$USER_HOME` par remplacement littéral et les répertoires temporaires utilisent `$TMPDIR` lorsqu’il est défini. Cela rend le masquage indépendant d’un home particulier et sûr face aux caractères spéciaux shell, sans ajouter de garantie macOS/Windows. Le script mis à jour n’a pas été exécuté. L’essai `attempt-04` conservé correspond à une version précédente du script. Un futur replay doit utiliser un nom d’essai inédit pour préserver cette capture. + +Preuves supplémentaires gardées : jeux d’entrée/sortie expurgés de l’observation phase 2 ; preuve minimale phase 4 avec JSONL réussi, code de sortie, modèle gratuit sélectionné et hashes avant/après ; logs de mutation phase 5 utiles à l’audit du contrat de sécurité ; description de la correction CodeQL et de ses limites de plateforme. Les exports de session historiques phase 4 ont été retirés car leurs marqueurs conservaient des identifiants techniques ; leur contenu n’est pas reconstruit. + +## Limites de validation à conserver + +- Phase 2 reste partielle selon ses critères ouverts et `in-progress`. +- Phase 6 reste `in-progress`, verdict **NOT READY**. +- Aucune validation Claude authentifiée n’a été réalisée. +- La couverture macOS/Windows reste incomplète ; Linux seulement pour les observations runtime archivées ici. +- Le scénario runtime des hooks Claude/Codex/Cursor/Copilot n’a pas été validé à cause du sandbox Codex enfant. +- Un test échoué, un test non exécuté et une validation réussie gardent des statuts distincts dans les rapports. + +## Vérifications après nettoyage + +Le contrôle Markdown rapporte 0 lien cassé dans 954 fichiers ; les 16 tests des vérificateurs documentaires passent. `bash -n` accepte `replay-kilo.sh`, les hashes de la fixture correspondent au manifeste et les fichiers de hash avant/après sont identiques. Le replay lui-même n’a pas été lancé. Aucune archive conservée n’est une dépendance d’un test ou script permanent du dépôt. Aucun code fonctionnel ni test permanent n’a été modifié. Aucun fichier n’a été staged ou commité. Le `git diff --check` standard signale les deux lignes modifiées de `user-request.md` parce que le document historique conserve ses fins CRLF ; le même contrôle passe lorsque Git reconnaît explicitement CRLF comme fin de ligne (`core.whitespace=cr-at-eol`). + +Le scan de confidentialité n’a détecté aucun motif courant de secret (clés/API tokens, clés privées, mot de passe ou bearer token). L’identité du fork privé et les chemins personnels `$HOME` ont été expurgés ou normalisés dans les fichiers conservés où ils apparaissaient. Les journaux retenus demeurent des traces historiques expurgées, pas des sorties reproductibles. + +## Fichiers conservés + +- `challenge.md` +- `coverage-matrix.md` +- `environment-evidence.md` +- `evidence/phase-2/runtime-corrections-negative-config.jsonc` +- `evidence/phase-2/runtime-corrections-positive-config.jsonc` +- `evidence/phase-2/runtime-corrections-rule.md` +- `evidence/phase-2/runtime-negative-redacted.jsonl` +- `evidence/phase-2/runtime-positive-redacted.jsonl` +- `evidence/phase-2/runtime-report.md` +- `evidence/phase-2/runtime-request.json` +- `evidence/phase-4/README.md` +- `evidence/phase-4/attempt-04-outside-sandbox/artifact-hashes-consumed-after.txt` +- `evidence/phase-4/attempt-04-outside-sandbox/artifact-hashes-consumed-before.txt` +- `evidence/phase-4/attempt-04-outside-sandbox/selected-free-model.stdout.txt` +- `evidence/phase-4/attempt-04-outside-sandbox/workflow-run.exit.txt` +- `evidence/phase-4/attempt-04-outside-sandbox/workflow-run.redacted.jsonl` +- `evidence/phase-4/caller-receipt.json` +- `evidence/phase-4/extract-free-model.cjs` +- `evidence/phase-4/generated-artifact-hashes.txt` +- `evidence/phase-4/generated-project/.kilo/agents/assets/agent-payload.txt` +- `evidence/phase-4/generated-project/.kilo/agents/verify-agent.md` +- `evidence/phase-4/generated-project/.kilo/commands/verify-workflow.md` +- `evidence/phase-4/generation-validation.json` +- `evidence/phase-4/replay-kilo.sh` +- `evidence/phase-4/source-contract-hashes.txt` +- `evidence/phase-4/validate-generation.cjs` +- `evidence/phase-5/source-contract-final-sweep.log` +- `evidence/phase-5/source-contract-mutations.log` +- `evidence/security-fix/filesystem-access.md` +- `evidence/security-fix/platform-followup.md` +- `gather.md` +- `issue-914.md` +- `official-sources.md` +- `overlap-979.md` +- `phase-1-report.md` +- `phase-1-runtime.md` +- `phase-1.md` +- `phase-2-report.md` +- `phase-2.md` +- `phase-3-report.md` +- `phase-3-runtime.md` +- `phase-3.md` +- `phase-4-report.md` +- `phase-4-runtime.md` +- `phase-4.md` +- `phase-5-report.md` +- `phase-5.md` +- `phase-6-report.md` +- `phase-6.md` +- `plan-adjustments.md` +- `plan.md` +- `planning-validation.md` +- `remote-state.json` +- `test-strategy.md` +- `user-request.md` +- `cleanup-report.md` (ce manifeste et les décisions de nettoyage) + +## Fichiers supprimés + +- `assertions-phase-1.md` +- `assertions-phase-3.md` +- `assertions-phase-4.md` +- `backlog-link.json` +- `commits-phase-1.json` +- `commits-phase-3.json` +- `evidence/phase-1/artifact-validation.json` +- `evidence/phase-1/detection-green.txt` +- `evidence/phase-1/detection-mutation.txt` +- `evidence/phase-1/detection-red.txt` +- `evidence/phase-1/make-check-before-review-fixes.txt` +- `evidence/phase-1/make-check-final.txt` +- `evidence/phase-1/memory-baseline-red.txt` +- `evidence/phase-1/memory-green.txt` +- `evidence/phase-1/memory-mutation.txt` +- `evidence/phase-1/memory-red.txt` +- `evidence/phase-1/memory-review-green.txt` +- `evidence/phase-1/review-before-final.txt` +- `evidence/phase-1/review-regressions-red.txt` +- `evidence/phase-1/runtime-cli-attempt-source-hashes.json` +- `evidence/phase-1/runtime-codex-attempt-1.jsonl` +- `evidence/phase-1/runtime-codex-attempt-2.jsonl` +- `evidence/phase-1/runtime-codex-attempt-3.jsonl` +- `evidence/phase-1/runtime-codex-caller-receipt.json` +- `evidence/phase-1/runtime-codex.stderr.txt` +- `evidence/phase-1/runtime-generated-artifacts.json` +- `evidence/phase-1/runtime-kilo-hook-only-attempt.jsonl` +- `evidence/phase-1/runtime-kilo-hook-only-attempt.stderr.txt` +- `evidence/phase-1/runtime-kilo-model.json` +- `evidence/phase-1/runtime-kilo.jsonl` +- `evidence/phase-1/runtime-kilo.stderr.txt` +- `evidence/phase-1/runtime-source-hashes.json` +- `evidence/phase-1/runtime-verification.json` +- `evidence/phase-1/targeted-green.txt` +- `evidence/phase-2/focused-corrections.txt` +- `evidence/phase-2/focused-tests.log` +- `evidence/phase-2/kilo-config-probe.cjs` +- `evidence/phase-2/kilo-config-probe.json` +- `evidence/phase-2/owner-mutation.log` +- `evidence/phase-2/pre-commit.log` +- `evidence/phase-2/scripts-full-corrections.txt` +- `evidence/phase-2/scripts-tests.log` +- `evidence/phase-3/caller-fixture-inputs.py` +- `evidence/phase-3/caller-modify-rerun.json` +- `evidence/phase-3/caller-modify.py` +- `evidence/phase-3/caller-native-codex-refusal.json` +- `evidence/phase-3/caller-receipt.json` +- `evidence/phase-3/caller-refusals.json` +- `evidence/phase-3/caller-refusals.py` +- `evidence/phase-3/caller-shared-fields-refusal.json` +- `evidence/phase-3/caller-write.py` +- `evidence/phase-3/contracts-green.txt` +- `evidence/phase-3/contracts-red.txt` +- `evidence/phase-3/detection-mutation.txt` +- `evidence/phase-3/explicit-checks.txt` +- `evidence/phase-3/kilo-catalog-native.json` +- `evidence/phase-3/kilo-catalog-portable.json` +- `evidence/phase-3/kilo-free-model.json` +- `evidence/phase-3/kilo-native-exact.jsonl` +- `evidence/phase-3/kilo-native-exact.stderr.txt` +- `evidence/phase-3/kilo-native.jsonl` +- `evidence/phase-3/kilo-native.stderr.txt` +- `evidence/phase-3/kilo-portable.jsonl` +- `evidence/phase-3/kilo-portable.stderr.txt` +- `evidence/phase-3/make-check-final.txt` +- `evidence/phase-3/make-check-staged.txt` +- `evidence/phase-3/make-check.txt` +- `evidence/phase-3/preflight-mutation-weak-guard.txt` +- `evidence/phase-3/preflight-mutation.txt` +- `evidence/phase-3/review-findings-resolved.md` +- `evidence/phase-3/runtime-oracle.py` +- `evidence/phase-3/runtime-verification.json` +- `evidence/phase-3/targeted-green.txt` +- `evidence/phase-3/tracking-verification.json` +- `evidence/phase-4/attempt-02-dns-timeout/agent-debug.exit.txt` +- `evidence/phase-4/attempt-02-dns-timeout/agent-debug.stderr.txt` +- `evidence/phase-4/attempt-02-dns-timeout/agent-debug.stdout.json` +- `evidence/phase-4/attempt-02-dns-timeout/artifact-hashes-consumed-after.txt` +- `evidence/phase-4/attempt-02-dns-timeout/artifact-hashes-consumed-before.txt` +- `evidence/phase-4/attempt-02-dns-timeout/generated-artifact-hashes.txt` +- `evidence/phase-4/attempt-02-dns-timeout/kilo-version.exit.txt` +- `evidence/phase-4/attempt-02-dns-timeout/kilo-version.stderr.txt` +- `evidence/phase-4/attempt-02-dns-timeout/kilo-version.txt` +- `evidence/phase-4/attempt-02-dns-timeout/model-catalog.exit.txt` +- `evidence/phase-4/attempt-02-dns-timeout/model-catalog.stderr.txt` +- `evidence/phase-4/attempt-02-dns-timeout/source-contract-hashes.txt` +- `evidence/phase-4/attempt-02-dns-timeout/workflow-run.exit.txt` +- `evidence/phase-4/attempt-02-dns-timeout/workflow-run.redacted.jsonl` +- `evidence/phase-4/attempt-02-dns-timeout/workflow-run.stderr.txt` +- `evidence/phase-4/attempt-03-replay/agent-debug.exit.txt` +- `evidence/phase-4/attempt-03-replay/agent-debug.stderr.txt` +- `evidence/phase-4/attempt-03-replay/agent-debug.stdout.json` +- `evidence/phase-4/attempt-03-replay/artifact-hashes-consumed-after.txt` +- `evidence/phase-4/attempt-03-replay/artifact-hashes-consumed-before.txt` +- `evidence/phase-4/attempt-03-replay/generated-artifact-hashes.txt` +- `evidence/phase-4/attempt-03-replay/kilo-version.exit.txt` +- `evidence/phase-4/attempt-03-replay/kilo-version.stderr.txt` +- `evidence/phase-4/attempt-03-replay/kilo-version.txt` +- `evidence/phase-4/attempt-03-replay/model-catalog.exit.txt` +- `evidence/phase-4/attempt-03-replay/model-catalog.stderr.txt` +- `evidence/phase-4/attempt-03-replay/selected-free-model.stdout.txt` +- `evidence/phase-4/attempt-03-replay/source-contract-hashes.txt` +- `evidence/phase-4/attempt-03-replay/workflow-run.exit.txt` +- `evidence/phase-4/attempt-03-replay/workflow-run.redacted.jsonl` +- `evidence/phase-4/attempt-03-replay/workflow-run.stderr.txt` +- `evidence/phase-4/attempt-04-outside-sandbox/agent-debug.exit.txt` +- `evidence/phase-4/attempt-04-outside-sandbox/agent-debug.stderr.txt` +- `evidence/phase-4/attempt-04-outside-sandbox/agent-debug.stdout.json` +- `evidence/phase-4/attempt-04-outside-sandbox/generated-artifact-hashes.txt` +- `evidence/phase-4/attempt-04-outside-sandbox/kilo-version.exit.txt` +- `evidence/phase-4/attempt-04-outside-sandbox/kilo-version.stderr.txt` +- `evidence/phase-4/attempt-04-outside-sandbox/kilo-version.txt` +- `evidence/phase-4/attempt-04-outside-sandbox/main-session-export.exit.txt` +- `evidence/phase-4/attempt-04-outside-sandbox/main-session-export.stderr.txt` +- `evidence/phase-4/attempt-04-outside-sandbox/main-session.sanitized.json` +- `evidence/phase-4/attempt-04-outside-sandbox/model-catalog.exit.txt` +- `evidence/phase-4/attempt-04-outside-sandbox/model-catalog.stderr.txt` +- `evidence/phase-4/attempt-04-outside-sandbox/source-contract-hashes.txt` +- `evidence/phase-4/attempt-04-outside-sandbox/subagent-session-export.exit.txt` +- `evidence/phase-4/attempt-04-outside-sandbox/subagent-session-export.stderr.txt` +- `evidence/phase-4/attempt-04-outside-sandbox/subagent-session.sanitized.json` +- `evidence/phase-4/attempt-04-outside-sandbox/workflow-run.stderr.txt` +- `evidence/phase-4/historical-session-export/main-session-export.exit.txt` +- `evidence/phase-4/historical-session-export/main-session-export.stderr.txt` +- `evidence/phase-4/historical-session-export/main-session.sanitized.json` +- `evidence/phase-4/historical-session-export/subagent-session-export.exit.txt` +- `evidence/phase-4/historical-session-export/subagent-session-export.stderr.txt` +- `evidence/phase-4/historical-session-export/subagent-session.sanitized.json` +- `evidence/phase-5/caller-receipt.json` +- `evidence/phase-5/diff-check-final.log` +- `evidence/phase-5/fixture-hashes.txt` +- `evidence/phase-5/kilo-7.7.5-install.txt` +- `evidence/phase-5/kilo-7.7.5-smoke-sandbox-blocked.log` +- `evidence/phase-5/kilo-7.7.5-smoke.log` +- `evidence/phase-5/kilo-isolation-verification.txt` +- `evidence/phase-5/make-check-final.log` +- `evidence/phase-5/make-check.log` +- `evidence/phase-5/markdown-links-final.log` +- `evidence/phase-5/persona-rerun.log` +- `evidence/phase-5/rerun-after-mtime.txt` +- `evidence/phase-5/rerun-before-mtime.txt` +- `evidence/phase-5/rerun-decision.txt` +- `evidence/phase-5/review.md` +- `evidence/phase-5/source-contract-mutation-probe.log` +- `evidence/phase-5/source-contract-mutations-iteration-1.log` +- `evidence/phase-5/test-changed-final.log` +- `evidence/phase-5/test-changed.log` +- `evidence/phase-5/validation.json` +- `evidence/security-fix/pre-security-fix-review.md` +- `explore.md` +- `next-session.md` +- `phase-1-request.md` +- `phase-5-runtime.md` +- `plan-before-phase-1.json` +- `resume.md` +- `review-phase-1.md` +- `review.md` diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/coverage-matrix.md b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/coverage-matrix.md new file mode 100644 index 000000000..b6552c8a8 --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/coverage-matrix.md @@ -0,0 +1,50 @@ +# Matrice de traçabilité #914 + +Les critères ci-dessous sont repris **verbatim et dans leur ordre officiel**. État de planification conservé dans `plan.md` et les rapports de phase ; le snapshot machine-readable antérieur, redondant et porteur de chemins locaux, a été retiré au nettoyage. Les phases 1–5 sont implémentées ; la phase 2 reste `in-progress` jusqu’au test Claude demandé par `CONTRIBUTING.md` ou à une exception explicite du mainteneur. Les autres chemins restent des projections dans les phase-n.md. La sonde de connectivité ne valide pas AC15 ; les preuves runtime concernent mémoire, règle, skills, agent, workflow et guidance hooks sur Linux. Les sorties Kilo de phase 2 publiées sont expurgées des identifiants externes. + +| AC | Critère officiel | Phases | Fichiers / responsabilité | Tests nécessaires | Preuves attendues | Risques / environnement | +| --- | --- | --- | --- | --- | --- | --- | +| 1 | A Kilo-only fixture is detected by onboarding and every applicable generator. | 1–5 | 00-onboard detection ; 02 memory tools ; refs detect/paths de 04–08 | Fixture Kilo-only exécutée par les skills livrés ; proposition de cible enregistrée pour chaque applicable | Transcript des sept capacités : onboard, mémoire, skill, règle, agent, commande, hook ; choix Kilo explicite | Codex fixture/génération puis Kilo ; bootstrap hors responsabilité | +| 2 | Detection covers .kilo/, root kilo.json[c], .kilo/kilo.json[c], and legacy .kilocode/. | 1–5 | Mêmes références ; fixtures detection | Six fixtures séparées : .kilo/, .kilocode/, kilo.json, kilo.jsonc, .kilo/kilo.json, .kilo/kilo.jsonc | Résultat positif par signal et par capacité applicable ; ne pas masquer le config imbriqué par un simple test de dossier | Portable ; aucun recours au resolver CLI dual-config | +| 3 | Legacy .kilocode/ detection still produces new artifacts under canonical .kilo/ paths. | 1–5 | Actions write de 04–08 ; 02 sync ; fixtures legacy | Legacy-only, génération effective de chaque artefact et recherche de nouvelles sorties | Nouveaux fichiers spécifiques dans .kilo seulement ; AGENTS.md mémoire racine ; aucun fichier ajouté dans .kilocode | Hooks guidance sans fichier ; chemins non Kilo sélectionnés restent leur cible | +| 4 | opencode.json[c] alone does not produce a false Kilo detection. | 1–5 | Références de détection ; context-kilo-detection.test.js | opencode.json seul puis opencode.jsonc seul, sans autre signal Kilo | Pas de proposition/détection Kilo ; OpenCode conserve son comportement propre | AGENTS.md seul aussi négatif ; compatibilité runtime != détection AIDD | +| 5 | AGENTS.md generation preserves user content and deduplicates a shared target. | 1 | 02 action sync et contrat de mémoire ; tools.md ; update_memory.js ; update-memory.test.js | Kilo seul et Kilo/Codex/OpenCode/Cursor, user bytes autour bloc, README opt-in, Claude non choisi | Un seul bloc AGENTS.md ; hashes des parties user identiques ; une destination ; relance inchangée | Claude @imports et Copilot ../ conservés ; refus marqueurs avant Upsert/Fill | +| 6 | Rule generation writes .kilo/rules//.md and wires the exact path through instructions. | 2 | 05 write-rule.cjs et kilo-config.cjs ; refs/tool-paths | Filesystem réel, règle catégorisée et config sélectionnée ; positif/négatif loader | Chemin exact .kilo/rules/category/slug.md ajouté une fois ; corps réellement chargé ; absence sans instructions | Après alignement #979 ; dossier config-backed, aucune distribution flat de règle | +| 7 | Config selection respects Kilo's precedence and asks before choosing among ambiguous existing files. | 2 | 05 capture-rule et kilo-config.cjs ; fixtures kilo-config | 0, 1, plusieurs configs ; propriétaire unique ; aucun ou plusieurs owners ; annulation ; resolved config Kilo | Fichier retenu enregistré ; candidats par priorité vérifiée ; demande explicite en ambiguïté ; autres configs intacts | Priorité .kilo > root documentée ; tie JSON/JSONC à sonder avant ordre total ; CLI strict inchangé | +| 8 | JSON and JSONC updates preserve existing entries, duplicates, order, comments, trailing commas, and unrelated formatting. | 2 | kilo-config.cjs ; kilo-rule-publication.test.js | JSON/JSONC, entries répétées, commentaires, virgules finales, CRLF/tabs, chaînes ; vérification diff byte-ranges | Bytes hors insertion identiques ; ordre/doublons utilisateur conservés ; syntaxe valide après insertion | Fusion runtime peut dédupliquer, ne pas confondre avec conservation fichier ; JSON.parse global insuffisant | +| 9 | Skill placement requires an explicit choice before using .agents/skills/. | 3 | 04 scope-frame et tool-write ; actions scope/write/validate | Native-only, portable explicite, portable non choisi, double copie existante et cible partagée multi-outils | Une copie ; aucun .agents/skills implicite ; transcript accord portable ; name == parent | Assets Claude inchangés ; `.agents` loader-compatible ne vaut pas autorisation de placement | +| 10 | Agent paths and frontmatter match the documented Markdown-agent contract. | 4 | 06 tool-paths et agent-authoring ; capture/write/validate | Nom filename, description/mode, options demandées model/temperature/permission ; YAML parser et catalogue/debug Kilo | Validé Linux : frontmatter exact, sous-agent chargé et invoqué par workflow; aucune name Claude injectée | Claude name/description et TOML Codex contrôlés hors runtime ; macOS/Windows ouverts | +| 11 | Workflows are generated under .kilo/commands/ with supported frontmatter only. | 4 | 07 tool-paths et command-authoring ; capture/write/validate | Workflow avec chaque option puis combinaison ; champs Claude absents ; command invocation réelle | Validé Linux : `.kilo/commands/verify-workflow.md`, sous-agent et payload exact | Pas de nested location Kilo supposé ; commande Codex unsupported reste skip explicite; macOS/Windows ouverts | +| 12 | Hook generation returns precise plugin guidance and never emits an unsupported declarative hook. | 5 | 08 SKILL et actions ; hook-authoring/tool-paths ; context-hook-generation.test.js | session.created, événement non documenté, Kilo-only et mixed targets ; arbre complet avant/après ; mutations ciblées de chaque garde source | Guidance sourcée, limites précises ; zéro hook déclaratif/JS générique émis pour Kilo ; pas de mapping inventé ; assertions tuent les 7 régressions ciblées | Bridge #971 gardé ; ses événements élargis ne deviennent pas nouveau scope de génération | +| 13 | Re-running every generator is idempotent. | 1–6 | Toutes actions write/sync ; writer ; fixtures/artifact tests ; hook generation | Même demande et mêmes choix exécutés deux fois pour mémoire/règle/skill/agent/commande/hook ; sorties non Kilo ; mutation du contrat de second passage | Arbre, bytes et mtime stables ; instructions/blocs/hooks AIDD non dupliqués ; corpus user intact ; garde du second passage tuée par mutation | Répétition LLM doit réutiliser artefact actuel, pas réécrire différemment à intention identique | +| 14 | Invalid or unsafe configuration causes no partial write. | 1–5 | Préflight mémoire et actions multi-cibles ; writer transaction ; tests refus | Config invalide, type instructions erroné, keys dupliquées, collision, path unsafe, erreur dernière cible, panne staging/rename ; mutation de l’abandon du dernier target | Snapshot projet entier identique après refus/erreur testée ; aucun fichier/dir artefact ou temporaire nouveau ; assertion de préflight tuée par mutation | Garanties documentées ; ne pas cocher avec atomic rename par fichier uniquement ; aucune promesse crash-machine générale | +| 15 | Runtime-oriented tests prove Kilo discovers each emitted artifact on supported platforms. | 1–6 | Runner skill-eval ; kilo-native-generation.e2e ; helpers runtime et CI existante | Vrais skills génèrent puis vrai Kilo découvre/charge/utilise mémoire/règle/skill/agent/workflow ; hook guidance absence ; négatifs | Catalogues/config, payloads, tool traces, résultat tâche, OS/version/hash et cleanup ; modèle gratuit usage réel | Linux 7.8.8 faisable ; Windows/macOS restent à exécuter ; inférence locale complément, pas seule preuve d’usage | +| 16 | Every Kilo path claim carries an official source and the 2026-09-25 verification date. | 1–5 | Chaque référence outil modifiée ; README pointeur ; official-sources.md | Garde source/date par assertion chemin Kilo + revue des affirmations contre docs et ticket | URL officielle et Verified on 2026-09-25 attribué au ticket ; reverified 2026-10-10 séparé ; divergences explicites | Ne pas antidater notre observation ; refs autonomes sans liens inter-skills | + +## Lecture de l’acceptation + +Aucun AC officiellement impossible à cause du seul manque de compte Anthropic : AC15 vise le runtime Kilo. En revanche, la non-régression comportementale dans un Claude authentifié demandée par CONTRIBUTING reste impossible ici. OpenCode runtime et Kilo sur les autres OS nécessitent un environnement existant/CI, sinon non validés. L’alignement #979 conditionne la phase 2 et l’acceptation finale, pas la qualité des artefacts de planification. + +La stratégie complète précise fixtures/oracles, limites et commandes dans [test-strategy.md](./test-strategy.md). La projection pour chaque ligne est répartie dans [plan.md](./plan.md) et ses six phases. Rapport final d’implémentation : une preuve nommée par AC et par surface ; ne jamais remplacer un skip par pass. + +## Résultat acquis au 12 octobre 2026 + +| AC | Statut réel | Preuve et reste à faire | +| --- | --- | --- | +| 1 | Partiel | Onboard/mémoire et skill détectent Kilo ; sync et génération skill réellement exécutés par Codex natif. Onboarding autonome et générateurs 05–08 futurs. | +| 2 | Partiel | Six signaux dans trois contrats, tests phase 1 + oracle fixtures phase 3 ; autres générateurs futurs. | +| 3 | Partiel | Mémoire racine AGENTS et fixture skill legacy générée sous .kilo ; autres artefacts futurs. | +| 4 | Partiel | Configs OpenCode seules et AGENTS seul négatifs dans ces trois contrats ; autres générateurs futurs. | +| 5 | Validé | Preuves phase 1 inchangées : 40 tests mémoire, contenu utilisateur et bloc unique, rerun bytes/mtime stable. | +| 6 | Validé pour la fixture Linux | Writer #979 étendu : règle `.kilo/rules` et chemin exact publiés, tests d'intégration du writer réussis ; Kilo 7.8.8 a répondu avec le marqueur du corps, puis sans celui-ci après retrait de `instructions` ([rapport runtime et sorties expurgées](evidence/phase-2/runtime-report.md)). Autres plateformes non vérifiées. | +| 7 | Partiel | Quatre configurations inspectées ; choix de propriétaire unique ou choix explicite en ambiguïté, refus de suppression à plusieurs propriétaires testé. La sonde Kilo 7.8.8 mesure un ordre de fusion pour sa fixture à quatre configurations ; cela ne valide pas tous les cas de priorité/sélection. | +| 8 | Validé par tests de fichier | Insertions JSONC et publication préservent octets non concernés, commentaires, ordre, doublons et virgules finales ; relance identique. Cela ne prouve pas la fusion ni le chargement runtime Kilo. | +| 9 | Validé | Contrat de choix explicite, caller natif/portable/shared et refus sans accord ou copies/conflits non résolus ; une copie, runtime Kilo natif et portable réussi. | +| 10–11 | Validé | Preuves phase 4 inchangées : agent/workflow Kilo exécutés sur Linux ; autres OS restent ouverts. | +| 12 | Validé pour la phase 5 | Caller hook, fixtures mixed/invalid, et 7/7 mutations ciblées tuées par les assertions ([rapport phase 5](phase-5-report.md), [log mutations](evidence/phase-5/source-contract-mutations.log), [balayage final](evidence/phase-5/source-contract-final-sweep.log)). | +| 13 | Partiel | Mémoire, skills, règles et hooks idempotents dans leurs preuves de phase ; règles : octets et `mtime` stables pour source, sortie native et configuration. Autres générateurs futurs. | +| 14 | Partiel | Préflight mémoire phase 1, refus caller skills phase 3, refus hook mixed ; writer règles : ambiguïté, doublons, disparition/modification de config, écriture temporaire partielle et rollback testés avec snapshots incluant répertoires. Courses non détectables entre contrôle et appel système, arrêt brutal et échec de restauration restent hors garantie. | +| 15 | Partiel | Linux/Kilo 7.8.8 : mémoire, règle corrigée, skills, agent et workflow ont des preuves runtime ; la phase 6 vérifie aussi le skill généré par Codex jusqu’à la lecture du payload, avec contrôles positif/négatif et provider local déterministe. macOS/Windows ne sont pas testés ; AC15 reste partiel. Aucune validation Claude authentifiée n’est revendiquée. | +| 16 | Partiel | Quatre références modifiées portent sources/date contractuelle attribuée à #914 et revalidation distincte ; autres références futures. | + +Preuves et limites : [rapport phase 1](phase-1-report.md), [runtime mémoire](phase-1-runtime.md), [rapport phase 3](phase-3-report.md), [runtime skills](phase-3-runtime.md). Les revues détaillées des phases 1 et 3 ont été retirées après synthèse dans les rapports. Aucun critère transversal déclaré entièrement satisfait. diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/environment-evidence.md b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/environment-evidence.md new file mode 100644 index 000000000..24c22c451 --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/environment-evidence.md @@ -0,0 +1,24 @@ +# Preuves de faisabilité de cette session + +Ces observations ne valident aucun artefact #914 encore non implémenté. + +| Vérification | Résultat observé | +| --- | --- | +| git status --short --branch | feat/kilo-native-generation-914 ; uniquement dossier de tâche non suivi | +| git rev-parse HEAD upstream/next | deux fois 41e91691b5837a8c27475d0115725131bb53bf41 | +| merge-base --is-ancestor 12777d03808cee29f611e18d3816ae62ebc5c969 HEAD | code 0 | +| kilo --help hors sandbox | CLI 7.8.8 ; commandes run/auth/models/debug disponibles | +| kilo auth list | 0 credentials ; aucun login déclenché | +| kilo models kilo --verbose | modèle cohere/north-mini-code:free : isFree=true, cost input/output/cache=0, toolcall=true | +| claude --version | 2.1.296 ; aucun modèle Claude lancé | +| command -v opencode | aucune sortie ; absent du PATH | + +Sonde exécutée dans /tmp/aidd-914-kilo-probe vide, hors sandbox après élévation ; aucune instruction contenant le code du projet envoyée. Commande : + +```sh +timeout 40s kilo run --pure --dir /tmp/aidd-914-kilo-probe --model kilo/cohere/north-mini-code:free --format json 'Reply exactly KILO_FREE_ACCESS_OK. Do not call tools or read any files.' +``` + +Code 0. Sortie text : `KILO_FREE_ACCESS_OK`. step_finish : reason stop, cost 0. Identifiants de session et d'exécution non conservés. Pas d'événement tool dans la sortie reçue. Accès gratuit effectif confirmé ; authentification de compte non établie. Gratuité/quotas à revérifier à chaque campagne ; aucune substitution vers un modèle payant, même si le gratuit est indisponible. + +Le sandbox initial donne EROFS sur ~/.local/state/kilo et ~/.local/share/kilo/state. Ce n'est pas un échec d'authentification. Le binaire produit son état/cache hors du projet lors du diagnostic ; aucune modification intentionnelle de configuration globale ou credential. Aucun achat ni abonnement. diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-2/runtime-corrections-negative-config.jsonc b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-2/runtime-corrections-negative-config.jsonc new file mode 100644 index 000000000..571a15bec --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-2/runtime-corrections-negative-config.jsonc @@ -0,0 +1,3 @@ +{ + "$schema": "https://app.kilo.ai/config.json" +} diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-2/runtime-corrections-positive-config.jsonc b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-2/runtime-corrections-positive-config.jsonc new file mode 100644 index 000000000..51c6a471d --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-2/runtime-corrections-positive-config.jsonc @@ -0,0 +1,6 @@ +{ + "$schema": "https://app.kilo.ai/config.json", + "instructions": [ + ".kilo/rules/01-standards/1-naming.md" + ] +} diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-2/runtime-corrections-rule.md b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-2/runtime-corrections-rule.md new file mode 100644 index 000000000..e668279df --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-2/runtime-corrections-rule.md @@ -0,0 +1,5 @@ +# Naming + +The exact answer to the verification question is COBALT_914. + + diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-2/runtime-negative-redacted.jsonl b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-2/runtime-negative-redacted.jsonl new file mode 100644 index 000000000..135fb7eaf --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-2/runtime-negative-redacted.jsonl @@ -0,0 +1,2 @@ +{"type":"text","text":"I don't have access to any verification question context. Could you provide the verification question or specify what token/answer you're expecting?"} +{"type":"step_finish","reason":"stop","exitCode":0,"cost":0} diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-2/runtime-positive-redacted.jsonl b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-2/runtime-positive-redacted.jsonl new file mode 100644 index 000000000..3fd166bbe --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-2/runtime-positive-redacted.jsonl @@ -0,0 +1,2 @@ +{"type":"text","text":"COBALT_914"} +{"type":"step_finish","reason":"stop","exitCode":0,"cost":0} diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-2/runtime-report.md b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-2/runtime-report.md new file mode 100644 index 000000000..fc7c94e2e --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-2/runtime-report.md @@ -0,0 +1,51 @@ +# Observation runtime de la phase 2 + +Le 12 octobre 2026, Kilo CLI 7.8.8 a été exécuté sous Linux dans un projet temporaire isolé. Avec la règle `.kilo/rules/01-standards/1-naming.md` et son chemin exact dans `instructions`, la réponse contenait `COBALT_914`. Pour le contrôle négatif, le même fichier règle est resté présent ; seule la propriété `instructions` a été retirée et une nouvelle session a été lancée. La réponse négative demandait le contexte et ne contenait pas le marqueur. Les deux processus ont terminé avec le code 0 et le coût rapporté était 0. + +Cette observation vérifie le chargement du corps de cette règle dans cette configuration et cette version de Kilo. Elle ne provient pas d’un test unitaire, ne couvre pas toutes les versions/modèles/plateformes et ne prouve rien sur Claude Code. Les sorties publiées sont expurgées des identifiants de session, message, requête et fournisseur ; les captures brutes ont été conservées hors de l’arbre publiable sans modification. + +## Entrées conservées + +- [Configuration positive](runtime-corrections-positive-config.jsonc) +- [Configuration négative](runtime-corrections-negative-config.jsonc) +- [Requête exacte du writer](runtime-request.json) +- [Règle de la fixture](runtime-corrections-rule.md) +- [Sortie positive expurgée](runtime-positive-redacted.jsonl) +- [Sortie négative expurgée](runtime-negative-redacted.jsonl) + +La configuration positive référence le chemin exact de la règle ; la négative conserve `$schema` et omet uniquement `instructions`. La règle contient le marqueur de vérification `COBALT_914`. Le journal brut n’est pas un artefact de publication. + +## Reproduction + +Avec Kilo 7.8.8 installé, définir les chemins de fixture et résoudre `$WRITER` depuis le skill installé : + +```sh +EVIDENCE=/path/to/phase-2/evidence +FIXTURE_POSITIVE="$(mktemp -d)" +FIXTURE_NEGATIVE="$(mktemp -d)" +REQUEST="$EVIDENCE/runtime-request.json" +POSITIVE_CONFIG="$EVIDENCE/runtime-corrections-positive-config.jsonc" +FIXTURE_NEGATIVE_CONFIG="$EVIDENCE/runtime-corrections-negative-config.jsonc" +# WRITER = chemin absolu de scripts/write-rule.cjs dans le skill installé +node "$WRITER" --project "$FIXTURE_POSITIVE" --tools kilo --input "$REQUEST" +mkdir -p "$FIXTURE_NEGATIVE/.kilo" +cp "$POSITIVE_CONFIG" "$FIXTURE_POSITIVE/.kilo/kilo.jsonc" +cp -a "$FIXTURE_POSITIVE/.kilo/rules" "$FIXTURE_NEGATIVE/.kilo/" +cp "$FIXTURE_NEGATIVE_CONFIG" "$FIXTURE_NEGATIVE/.kilo/kilo.jsonc" +``` + +La requête exacte est conservée dans [runtime-request.json](runtime-request.json). `$FIXTURE_NEGATIVE_CONFIG` désigne la configuration négative archivée ; la positive est celle produite par le writer. Avec XDG_CONFIG_HOME, XDG_DATA_HOME, XDG_STATE_HOME et XDG_CACHE_HOME isolés différemment par session, lancer : + +```sh +kilo --version +env XDG_CONFIG_HOME="$(mktemp -d)" XDG_DATA_HOME="$(mktemp -d)" XDG_STATE_HOME="$(mktemp -d)" XDG_CACHE_HOME="$(mktemp -d)" \ + kilo run --pure --dir "$FIXTURE_POSITIVE" --model kilo/cohere/north-mini-code:free --format json \ + 'What is the exact answer to the verification question? Answer only that token.' +env XDG_CONFIG_HOME="$(mktemp -d)" XDG_DATA_HOME="$(mktemp -d)" XDG_STATE_HOME="$(mktemp -d)" XDG_CACHE_HOME="$(mktemp -d)" \ + kilo run --pure --dir "$FIXTURE_NEGATIVE" --model kilo/cohere/north-mini-code:free --format json \ + 'What is the exact answer to the verification question? Answer only that token.' +``` + +Les commandes d’exécution utilisent des chemins symboliques locaux ; aucun chemin temporaire personnel n’est requis. La disponibilité du service et du modèle peut changer. + +Une tentative initiale sans accès réseau a expiré sur l’appel de service Kilo. Les exécutions rapportées ont ensuite réussi avec accès réseau disponible ; une nouvelle exécution dépend de l’accès au service et de la disponibilité du modèle nommé. diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-2/runtime-request.json b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-2/runtime-request.json new file mode 100644 index 000000000..be8ae47a7 --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-2/runtime-request.json @@ -0,0 +1,7 @@ +{ + "category": "01-standards", + "slug": "1-naming", + "description": "Naming", + "paths": [], + "body": "# Naming\n\nThe exact answer to the verification question is COBALT_914.\n" +} diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-4/README.md b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-4/README.md new file mode 100644 index 000000000..4b4c6a0c3 --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-4/README.md @@ -0,0 +1,18 @@ +# Phase 4 evidence inventory + +- `caller-receipt.json`, `generation-validation.json`, `generated-artifact-hashes.txt`, `source-contract-hashes.txt`: synthetic caller inputs, structural validation and provenance. +- `generated-project/`: exact generated Kilo agent, workflow and payload fixture. +- `replay-kilo.sh`, `extract-free-model.cjs`, `validate-generation.cjs`: isolated replay, compact free-model catalog extraction and fixture validation. +- `attempt-04-outside-sandbox/`: retained successful workflow JSONL and exit code, selected free-model catalog entry, and artifact hashes before and after Kilo consumed the fixture. This is historical evidence from the original authorized run. + +Two earlier sandbox runs were historically reported as DNS timeouts. Their detailed logs were removed; they are not replay evidence. Historical sanitized session exports were also removed because their redaction markers retained technical session/message/part identifiers. No separate external copy of original export bytes or their SHA-256 hashes was found during the audit. The local B1 backup is described in the phase 1 report as pre-cleanup modified working state, not a distinct set of raw original exports; the B1 backup itself was not independently available for verification in this repository. No successful trace was reconstructed from the historical exports; the original successful attempt 04 JSONL remains separately archived. + +The replay script masks the current `$HOME`, runtime project and XDG paths in captured output using literal path replacement, so shell-special characters in those paths are safe. Temporary directories use `$TMPDIR` when set and `/tmp` otherwise. This removes a fixed home-directory assumption; it does not establish macOS or Windows compatibility. The updated script has not been rerun. The historical attempt 04 output predates that script edit. + +For a future replay, use a fresh attempt name, for example: + +```sh +bash aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-4/replay-kilo.sh attempt-post-cleanup +``` + +The script requires Bash, Git metadata, Node, `sha256sum`, `mktemp`, `sed` (for session selection), an available Kilo CLI with working network/model access, and a catalog entry for the explicitly selected free model. It writes output into the named directory. This command was not run during the cleanup. diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-4/attempt-04-outside-sandbox/artifact-hashes-consumed-after.txt b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-4/attempt-04-outside-sandbox/artifact-hashes-consumed-after.txt new file mode 100644 index 000000000..ce42e3d99 --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-4/attempt-04-outside-sandbox/artifact-hashes-consumed-after.txt @@ -0,0 +1,3 @@ +83fe54db94a80c71930aedf756f17881b42ebda02ef481a0668bb7b1f9bde47e .kilo/agents/verify-agent.md +55630dd7c27452fb05892541f2cf1c2f0748853417b1bbe00c5e4345243bad6e .kilo/agents/assets/agent-payload.txt +9daf5e2a3a5e211daf1dc42b3ffebff1518eff543ffab89375df3abc6707218f .kilo/commands/verify-workflow.md diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-4/attempt-04-outside-sandbox/artifact-hashes-consumed-before.txt b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-4/attempt-04-outside-sandbox/artifact-hashes-consumed-before.txt new file mode 100644 index 000000000..ce42e3d99 --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-4/attempt-04-outside-sandbox/artifact-hashes-consumed-before.txt @@ -0,0 +1,3 @@ +83fe54db94a80c71930aedf756f17881b42ebda02ef481a0668bb7b1f9bde47e .kilo/agents/verify-agent.md +55630dd7c27452fb05892541f2cf1c2f0748853417b1bbe00c5e4345243bad6e .kilo/agents/assets/agent-payload.txt +9daf5e2a3a5e211daf1dc42b3ffebff1518eff543ffab89375df3abc6707218f .kilo/commands/verify-workflow.md diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-4/attempt-04-outside-sandbox/selected-free-model.stdout.txt b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-4/attempt-04-outside-sandbox/selected-free-model.stdout.txt new file mode 100644 index 000000000..a12806112 --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-4/attempt-04-outside-sandbox/selected-free-model.stdout.txt @@ -0,0 +1,66 @@ +kilo/cohere/north-mini-code:free +{ + "id": "cohere/north-mini-code:free", + "providerID": "kilo", + "name": "Cohere: North Mini Code (free)", + "api": { + "id": "cohere/north-mini-code:free", + "url": "https://api.kilo.ai/api/openrouter/", + "npm": "@kilocode/kilo-gateway" + }, + "status": "active", + "headers": {}, + "options": { + "description": "North Mini Code is Cohere's first agentic coding model and the debut of its North family. A sparse mixture-of-experts model with 30B total parameters and 3B active, it is optimized..." + }, + "cost": { + "input": 0, + "output": 0, + "cache": { + "read": 0, + "write": 0 + } + }, + "limit": { + "context": 256000, + "output": 64000 + }, + "capabilities": { + "temperature": true, + "reasoning": true, + "attachment": false, + "toolcall": true, + "input": { + "text": true, + "audio": false, + "image": false, + "video": false, + "pdf": false + }, + "output": { + "text": true, + "audio": false, + "image": false, + "video": false, + "pdf": false + }, + "interleaved": false + }, + "release_date": "2026-10-10", + "variants": { + "instant": { + "reasoning": { + "enabled": false, + "effort": "none" + } + }, + "thinking": { + "reasoning": { + "enabled": true, + "effort": "high" + } + } + }, + "isFree": true, + "mayTrainOnYourPrompts": true +} diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-4/attempt-04-outside-sandbox/workflow-run.exit.txt b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-4/attempt-04-outside-sandbox/workflow-run.exit.txt new file mode 100644 index 000000000..573541ac9 --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-4/attempt-04-outside-sandbox/workflow-run.exit.txt @@ -0,0 +1 @@ +0 diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-4/attempt-04-outside-sandbox/workflow-run.redacted.jsonl b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-4/attempt-04-outside-sandbox/workflow-run.redacted.jsonl new file mode 100644 index 000000000..8508b802e --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-4/attempt-04-outside-sandbox/workflow-run.redacted.jsonl @@ -0,0 +1,10 @@ +{"type":"tool_use","timestamp":1791646240656,"part":{"type":"tool","tool":"task","state":{"status":"completed","input":{"prompt":"Invoke `verify-agent` as a subtask. Ask it to read `.kilo/agents/assets/agent-payload.txt` and return only its exact content. Return only the subtask result.\n\n\"Run verify-workflow. Return only the exact payload obtained by its subagent. Do not modify files.\"","description":"Verify the phase four fixture by invoking its subagent.","subagent_type":"verify-agent","command":"verify-workflow"},"output":"\n\nAGENT_APPLIED:willow-5836\n\n","title":"Verify the phase four fixture by invoking its subagent.","metadata":{"model":{},"truncated":false},"time":{"start":1791646229332,"end":1791646240644}}}} +{"type":"step_start","timestamp":1791646241433,"part":{"type":"step-start"}} +{"type":"tool_use","timestamp":1791646253359,"part":{"type":"tool","tool":"task","state":{"status":"completed","input":{"command":"verify-workflow","description":"Verify the phase four fixture by invoking its subagent to read the payload file.","prompt":"Invoke `verify-agent` as a subtask. Ask it to read `.kilo/agents/assets/agent-payload.txt` and return only its exact content. Do not modify files. Return only the subtask result.","subagent_type":"verify-agent"},"output":"\n\nAGENT_APPLIED:willow-5836\n\n","title":"Verify the phase four fixture by invoking its subagent to read the payload file.","metadata":{"model":{},"truncated":false,"approval":{"source":"agent","agent":"code","rule":{"permission":"*","pattern":"*","action":"allow"}}},"time":{"start":1791646245380,"end":1791646253353}},"metadata":{"openrouter":{}}}} +{"type":"step_finish","timestamp":1791646253388,"part":{"type":"step-finish","reason":"tool-calls","metrics":{"generation":31.60006687844842,"source":"computed"},"time":{"start":1791646241417,"end":1791646253379,"elapsed":11962},"cost":0,"tokens":{"total":14384,"input":14006,"output":33,"cache":{"read":0,"write":0}}}} +{"type":"step_start","timestamp":1791646254111,"part":{"type":"step-start"}} +{"type":"tool_use","timestamp":1791646256521,"part":{"type":"tool","tool":"read","state":{"status":"completed","input":{"filePath":"$RUNTIME_PROJECT/.kilo/agents/assets/agent-payload.txt"},"output":"$RUNTIME_PROJECT/.kilo/agents/assets/agent-payload.txt\nfile\n\n1: AGENT_APPLIED:willow-5836\n\n(End of file - total 1 lines)\n","title":"tmp/aidd-914-phase-4-runtime.ZO9Tw6/.kilo/agents/assets/agent-payload.txt","metadata":{"preview":"AGENT_APPLIED:willow-5836","truncated":false,"loaded":[],"display":{"type":"file","path":"$RUNTIME_PROJECT/.kilo/agents/assets/agent-payload.txt","text":"AGENT_APPLIED:willow-5836","lineStart":1,"lineEnd":1,"totalLines":1,"truncated":false},"approval":{"source":"agent","agent":"code","rule":{"permission":"read","pattern":"*","action":"allow"}}},"time":{"start":1791646256472,"end":1791646256515}},"metadata":{"openrouter":{}}}} +{"type":"step_finish","timestamp":1791646256543,"part":{"type":"step-finish","reason":"tool-calls","metrics":{"generation":62.21672847136382,"source":"computed"},"time":{"start":1791646254097,"end":1791646256524,"elapsed":2427},"cost":0,"tokens":{"total":14205,"input":14054,"output":10,"cache":{"read":0,"write":0}}}} +{"type":"step_start","timestamp":1791646257687,"part":{"type":"step-start"}} +{"type":"text","timestamp":1791646260447,"part":{"type":"text","text":"The verification is complete. The `.kilo/agents/assets/agent-payload.txt` file contains:\n\n```\nAGENT_APPLIED:willow-5836\n```\n\nThe verify-agent successfully read the file and returned its exact content as required.","time":{"start":1791646259269,"end":1791646260440}}} +{"type":"step_finish","timestamp":1791646260488,"part":{"type":"step-finish","reason":"stop","metrics":{"generation":68.01007556675063,"source":"computed"},"time":{"start":1791646257672,"end":1791646260451,"elapsed":2779},"cost":0,"tokens":{"total":14310,"input":14121,"output":31,"cache":{"read":0,"write":0}}}} diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-4/caller-receipt.json b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-4/caller-receipt.json new file mode 100644 index 000000000..f1de3c164 --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-4/caller-receipt.json @@ -0,0 +1,56 @@ +{ + "kind": "new-replay", + "generated_at_utc": "2026-10-10T15:09:24.296Z", + "caller": "Codex principal applying aidd-context:06-agent-generate and aidd-context:07-command-generate", + "router_loaded": "installed aidd-context 2.8.1 SKILL.md; action routing only", + "contracts_applied": "phase-4 candidate actions and references at HEAD 19c81c4f", + "interactive_answers": false, + "fixture_inputs": { + "agent": { + "proposed_names": [ + "verifier", + "inspector", + "verify-agent" + ], + "selected_name": "verify-agent", + "purpose": "Read one bundled fixture payload and return its exact content.", + "mode": "Kilo host project", + "requested_options": { + "temperature": 0, + "permission": { + "read": "allow" + } + }, + "model": "not requested" + }, + "workflow": { + "name": "verify-workflow", + "goal": "Invoke verify-agent as a subtask and return its exact fixture payload.", + "location": "flat .kilo/commands/", + "arguments": "none", + "mode": "Kilo host project", + "requested_options": { + "agent": "verify-agent", + "variant": "minimal", + "subtask": true + } + } + }, + "fixture_note": "Names and choices are synthetic test inputs, not an interactive user response. Outputs were rendered by the current Codex caller by following the named capture, write, and validate actions.", + "generated_files": { + ".kilo/agents/verify-agent.md": { + "sha256": "83fe54db94a80c71930aedf756f17881b42ebda02ef481a0668bb7b1f9bde47e", + "bytes": 305 + }, + ".kilo/agents/assets/agent-payload.txt": { + "sha256": "55630dd7c27452fb05892541f2cf1c2f0748853417b1bbe00c5e4345243bad6e", + "bytes": 26 + }, + ".kilo/commands/verify-workflow.md": { + "sha256": "9daf5e2a3a5e211daf1dc42b3ffebff1518eff543ffab89375df3abc6707218f", + "bytes": 287 + } + }, + "source_contract_hashes": "source-contract-hashes.txt, produced by replay-kilo.sh from the candidate skill router, capture/write/validate actions, references, and templates", + "runtime_copy": "replay-kilo.sh copies generated-project into a fresh /tmp project and compares SHA-256 values before and after Kilo consumes it" +} diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-4/extract-free-model.cjs b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-4/extract-free-model.cjs new file mode 100644 index 000000000..05b8a6e3c --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-4/extract-free-model.cjs @@ -0,0 +1,47 @@ +const { readFileSync, writeFileSync } = require('node:fs'); + +const [inputPath, outputPath] = process.argv.slice(2); +const text = readFileSync(inputPath, 'utf8'); +const modelId = 'kilo/cohere/north-mini-code:free'; +const marker = `${modelId}\n`; +const start = text.indexOf(marker); + +if (start < 0) throw new Error(`Model catalog did not contain ${modelId}`); + +const jsonStart = start + marker.length; +let depth = 0; +let inString = false; +let escaped = false; +let jsonEnd = -1; + +for (let index = jsonStart; index < text.length; index += 1) { + const character = text[index]; + if (inString) { + if (escaped) escaped = false; + else if (character === '\\') escaped = true; + else if (character === '"') inString = false; + continue; + } + if (character === '"') inString = true; + else if (character === '{') depth += 1; + else if (character === '}') { + depth -= 1; + if (depth === 0) { + jsonEnd = index + 1; + break; + } + } +} + +if (jsonEnd < 0) throw new Error('Selected model JSON was incomplete'); +const model = JSON.parse(text.slice(jsonStart, jsonEnd)); +const freeZeroCost = model.cost?.input === 0 + && model.cost?.output === 0 + && model.cost?.cache?.read === 0 + && model.cost?.cache?.write === 0; + +if (model.id !== 'cohere/north-mini-code:free' || !model.name.includes('(free)') || !freeZeroCost) { + throw new Error('Selected catalog entry is not the expected explicitly free, zero-cost model'); +} + +writeFileSync(outputPath, `${modelId}\n${JSON.stringify(model, null, 2)}\n`); diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-4/generated-artifact-hashes.txt b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-4/generated-artifact-hashes.txt new file mode 100644 index 000000000..ce42e3d99 --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-4/generated-artifact-hashes.txt @@ -0,0 +1,3 @@ +83fe54db94a80c71930aedf756f17881b42ebda02ef481a0668bb7b1f9bde47e .kilo/agents/verify-agent.md +55630dd7c27452fb05892541f2cf1c2f0748853417b1bbe00c5e4345243bad6e .kilo/agents/assets/agent-payload.txt +9daf5e2a3a5e211daf1dc42b3ffebff1518eff543ffab89375df3abc6707218f .kilo/commands/verify-workflow.md diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-4/generated-project/.kilo/agents/assets/agent-payload.txt b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-4/generated-project/.kilo/agents/assets/agent-payload.txt new file mode 100644 index 000000000..5c278faae --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-4/generated-project/.kilo/agents/assets/agent-payload.txt @@ -0,0 +1 @@ +AGENT_APPLIED:willow-5836 diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-4/generated-project/.kilo/agents/verify-agent.md b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-4/generated-project/.kilo/agents/verify-agent.md new file mode 100644 index 000000000..699725783 --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-4/generated-project/.kilo/agents/verify-agent.md @@ -0,0 +1,16 @@ +--- +description: Verify the phase four fixture payload and return its exact content. +mode: subagent +temperature: 0 +permission: + read: allow +--- + +# Role + +Verify the payload supplied by the phase four fixture. + +# Behavior + +1. Read `.kilo/agents/assets/agent-payload.txt`. +2. Return only its exact content. diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-4/generated-project/.kilo/commands/verify-workflow.md b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-4/generated-project/.kilo/commands/verify-workflow.md new file mode 100644 index 000000000..22268e890 --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-4/generated-project/.kilo/commands/verify-workflow.md @@ -0,0 +1,8 @@ +--- +description: Verify the phase four fixture by invoking its subagent. +agent: verify-agent +variant: minimal +subtask: true +--- + +Invoke `verify-agent` as a subtask. Ask it to read `.kilo/agents/assets/agent-payload.txt` and return only its exact content. Return only the subtask result. diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-4/generation-validation.json b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-4/generation-validation.json new file mode 100644 index 000000000..0c8ca1bb6 --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-4/generation-validation.json @@ -0,0 +1,18 @@ +{ + "verdict": "pass", + "validation": "YAML frontmatter, Kilo field allowlists, body shape, canonical paths, payload fixture", + "artifacts": { + ".kilo/agents/verify-agent.md": { + "sha256": "83fe54db94a80c71930aedf756f17881b42ebda02ef481a0668bb7b1f9bde47e", + "bytes": 305 + }, + ".kilo/agents/assets/agent-payload.txt": { + "sha256": "55630dd7c27452fb05892541f2cf1c2f0748853417b1bbe00c5e4345243bad6e", + "bytes": 26 + }, + ".kilo/commands/verify-workflow.md": { + "sha256": "9daf5e2a3a5e211daf1dc42b3ffebff1518eff543ffab89375df3abc6707218f", + "bytes": 287 + } + } +} diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-4/replay-kilo.sh b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-4/replay-kilo.sh new file mode 100644 index 000000000..802995307 --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-4/replay-kilo.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +set -u +: "${HOME:?HOME must be set for path redaction}" + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +attempt_name="${1:-attempt-latest}" +result_dir="$script_dir/$attempt_name" +mkdir -p "$result_dir" +repo_root="$(git -C "$script_dir" rev-parse --show-toplevel)" +project_source="$script_dir/generated-project" +runtime_root="$(mktemp -d "${TMPDIR:-/tmp}/aidd-914-phase-4-runtime.XXXXXX")" +xdg_root="$(mktemp -d "${TMPDIR:-/tmp}/aidd-914-phase-4-xdg.XXXXXX")" +mkdir -p "$xdg_root/config" "$xdg_root/data" "$xdg_root/state" "$xdg_root/cache" + +cleanup() { + rm -rf -- "$runtime_root" "$xdg_root" +} +trap cleanup EXIT + +scrub_stderr() { + node -e 'const fs=require("node:fs"); let text=fs.readFileSync(process.argv[1],"utf8"); for(const [from,to] of [[process.argv[3],"$RUNTIME_PROJECT"],[process.argv[4],"$XDG_ROOT"],[process.env.HOME,"$USER_HOME"]]) if(from) text=text.split(from).join(to); text=text.replace(/run_id=[0-9a-fA-F-]+/gu,"run_id=[redacted]").replace(/ses_[A-Za-z0-9]+/gu,"[redacted-session]"); fs.writeFileSync(process.argv[2],text);' "$1" "$2" "$runtime_root" "$xdg_root" +} + +export XDG_CONFIG_HOME="$xdg_root/config" +export XDG_DATA_HOME="$xdg_root/data" +export XDG_STATE_HOME="$xdg_root/state" +export XDG_CACHE_HOME="$xdg_root/cache" + +cp -R "$project_source/." "$runtime_root/" +cd "$runtime_root" || exit 90 + +kilo --version > "$result_dir/kilo-version.txt" 2> "$result_dir/kilo-version.stderr.txt" + printf '%s\n' "$?" > "$result_dir/kilo-version.exit.txt" +( + cd "$repo_root" || exit 91 + sha256sum \ + plugins/aidd-context/skills/06-agent-generate/SKILL.md \ + plugins/aidd-context/skills/06-agent-generate/actions/01-capture-agent.md \ + plugins/aidd-context/skills/06-agent-generate/actions/02-write-agent.md \ + plugins/aidd-context/skills/06-agent-generate/actions/03-validate.md \ + plugins/aidd-context/skills/06-agent-generate/references/tool-paths.md \ + plugins/aidd-context/skills/06-agent-generate/references/agent-authoring.md \ + plugins/aidd-context/skills/06-agent-generate/assets/agent-template.md \ + plugins/aidd-context/skills/07-command-generate/SKILL.md \ + plugins/aidd-context/skills/07-command-generate/actions/01-capture-command.md \ + plugins/aidd-context/skills/07-command-generate/actions/02-write-command.md \ + plugins/aidd-context/skills/07-command-generate/actions/03-validate.md \ + plugins/aidd-context/skills/07-command-generate/references/tool-paths.md \ + plugins/aidd-context/skills/07-command-generate/references/command-authoring.md \ + plugins/aidd-context/skills/07-command-generate/assets/command-template.md +) > "$result_dir/source-contract-hashes.txt" +( + cd "$project_source" || exit 92 + sha256sum .kilo/agents/verify-agent.md .kilo/agents/assets/agent-payload.txt .kilo/commands/verify-workflow.md +) > "$result_dir/generated-artifact-hashes.txt" +sha256sum .kilo/agents/verify-agent.md .kilo/agents/assets/agent-payload.txt .kilo/commands/verify-workflow.md > "$result_dir/artifact-hashes-consumed-before.txt" + +kilo models kilo --verbose --pure > "$runtime_root/model-catalog.raw.txt" 2> "$runtime_root/model-catalog.raw.stderr.txt" +printf '%s\n' "$?" > "$result_dir/model-catalog.exit.txt" +scrub_stderr "$runtime_root/model-catalog.raw.stderr.txt" "$result_dir/model-catalog.stderr.txt" +node "$script_dir/extract-free-model.cjs" "$runtime_root/model-catalog.raw.txt" "$result_dir/selected-free-model.stdout.txt" + +kilo debug agent verify-agent --pure --print-logs > "$runtime_root/agent-debug.raw.json" 2> "$runtime_root/agent-debug.raw.stderr.txt" +printf '%s\n' "$?" > "$result_dir/agent-debug.exit.txt" +node -e 'const fs=require("node:fs"); const value=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); const scrub=(x)=>{if(typeof x==="string")return x.replaceAll(process.argv[3],"$RUNTIME_PROJECT").replaceAll(process.argv[4],"$XDG_ROOT").replaceAll(process.env.HOME,"$USER_HOME"); if(Array.isArray(x))return x.map(scrub); if(x&&typeof x==="object")return Object.fromEntries(Object.entries(x).map(([k,v])=>[k,scrub(v)])); return x;}; fs.writeFileSync(process.argv[2],JSON.stringify(scrub(value),null,2)+"\n");' "$runtime_root/agent-debug.raw.json" "$result_dir/agent-debug.stdout.json" "$runtime_root" "$xdg_root" +scrub_stderr "$runtime_root/agent-debug.raw.stderr.txt" "$result_dir/agent-debug.stderr.txt" + +kilo run --pure --command verify-workflow --model kilo/cohere/north-mini-code:free --format json \ + 'Run verify-workflow. Return only the exact payload obtained by its subagent. Do not modify files.' \ + > "$runtime_root/workflow-run.raw.jsonl" 2> "$runtime_root/workflow-run.raw.stderr.txt" +run_status=$? +printf '%s\n' "$run_status" > "$result_dir/workflow-run.exit.txt" + +if [ "$run_status" -eq 0 ]; then + session_ids="$(node -e 'const fs=require("node:fs"); const lines=fs.readFileSync(process.argv[1],"utf8").trim().split(/\n/u); const events=lines.map(JSON.parse); const main=events.find((e)=>e.sessionID)?.sessionID; const task=events.find((e)=>e.type==="tool_use"&&e.part?.tool==="task"&&e.part?.state?.status==="completed"); const child=task?.part?.state?.metadata?.sessionId; if(main) console.log(main); if(child) console.log(child);' "$runtime_root/workflow-run.raw.jsonl")" + main_session="$(printf '%s\n' "$session_ids" | sed -n '1p')" + child_session="$(printf '%s\n' "$session_ids" | sed -n '2p')" + if [ -n "$main_session" ]; then + kilo export --sanitize "$main_session" > "$runtime_root/main-session.raw.json" 2> "$runtime_root/main-session.raw.stderr.txt" + printf '%s\n' "$?" > "$result_dir/main-session-export.exit.txt" + node -e 'const fs=require("node:fs"); const value=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); const scrub=(x)=>{if(typeof x==="string")return x.replace(/(?:prt|msg|ses)_[A-Za-z0-9]+/gu,"[redacted-id]").replaceAll(process.argv[3],"$RUNTIME_PROJECT").replaceAll(process.argv[4],"$XDG_ROOT").replaceAll(process.env.HOME,"$USER_HOME"); if(Array.isArray(x))return x.map(scrub); if(x&&typeof x==="object")return Object.fromEntries(Object.entries(x).filter(([k])=>!/(^id$|id$|sessionid|callid)/iu.test(k)).map(([k,v])=>[k,scrub(v)])); return x;}; fs.writeFileSync(process.argv[2],JSON.stringify(scrub(value),null,2)+"\n");' "$runtime_root/main-session.raw.json" "$result_dir/main-session.sanitized.json" "$runtime_root" "$xdg_root" + scrub_stderr "$runtime_root/main-session.raw.stderr.txt" "$result_dir/main-session-export.stderr.txt" + fi + if [ -n "$child_session" ]; then + kilo export --sanitize "$child_session" > "$runtime_root/subagent-session.raw.json" 2> "$runtime_root/subagent-session.raw.stderr.txt" + printf '%s\n' "$?" > "$result_dir/subagent-session-export.exit.txt" + node -e 'const fs=require("node:fs"); const value=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); const scrub=(x)=>{if(typeof x==="string")return x.replace(/(?:prt|msg|ses)_[A-Za-z0-9]+/gu,"[redacted-id]").replaceAll(process.argv[3],"$RUNTIME_PROJECT").replaceAll(process.argv[4],"$XDG_ROOT").replaceAll(process.env.HOME,"$USER_HOME"); if(Array.isArray(x))return x.map(scrub); if(x&&typeof x==="object")return Object.fromEntries(Object.entries(x).filter(([k])=>!/(^id$|id$|sessionid|callid)/iu.test(k)).map(([k,v])=>[k,scrub(v)])); return x;}; fs.writeFileSync(process.argv[2],JSON.stringify(scrub(value),null,2)+"\n");' "$runtime_root/subagent-session.raw.json" "$result_dir/subagent-session.sanitized.json" "$runtime_root" "$xdg_root" + scrub_stderr "$runtime_root/subagent-session.raw.stderr.txt" "$result_dir/subagent-session-export.stderr.txt" + fi +fi + +node -e 'const fs=require("node:fs"); const raw=fs.readFileSync(process.argv[1],"utf8"); const scrub=(x)=>{if(typeof x==="string")return x.replace(/(]*>)/gu,"$1[redacted]$2").replace(/ses_[A-Za-z0-9]+/gu,"[redacted-session]").replaceAll(process.argv[3],"$RUNTIME_PROJECT").replaceAll(process.argv[4],"$XDG_ROOT").replaceAll(process.env.HOME,"$USER_HOME"); if(Array.isArray(x))return x.map(scrub); if(x&&typeof x==="object")return Object.fromEntries(Object.entries(x).filter(([k])=>!/(^id$|id$|sessionid|callid|reasoning)/iu.test(k)).map(([k,v])=>[k,scrub(v)])); return x;}; const out=raw.split(/\n/u).filter(Boolean).map((line)=>JSON.stringify(scrub(JSON.parse(line)))).join("\n")+"\n"; fs.writeFileSync(process.argv[2],out);' "$runtime_root/workflow-run.raw.jsonl" "$result_dir/workflow-run.redacted.jsonl" "$runtime_root" "$xdg_root" +scrub_stderr "$runtime_root/workflow-run.raw.stderr.txt" "$result_dir/workflow-run.stderr.txt" + +sha256sum .kilo/agents/verify-agent.md .kilo/agents/assets/agent-payload.txt .kilo/commands/verify-workflow.md > "$result_dir/artifact-hashes-consumed-after.txt" +printf 'runtime_exit=%s\n' "$run_status" +printf 'runtime_project=/tmp isolated; cleaned_on_exit=true\n' +exit "$run_status" diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-4/source-contract-hashes.txt b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-4/source-contract-hashes.txt new file mode 100644 index 000000000..e7a921394 --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-4/source-contract-hashes.txt @@ -0,0 +1,14 @@ +7a0d74a9fb363653afa5873f2df953686e675401acf7cb37d236418b7dc8b6b7 plugins/aidd-context/skills/06-agent-generate/SKILL.md +6cc02558c7c85bdee5cf7dec94d7e547a0fa9b6085d78386f2c84e2e737b5581 plugins/aidd-context/skills/06-agent-generate/actions/01-capture-agent.md +0a48a6c52480e8cbf704511625850284773207b61da99920c524a19275d8917b plugins/aidd-context/skills/06-agent-generate/actions/02-write-agent.md +e940e6eb61c4c48dfe52fc242833782d9bd4f0ed31315925cb0b7b96f2e01c10 plugins/aidd-context/skills/06-agent-generate/actions/03-validate.md +33b659accbd8db9291a61cffaf930962955a3591c019b3149688f19935ff3d84 plugins/aidd-context/skills/06-agent-generate/references/tool-paths.md +88eb8361a05e42ad3c6d2202dd50f13d8ba34297504ed5b20901685827e77942 plugins/aidd-context/skills/06-agent-generate/references/agent-authoring.md +c7091084a39eb2ad5e4b18e4119dec7a0b114fe10af4a8b9f6be407f8244c929 plugins/aidd-context/skills/06-agent-generate/assets/agent-template.md +aff0ba35ace65fd217db0821d81b8fd6f34d71614684e64459965b9125e58cec plugins/aidd-context/skills/07-command-generate/SKILL.md +57ec6be06ab03db0e350654edcf829cbc8d9f68ffadf20b26a67365be8b71496 plugins/aidd-context/skills/07-command-generate/actions/01-capture-command.md +8c2fec96fa66e9330e58418bd6b8bdff1b16e203ffe30d62569aaee455dfc05e plugins/aidd-context/skills/07-command-generate/actions/02-write-command.md +5eae9d42226d9808b130c8aeeb232077ee3621e873032c6a76bf68ec618b5d57 plugins/aidd-context/skills/07-command-generate/actions/03-validate.md +f9ca928d3f469e080bb478e88f0ed19e448ef2f80399ba7c3f2a141009d6231f plugins/aidd-context/skills/07-command-generate/references/tool-paths.md +c577e76a21d79f38668a7af9529f164fd1c5dca71abfbcd76ebbfdfc89b70f1b plugins/aidd-context/skills/07-command-generate/references/command-authoring.md +c14b2c3dfd4ec7a64788d58020cce55e43a68e55fc6c9af907ded13c66ab58e7 plugins/aidd-context/skills/07-command-generate/assets/command-template.md diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-4/validate-generation.cjs b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-4/validate-generation.cjs new file mode 100644 index 000000000..5c8da69c3 --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-4/validate-generation.cjs @@ -0,0 +1,92 @@ +const crypto = require('node:crypto'); +const fs = require('node:fs'); +const path = require('node:path'); +const yaml = require('js-yaml'); + +const evidenceDir = __dirname; +const projectDir = path.join(evidenceDir, 'generated-project'); +const relativeFiles = [ + '.kilo/agents/verify-agent.md', + '.kilo/agents/assets/agent-payload.txt', + '.kilo/commands/verify-workflow.md', +]; +const expectedPayload = 'AGENT_APPLIED:willow-5836\n'; +const read = (relative) => fs.readFileSync(path.join(projectDir, relative), 'utf8'); +const digest = (value) => crypto.createHash('sha256').update(value).digest('hex'); +const parseMarkdown = (relative) => { + const text = read(relative); + const match = text.match(/^---\n([\s\S]*?)\n---\n/u); + if (!match) throw new Error(`${relative}: missing YAML frontmatter`); + return { text, fields: yaml.load(match[1]) }; +}; + +const agent = parseMarkdown(relativeFiles[0]); +const command = parseMarkdown(relativeFiles[2]); +const agentKeys = Object.keys(agent.fields).sort(); +const commandKeys = Object.keys(command.fields).sort(); +if (JSON.stringify(agentKeys) !== JSON.stringify(['description', 'mode', 'permission', 'temperature'])) { + throw new Error(`Unexpected Kilo agent fields: ${agentKeys.join(', ')}`); +} +if (agent.fields.description !== 'Verify the phase four fixture payload and return its exact content.' || + agent.fields.mode !== 'subagent' || agent.fields.temperature !== 0 || + agent.fields.permission?.read !== 'allow' || /^---[\s\S]*?\nname:/u.test(agent.text)) { + throw new Error('Kilo agent fields do not match the captured fixture input'); +} +if (!agent.text.includes('# Role') || !agent.text.includes('# Behavior') || + !agent.text.includes('.kilo/agents/assets/agent-payload.txt')) { + throw new Error('Kilo agent body is incomplete'); +} +if (JSON.stringify(commandKeys) !== JSON.stringify(['agent', 'description', 'subtask', 'variant'])) { + throw new Error(`Unexpected Kilo workflow fields: ${commandKeys.join(', ')}`); +} +if (command.fields.description !== 'Verify the phase four fixture by invoking its subagent.' || + command.fields.agent !== 'verify-agent' || command.fields.variant !== 'minimal' || + command.fields.subtask !== true || /argument-hint|allowed-tools|disable-model-invocation/u.test(command.text) || + !command.text.includes('Invoke `verify-agent` as a subtask')) { + throw new Error('Kilo workflow fields or body do not match the captured fixture input'); +} +if (read(relativeFiles[1]) !== expectedPayload) throw new Error('Agent payload does not match the fixture oracle'); + +const artifacts = Object.fromEntries(relativeFiles.map((relative) => [relative, { + sha256: digest(fs.readFileSync(path.join(projectDir, relative))), + bytes: fs.statSync(path.join(projectDir, relative)).size, +}])); +const result = { + verdict: 'pass', + validation: 'YAML frontmatter, Kilo field allowlists, body shape, canonical paths, payload fixture', + artifacts, +}; +fs.writeFileSync(path.join(evidenceDir, 'generation-validation.json'), `${JSON.stringify(result, null, 2)}\n`); + +const receipt = { + kind: 'new-replay', + generated_at_utc: new Date().toISOString(), + caller: 'Codex principal applying aidd-context:06-agent-generate and aidd-context:07-command-generate', + router_loaded: 'installed aidd-context 2.8.1 SKILL.md; action routing only', + contracts_applied: 'phase-4 candidate actions and references at HEAD 19c81c4f', + interactive_answers: false, + fixture_inputs: { + agent: { + proposed_names: ['verifier', 'inspector', 'verify-agent'], + selected_name: 'verify-agent', + purpose: 'Read one bundled fixture payload and return its exact content.', + mode: 'Kilo host project', + requested_options: { temperature: 0, permission: { read: 'allow' } }, + model: 'not requested', + }, + workflow: { + name: 'verify-workflow', + goal: 'Invoke verify-agent as a subtask and return its exact fixture payload.', + location: 'flat .kilo/commands/', + arguments: 'none', + mode: 'Kilo host project', + requested_options: { agent: 'verify-agent', variant: 'minimal', subtask: true }, + }, + }, + fixture_note: 'Names and choices are synthetic test inputs, not an interactive user response. Outputs were rendered by the current Codex caller by following the named capture, write, and validate actions.', + generated_files: artifacts, + source_contract_hashes: 'source-contract-hashes.txt, produced by replay-kilo.sh from the candidate skill router, capture/write/validate actions, references, and templates', + runtime_copy: 'replay-kilo.sh copies generated-project into a fresh /tmp project and compares SHA-256 values before and after Kilo consumes it', +}; +fs.writeFileSync(path.join(evidenceDir, 'caller-receipt.json'), `${JSON.stringify(receipt, null, 2)}\n`); +process.stdout.write(`${JSON.stringify({ verdict: result.verdict, artifacts: Object.keys(artifacts).length })}\n`); diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-5/source-contract-final-sweep.log b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-5/source-contract-final-sweep.log new file mode 100644 index 000000000..10e95de53 --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-5/source-contract-final-sweep.log @@ -0,0 +1,12 @@ +✔ Kilo hooks end with sourced guidance and no generated artifacts (2.308718ms) +✔ mixed targets preflight before any script or config and preserve user hooks (2.107542ms) +✔ mixed hook contract fixtures retain user duplicates and one AIDD entry per target (3.701386ms) +✔ invalid final mixed target is detectable before creating any earlier target (0.688033ms) +ℹ tests 4 +ℹ suites 0 +ℹ pass 4 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 35.064541 diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-5/source-contract-mutations.log b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-5/source-contract-mutations.log new file mode 100644 index 000000000..63e02aa3c --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-5/source-contract-mutations.log @@ -0,0 +1,544 @@ +Guarded assertion command: node scripts/check-tests-leave-git-alone.js --watch .git/hooks -- node --test --test-isolation=none scripts/__tests__/context-hook-generation.test.js +The wrapper's automatic hooks-path lookup is unavailable in this sandbox; explicit read-only --watch .git/hooks succeeds and is used for each run. + +=== MUTATION documented_kilo_path === +File: plugins/aidd-context/skills/08-hook-generate/references/tool-paths.md +Expected red assertion: Kilo hooks end with sourced guidance and no generated artifacts +Change: Kilo documents JavaScript or TypeScript modules under `.kilo/plugin/` in a project. => Kilo documents JavaScript or TypeScript modules under `.kilo/plugin-mutant/` in a project. +✖ Kilo hooks end with sourced guidance and no generated artifacts (8.231481ms) +✔ mixed targets preflight before any script or config and preserve user hooks (1.049228ms) +✔ mixed hook contract fixtures retain user duplicates and one AIDD entry per target (1.855322ms) +✔ invalid final mixed target is detectable before creating any earlier target (0.327982ms) +ℹ tests 4 +ℹ suites 0 +ℹ pass 3 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 31.798075 + +✖ failing tests: + +test at scripts/__tests__/context-hook-generation.test.js:10:1 +✖ Kilo hooks end with sourced guidance and no generated artifacts (8.231481ms) + AssertionError [ERR_ASSERTION]: The input did not match the regular expression /\*\*Kilo guidance\.\*\* Kilo documents JavaScript or TypeScript modules under `\.kilo\/plugin\/` in a project\./u. Input: + + '# Tool paths (hooks)\n' + + '\n' + + 'Per-tool hook support, event names, file formats, and scopes. This reference covers the Kilo plugin boundary only for hook guidance; it does not generate plugins.\n' + + '\n' + + '## Per-tool support\n' + + '\n' + + '| Tool | Supported | Note |\n' + + '| -------------- | --------- | --------------------------------------------------------------- |\n' + + '| Claude Code | yes | JSON config + script. Richest event set. |\n' + + '| Codex CLI | yes | JSON/TOML config + script. Hooks are stable, on by default. |\n' + + '| Cursor | yes | JSON config + script. |\n' + + "| GitHub Copilot | yes | JSON config + script. Also reads Claude's `.claude/` config. |\n" + + '| OpenCode | no | Hooks are JS/TS plugin modules, not config. Skip with the reason below. |\n' + + '| Kilo | guidance | JS/TS plugin module; no declarative config or generated script. |\n' + + '\n' + + '**OpenCode skip reason.** OpenCode hooks are code, not a config entry plus a script. Point the user to write a plugin under `.opencode/plugins/` (project) or `~/.config/opencode/plugins/` (user), per `https://opencode.ai/docs/plugins`. This skill does not generate it.\n' + + '\n' + + '**Kilo guidance.** Kilo documents JavaScript or TypeScript modules under `.kilo/plugin-mutant/` in a project. Issue #914 also names `.kilo/plugins/`, but the current [official plugin documentation](https://kilo.ai/docs/automate/extending/plugins) does not establish that plural path; mention it only with this limit and recommend the documented singular path. The API exposes typed hooks and an `event` bus. For `session start`, `session.created` is documented; the existing AIDD bridge in `cli/src/contexts/tools/domain/profiles/kilo/kilo-hooks-bridge.ts` may be cited only for the events it actually maps. For any other requested moment, check the official plugin events and typed hooks and the bridge before naming an equivalent. If the equivalent is unproved, state `unsupported: no documented Kilo event for this requested moment; no fallback mapping`, and ask for a different moment or a separately scoped plugin implementation. Do not fold into another event. Do not create `.kilo/hooks.json`, Claude `hooks.json`, a shell script, or a generic plugin for Kilo. Source: [Kilo plugins](https://kilo.ai/docs/automate/extending/plugins). Issue contract verified on 2026-09-25; source rechecked on 2026-10-10.\n' + + '\n' + + '## Lifecycle moment to event name\n' + + '\n' + + 'Each tool names the same moment differently and supports a different subset. Core moments, with the canonical event name per tool. A `-` means the tool does not expose that moment.\n' + + '\n' + + '| Moment | Claude Code | Codex CLI | Cursor | GitHub Copilot |\n' + + '| ------------------ | ------------------ | --------------- | -------------------- | ------------------ |\n' + + '| session start | `SessionStart` | `SessionStart` | `sessionStart` | `SessionStart` |\n' + + '| prompt submitted | `UserPromptSubmit` | `UserPromptSubmit` | `beforeSubmitPrompt` | `UserPromptSubmit` |\n' + + '| before a tool runs | `PreToolUse` | `PreToolUse` | `preToolUse` | `PreToolUse` |\n' + + '| after a tool runs | `PostToolUse` | `PostToolUse` | `postToolUse` | `PostToolUse` |\n' + + '| before compaction | `PreCompact` | `PreCompact` | `preCompact` | `PreCompact` |\n' + + '| subagent stop | `SubagentStop` | `SubagentStop` | `subagentStop` | `SubagentStop` |\n' + + '| turn stop | `Stop` | `Stop` | `stop` | `Stop` |\n' + + '| session end | `SessionEnd` | `SessionEnd` | `sessionEnd` | `SessionEnd` |\n' + + '\n' + + "Each tool exposes more moments than these. For the full list, read the tool's docs: Claude `https://code.claude.com/docs/en/hooks`, Codex `https://developers.openai.com/codex/hooks`, Cursor `https://cursor.com/docs/hooks`, Copilot `https://docs.github.com/en/copilot/reference/hooks-configuration`. Confirm a moment exists before wiring it. Copilot also accepts the camelCase names (`sessionStart`, `preToolUse`). The table does not define Kilo equivalents; Kilo guidance follows its own documented plugin API.\n" + + '\n' + + '## File and format per tool\n' + + '\n' + + '| Tool | File | Shape |\n' + + '| -------------- | ---------------------------------------------------------------- | ------------------------------------------------ |\n' + + '| Claude Code | `settings.json`, plugin `hooks/hooks.json`, or component frontmatter | `{ "hooks": { "": [ { "matcher": "...", "hooks": [ { "type": "command", "command": "..." } ] } ] } }` |\n' + + '| Codex CLI | `~/.codex/hooks.json` or `[hooks]` in `config.toml` | same entry shape as Claude. |\n' + + '| Cursor | `.cursor/hooks.json` | `{ "version": 1, "hooks": { "": [ { "command": "..." } ] } }` |\n' + + '| GitHub Copilot | `.github/hooks/*.json` or a `hooks` block in `.github/copilot/settings.json` | `{ "version": 1, "hooks": { "": [ { "type": "command", "command": "..." } ] } }` |\n' + + '\n' + + 'A Claude `settings.json` and a plugin or standalone `hooks/hooks.json` both wrap the event map under a top-level `hooks` key, so the file is `{ "hooks": { "": [ ... ] } }`. A Codex `config.toml` uses a `[hooks]` table instead.\n' + + '\n' + + '## Scopes per tool\n' + + '\n' + + 'Ask the user which scope, then write the matching file. A `-` means the tool has no such scope.\n' + + '\n' + + '| Scope | Claude Code | Codex CLI | Cursor | GitHub Copilot |\n' + + '| ---------------- | ------------------------------ | -------------------------- | ---------------------------- | ------------------------ |\n' + + '| user / global | `~/.claude/settings.json` | `~/.codex/` (`hooks.json` or `config.toml`) | `~/.cursor/hooks.json` | `~/.copilot/hooks/` |\n' + + '| project, shared | `.claude/settings.json` | `/.codex/` (trust-gated) | `.cursor/hooks.json` | `.github/hooks/` |\n' + + '| project, local | `.claude/settings.local.json` | - | - | - |\n' + + '| component / agent | skill or agent frontmatter | - | - | `.agent.md` frontmatter |\n' + + '| plugin | plugin `hooks/hooks.json` | plugin `hooks.json` | - | - |\n' + + '| enterprise / team | managed policy settings | managed policy | team or enterprise path | `policy.d/` or registry |\n' + + '\n' + + 'Never pick a scope silently. State the resolved file and confirm it.\n' + + '\n' + + 'A component-scoped hook runs only while that component (the skill or agent) is active. A session-start or other always-on moment will not fire reliably from a component. Steer those to a project or user scope.\n' + + '\n' + + '## Handler contract\n' + + '\n' + + 'The shared `command` handler: a script the tool runs at the moment. It reads the event JSON on stdin, then signals back.\n' + + '\n' + + '- **Claude / Codex / Copilot.** Exit `0` is success (stdout may carry a JSON object or, on some moments, context). Exit `2` blocks on a moment that honors it (stderr surfaces to the model). Any other code is a non-blocking error.\n' + + '- **Cursor.** Exit `0` success, exit `2` blocks (same as `permission: "deny"`), any other code fails open. Set `failClosed: true` on the entry to make a crash block instead. Gating moments return `{ "permission": "allow" | "deny" | "ask" }` on stdout.\n' + + '\n' + + "Per-tool stdout schemas and which moments can block live in each tool's docs (linked above). Codex also has a `notify` mechanism (user scope only, single turn-complete event, JSON on argv not stdin) for a simple turn-complete notification.\n" + + '\n' + + '## Path placeholders in handlers\n' + + '\n' + + 'Written as `${VAR}` inside a command: `CLAUDE_PROJECT_DIR` (project root), `CLAUDE_PLUGIN_DATA` (plugin data dir), and the plugin install-directory variable (`CLAUDE_PLUGIN` + `_ROOT`). These are Claude tokens. For another tool, use an absolute path or the path the user names.\n' + + '\n' + + '## Write targets\n' + + '\n' + + "- **Host project.** Merge the entry into the resolved scope's file for each confirmed supported tool. The script goes in a `hooks/` dir beside the config by default, or another dir the user names, referenced by absolute path or an approved `${VAR}`.\n" + + '- **Plugin source.** Merge into `plugins//hooks/hooks.json` (the bare hooks object), with the script under `plugins//hooks/scripts/`.\n' + + '- **Kilo.** No write target. Return the guidance above for the requested moment, even in a mixed fan-out.\n' + + '\n' + + '## Safety checks\n' + + '\n' + + '- **Asset-access precheck.** Before writing, confirm this reference is readable. If not, stop: the plugin is not installed in this host.\n' + + "- **Merge check.** Before writing, read the target file and confirm the new entry is appended to the moment's list, never overwriting a sibling.\n" + + '- **Write-target validation.** After writing, confirm the file is valid and every handler path is an approved `${VAR}` or an absolute path under the workspace (a hook command runs from an arbitrary cwd, so an absolute path or `${VAR}` is expected, not a relative one). Otherwise stop and report.\n' + + at TestContext. ($HOME/project/scripts/__tests__/context-hook-generation.test.js:14:10) + at Test.runInAsyncScope (node:async_hooks:227:14) + at Test.run (node:internal/test_runner/test:1402:25) + at Test.start (node:internal/test_runner/test:1262:17) + at startSubtestAfterBootstrap (node:internal/test_runner/harness:387:17) { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: '# Tool paths (hooks)\n\nPer-tool hook support, event names, file formats, and scopes. This reference covers the Kilo plugin boundary only for hook guidance; it does not generate plugins.\n\n## Per-tool support\n\n| Tool | Supported | Note |\n| -------------- | --------- | --------------------------------------------------------------- |\n| Claude Code | yes | JSON config + script. Richest event set. |\n| Codex CLI | yes | JSON/TOML config + script. Hooks are stable, on by default. |\n...', + expected: /\*\*Kilo guidance\.\*\* Kilo documents JavaScript or TypeScript modules under `\.kilo\/plugin\/` in a project\./u, + operator: 'match', + diff: 'simple' + } +KILLED: exit=1; expected test failed; 3 siblings passed. + +=== MUTATION unsupported_event_no_fallback === +File: plugins/aidd-context/skills/08-hook-generate/references/tool-paths.md +Expected red assertion: Kilo hooks end with sourced guidance and no generated artifacts +Change: unsupported: no documented Kilo event for this requested moment; no fallback mapping => unsupported: Kilo fallback mapping may be used for this requested moment +✖ Kilo hooks end with sourced guidance and no generated artifacts (7.126479ms) +✔ mixed targets preflight before any script or config and preserve user hooks (1.126924ms) +✔ mixed hook contract fixtures retain user duplicates and one AIDD entry per target (1.814927ms) +✔ invalid final mixed target is detectable before creating any earlier target (0.454177ms) +ℹ tests 4 +ℹ suites 0 +ℹ pass 3 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 29.658412 + +✖ failing tests: + +test at scripts/__tests__/context-hook-generation.test.js:10:1 +✖ Kilo hooks end with sourced guidance and no generated artifacts (7.126479ms) + AssertionError [ERR_ASSERTION]: The input did not match the regular expression /unsupported.*no.*fallback/iu. Input: + + '# Tool paths (hooks)\n' + + '\n' + + 'Per-tool hook support, event names, file formats, and scopes. This reference covers the Kilo plugin boundary only for hook guidance; it does not generate plugins.\n' + + '\n' + + '## Per-tool support\n' + + '\n' + + '| Tool | Supported | Note |\n' + + '| -------------- | --------- | --------------------------------------------------------------- |\n' + + '| Claude Code | yes | JSON config + script. Richest event set. |\n' + + '| Codex CLI | yes | JSON/TOML config + script. Hooks are stable, on by default. |\n' + + '| Cursor | yes | JSON config + script. |\n' + + "| GitHub Copilot | yes | JSON config + script. Also reads Claude's `.claude/` config. |\n" + + '| OpenCode | no | Hooks are JS/TS plugin modules, not config. Skip with the reason below. |\n' + + '| Kilo | guidance | JS/TS plugin module; no declarative config or generated script. |\n' + + '\n' + + '**OpenCode skip reason.** OpenCode hooks are code, not a config entry plus a script. Point the user to write a plugin under `.opencode/plugins/` (project) or `~/.config/opencode/plugins/` (user), per `https://opencode.ai/docs/plugins`. This skill does not generate it.\n' + + '\n' + + '**Kilo guidance.** Kilo documents JavaScript or TypeScript modules under `.kilo/plugin/` in a project. Issue #914 also names `.kilo/plugins/`, but the current [official plugin documentation](https://kilo.ai/docs/automate/extending/plugins) does not establish that plural path; mention it only with this limit and recommend the documented singular path. The API exposes typed hooks and an `event` bus. For `session start`, `session.created` is documented; the existing AIDD bridge in `cli/src/contexts/tools/domain/profiles/kilo/kilo-hooks-bridge.ts` may be cited only for the events it actually maps. For any other requested moment, check the official plugin events and typed hooks and the bridge before naming an equivalent. If the equivalent is unproved, state `unsupported: Kilo fallback mapping may be used for this requested moment`, and ask for a different moment or a separately scoped plugin implementation. Do not fold into another event. Do not create `.kilo/hooks.json`, Claude `hooks.json`, a shell script, or a generic plugin for Kilo. Source: [Kilo plugins](https://kilo.ai/docs/automate/extending/plugins). Issue contract verified on 2026-09-25; source rechecked on 2026-10-10.\n' + + '\n' + + '## Lifecycle moment to event name\n' + + '\n' + + 'Each tool names the same moment differently and supports a different subset. Core moments, with the canonical event name per tool. A `-` means the tool does not expose that moment.\n' + + '\n' + + '| Moment | Claude Code | Codex CLI | Cursor | GitHub Copilot |\n' + + '| ------------------ | ------------------ | --------------- | -------------------- | ------------------ |\n' + + '| session start | `SessionStart` | `SessionStart` | `sessionStart` | `SessionStart` |\n' + + '| prompt submitted | `UserPromptSubmit` | `UserPromptSubmit` | `beforeSubmitPrompt` | `UserPromptSubmit` |\n' + + '| before a tool runs | `PreToolUse` | `PreToolUse` | `preToolUse` | `PreToolUse` |\n' + + '| after a tool runs | `PostToolUse` | `PostToolUse` | `postToolUse` | `PostToolUse` |\n' + + '| before compaction | `PreCompact` | `PreCompact` | `preCompact` | `PreCompact` |\n' + + '| subagent stop | `SubagentStop` | `SubagentStop` | `subagentStop` | `SubagentStop` |\n' + + '| turn stop | `Stop` | `Stop` | `stop` | `Stop` |\n' + + '| session end | `SessionEnd` | `SessionEnd` | `sessionEnd` | `SessionEnd` |\n' + + '\n' + + "Each tool exposes more moments than these. For the full list, read the tool's docs: Claude `https://code.claude.com/docs/en/hooks`, Codex `https://developers.openai.com/codex/hooks`, Cursor `https://cursor.com/docs/hooks`, Copilot `https://docs.github.com/en/copilot/reference/hooks-configuration`. Confirm a moment exists before wiring it. Copilot also accepts the camelCase names (`sessionStart`, `preToolUse`). The table does not define Kilo equivalents; Kilo guidance follows its own documented plugin API.\n" + + '\n' + + '## File and format per tool\n' + + '\n' + + '| Tool | File | Shape |\n' + + '| -------------- | ---------------------------------------------------------------- | ------------------------------------------------ |\n' + + '| Claude Code | `settings.json`, plugin `hooks/hooks.json`, or component frontmatter | `{ "hooks": { "": [ { "matcher": "...", "hooks": [ { "type": "command", "command": "..." } ] } ] } }` |\n' + + '| Codex CLI | `~/.codex/hooks.json` or `[hooks]` in `config.toml` | same entry shape as Claude. |\n' + + '| Cursor | `.cursor/hooks.json` | `{ "version": 1, "hooks": { "": [ { "command": "..." } ] } }` |\n' + + '| GitHub Copilot | `.github/hooks/*.json` or a `hooks` block in `.github/copilot/settings.json` | `{ "version": 1, "hooks": { "": [ { "type": "command", "command": "..." } ] } }` |\n' + + '\n' + + 'A Claude `settings.json` and a plugin or standalone `hooks/hooks.json` both wrap the event map under a top-level `hooks` key, so the file is `{ "hooks": { "": [ ... ] } }`. A Codex `config.toml` uses a `[hooks]` table instead.\n' + + '\n' + + '## Scopes per tool\n' + + '\n' + + 'Ask the user which scope, then write the matching file. A `-` means the tool has no such scope.\n' + + '\n' + + '| Scope | Claude Code | Codex CLI | Cursor | GitHub Copilot |\n' + + '| ---------------- | ------------------------------ | -------------------------- | ---------------------------- | ------------------------ |\n' + + '| user / global | `~/.claude/settings.json` | `~/.codex/` (`hooks.json` or `config.toml`) | `~/.cursor/hooks.json` | `~/.copilot/hooks/` |\n' + + '| project, shared | `.claude/settings.json` | `/.codex/` (trust-gated) | `.cursor/hooks.json` | `.github/hooks/` |\n' + + '| project, local | `.claude/settings.local.json` | - | - | - |\n' + + '| component / agent | skill or agent frontmatter | - | - | `.agent.md` frontmatter |\n' + + '| plugin | plugin `hooks/hooks.json` | plugin `hooks.json` | - | - |\n' + + '| enterprise / team | managed policy settings | managed policy | team or enterprise path | `policy.d/` or registry |\n' + + '\n' + + 'Never pick a scope silently. State the resolved file and confirm it.\n' + + '\n' + + 'A component-scoped hook runs only while that component (the skill or agent) is active. A session-start or other always-on moment will not fire reliably from a component. Steer those to a project or user scope.\n' + + '\n' + + '## Handler contract\n' + + '\n' + + 'The shared `command` handler: a script the tool runs at the moment. It reads the event JSON on stdin, then signals back.\n' + + '\n' + + '- **Claude / Codex / Copilot.** Exit `0` is success (stdout may carry a JSON object or, on some moments, context). Exit `2` blocks on a moment that honors it (stderr surfaces to the model). Any other code is a non-blocking error.\n' + + '- **Cursor.** Exit `0` success, exit `2` blocks (same as `permission: "deny"`), any other code fails open. Set `failClosed: true` on the entry to make a crash block instead. Gating moments return `{ "permission": "allow" | "deny" | "ask" }` on stdout.\n' + + '\n' + + "Per-tool stdout schemas and which moments can block live in each tool's docs (linked above). Codex also has a `notify` mechanism (user scope only, single turn-complete event, JSON on argv not stdin) for a simple turn-complete notification.\n" + + '\n' + + '## Path placeholders in handlers\n' + + '\n' + + 'Written as `${VAR}` inside a command: `CLAUDE_PROJECT_DIR` (project root), `CLAUDE_PLUGIN_DATA` (plugin data dir), and the plugin install-directory variable (`CLAUDE_PLUGIN` + `_ROOT`). These are Claude tokens. For another tool, use an absolute path or the path the user names.\n' + + '\n' + + '## Write targets\n' + + '\n' + + "- **Host project.** Merge the entry into the resolved scope's file for each confirmed supported tool. The script goes in a `hooks/` dir beside the config by default, or another dir the user names, referenced by absolute path or an approved `${VAR}`.\n" + + '- **Plugin source.** Merge into `plugins//hooks/hooks.json` (the bare hooks object), with the script under `plugins//hooks/scripts/`.\n' + + '- **Kilo.** No write target. Return the guidance above for the requested moment, even in a mixed fan-out.\n' + + '\n' + + '## Safety checks\n' + + '\n' + + '- **Asset-access precheck.** Before writing, confirm this reference is readable. If not, stop: the plugin is not installed in this host.\n' + + "- **Merge check.** Before writing, read the target file and confirm the new entry is appended to the moment's list, never overwriting a sibling.\n" + + '- **Write-target validation.** After writing, confirm the file is valid and every handler path is an approved `${VAR}` or an absolute path under the workspace (a hook command runs from an arbitrary cwd, so an absolute path or `${VAR}` is expected, not a relative one). Otherwise stop and report.\n' + + at TestContext. ($HOME/project/scripts/__tests__/context-hook-generation.test.js:21:10) + at Test.runInAsyncScope (node:async_hooks:227:14) + at Test.run (node:internal/test_runner/test:1402:25) + at Test.start (node:internal/test_runner/test:1262:17) + at startSubtestAfterBootstrap (node:internal/test_runner/harness:387:17) { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: '# Tool paths (hooks)\n\nPer-tool hook support, event names, file formats, and scopes. This reference covers the Kilo plugin boundary only for hook guidance; it does not generate plugins.\n\n## Per-tool support\n\n| Tool | Supported | Note |\n| -------------- | --------- | --------------------------------------------------------------- |\n| Claude Code | yes | JSON config + script. Richest event set. |\n| Codex CLI | yes | JSON/TOML config + script. Hooks are stable, on by default. |\n...', + expected: /unsupported.*no.*fallback/iu, + operator: 'match', + diff: 'simple' + } +KILLED: exit=1; expected test failed; 3 siblings passed. + +=== MUTATION preflight_precedes_write === +File: plugins/aidd-context/skills/08-hook-generate/actions/02-write-hook.md +Expected red assertion: mixed targets preflight before any script or config and preserve user hooks +✔ Kilo hooks end with sourced guidance and no generated artifacts (2.296055ms) +✖ mixed targets preflight before any script or config and preserve user hooks (8.026158ms) +✔ mixed hook contract fixtures retain user duplicates and one AIDD entry per target (2.210345ms) +✔ invalid final mixed target is detectable before creating any earlier target (0.368708ms) +ℹ tests 4 +ℹ suites 0 +ℹ pass 3 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 35.34342 + +✖ failing tests: + +test at scripts/__tests__/context-hook-generation.test.js:26:1 +✖ mixed targets preflight before any script or config and preserve user hooks (8.026158ms) + AssertionError [ERR_ASSERTION]: The expression evaluated to a falsy value: + + assert.ok(write.indexOf('**Preflight.**') < write.indexOf('**Script.**')) + + at TestContext. ($HOME/project/scripts/__tests__/context-hook-generation.test.js:29:10) + at Test.runInAsyncScope (node:async_hooks:227:14) + at Test.run (node:internal/test_runner/test:1402:25) + at Test.processPendingSubtests (node:internal/test_runner/test:974:18) + at Test.postRun (node:internal/test_runner/test:1542:19) + at Test.run (node:internal/test_runner/test:1467:12) + at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: false, + expected: true, + operator: '==', + diff: 'simple' + } +KILLED: exit=1; expected test failed; 3 siblings passed. + +=== MUTATION exact_aidd_entry_deduplication === +File: plugins/aidd-context/skills/08-hook-generate/actions/02-write-hook.md +Expected red assertion: mixed targets preflight before any script or config and preserve user hooks +Change: append only when that exact AIDD entry is absent => append an AIDD entry for every request +✔ Kilo hooks end with sourced guidance and no generated artifacts (2.21302ms) +✖ mixed targets preflight before any script or config and preserve user hooks (1.84329ms) +✔ mixed hook contract fixtures retain user duplicates and one AIDD entry per target (1.813525ms) +✔ invalid final mixed target is detectable before creating any earlier target (0.366884ms) +ℹ tests 4 +ℹ suites 0 +ℹ pass 3 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 25.54083 + +✖ failing tests: + +test at scripts/__tests__/context-hook-generation.test.js:26:1 +✖ mixed targets preflight before any script or config and preserve user hooks (1.84329ms) + AssertionError [ERR_ASSERTION]: The input did not match the regular expression /exact AIDD entry/u. Input: + + '# 02 - Write hook\n' + + '\n' + + 'Return Kilo guidance and write only confirmed declarative targets after preflighting the entire fan-out.\n' + + '\n' + + '## Input\n' + + '\n' + + 'From 01: the moment, action, script need, matcher, scope, confirmed tools, and write mode.\n' + + '\n' + + '## Output\n' + + '\n' + + 'The updated target file per tool, any script written, and the list of paths touched.\n' + + '\n' + + '## Process\n' + + '\n' + + '1. **Kilo.** Return its terminal guidance from action 01; Kilo has no write target. In a mixed fan-out, report it separately from written targets. No Kilo script, plugin or config is generated.\n' + + '2. **Preflight.** Before any script or config write, resolve every selected non-Kilo destination. Read and parse each existing config in its own format, validate the event list/entry shape and every existing handler needed for the merge, and reject an invalid entry, collision, symlink escape, unsafe script path or unwritable target. Stage the intended entry and handler bytes in memory. If any target is invalid, abort before any write; report the failing path and leave all targets unchanged. A Kilo-only request exits without reaching this step.\n' + + "3. **Script.** If the action needs a backing script ([hook-authoring.md](../references/hook-authoring.md)): copy [hook-script-template.sh](../assets/hook-script-template.sh), fill the logic, place it in the scope's script directory ([tool-paths.md](../references/tool-paths.md)), and make it executable. One script can back every declarative tool. Reuse an existing byte-identical AIDD script without rewriting it; do not overwrite user content.\n" + + "4. **Entry.** Per confirmed declarative tool, fill [hook-template.json](../assets/hook-template.json) from [tool-paths.md](../references/tool-paths.md), using that tool's shape and stripping the scaffold.\n" + + ' - Point the handler at the script by absolute path or an approved `${VAR}`.\n' + + '5. **Merge.** For each tool, append an AIDD entry for every request from the chosen event list. Compare the full entry, including matcher and handler command, not just the event name. Preserve existing order, every user hook and user duplicate, and pre-existing AIDD duplicates. Never normalize or remove prior entries. Skip byte-identical writes on a second pass.\n' + + '6. **Validate.** Run the merge check and write-target validation ([tool-paths.md](../references/tool-paths.md)).\n' + + '\n' + + '## Test\n' + + '\n' + + '- Each target file is valid after the merge.\n' + + '- The new entry is present in each, and every prior sibling survives.\n' + + "- A script-backed handler's script exists at its path and is executable.\n" + + '- A second identical pass leaves config/script bytes and mtimes unchanged; user hooks and duplicates survive.\n' + + '- An invalid last target aborts before any write to an earlier target.\n' + + at TestContext. ($HOME/project/scripts/__tests__/context-hook-generation.test.js:31:10) + at Test.runInAsyncScope (node:async_hooks:227:14) + at Test.run (node:internal/test_runner/test:1402:25) + at Test.processPendingSubtests (node:internal/test_runner/test:974:18) + at Test.postRun (node:internal/test_runner/test:1542:19) + at Test.run (node:internal/test_runner/test:1467:12) + at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: '# 02 - Write hook\n\nReturn Kilo guidance and write only confirmed declarative targets after preflighting the entire fan-out.\n\n## Input\n\nFrom 01: the moment, action, script need, matcher, scope, confirmed tools, and write mode.\n\n## Output\n\n...', + expected: /exact AIDD entry/u, + operator: 'match', + diff: 'simple' + } +KILLED: exit=1; expected test failed; 3 siblings passed. + +=== MUTATION preserve_user_duplicates === +File: plugins/aidd-context/skills/08-hook-generate/actions/02-write-hook.md +Expected red assertion: mixed targets preflight before any script or config and preserve user hooks +Change: Preserve existing order, every user hook and user duplicate, and pre-existing AIDD duplicates. => Replace existing order and user hooks; remove duplicates. +✔ Kilo hooks end with sourced guidance and no generated artifacts (2.365885ms) +✖ mixed targets preflight before any script or config and preserve user hooks (1.704059ms) +✔ mixed hook contract fixtures retain user duplicates and one AIDD entry per target (1.754935ms) +✔ invalid final mixed target is detectable before creating any earlier target (0.414133ms) +ℹ tests 4 +ℹ suites 0 +ℹ pass 3 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 27.295605 + +✖ failing tests: + +test at scripts/__tests__/context-hook-generation.test.js:26:1 +✖ mixed targets preflight before any script or config and preserve user hooks (1.704059ms) + AssertionError [ERR_ASSERTION]: The input did not match the regular expression /preserve.*user.*duplicate/iu. Input: + + '# 02 - Write hook\n' + + '\n' + + 'Return Kilo guidance and write only confirmed declarative targets after preflighting the entire fan-out.\n' + + '\n' + + '## Input\n' + + '\n' + + 'From 01: the moment, action, script need, matcher, scope, confirmed tools, and write mode.\n' + + '\n' + + '## Output\n' + + '\n' + + 'The updated target file per tool, any script written, and the list of paths touched.\n' + + '\n' + + '## Process\n' + + '\n' + + '1. **Kilo.** Return its terminal guidance from action 01; Kilo has no write target. In a mixed fan-out, report it separately from written targets. No Kilo script, plugin or config is generated.\n' + + '2. **Preflight.** Before any script or config write, resolve every selected non-Kilo destination. Read and parse each existing config in its own format, validate the event list/entry shape and every existing handler needed for the merge, and reject an invalid entry, collision, symlink escape, unsafe script path or unwritable target. Stage the intended entry and handler bytes in memory. If any target is invalid, abort before any write; report the failing path and leave all targets unchanged. A Kilo-only request exits without reaching this step.\n' + + "3. **Script.** If the action needs a backing script ([hook-authoring.md](../references/hook-authoring.md)): copy [hook-script-template.sh](../assets/hook-script-template.sh), fill the logic, place it in the scope's script directory ([tool-paths.md](../references/tool-paths.md)), and make it executable. One script can back every declarative tool. Reuse an existing byte-identical AIDD script without rewriting it; do not overwrite user content.\n" + + "4. **Entry.** Per confirmed declarative tool, fill [hook-template.json](../assets/hook-template.json) from [tool-paths.md](../references/tool-paths.md), using that tool's shape and stripping the scaffold.\n" + + ' - Point the handler at the script by absolute path or an approved `${VAR}`.\n' + + '5. **Merge.** For each tool, append only when that exact AIDD entry is absent from the chosen event list. Compare the full entry, including matcher and handler command, not just the event name. Replace existing order and user hooks; remove duplicates. Never normalize or remove prior entries. Skip byte-identical writes on a second pass.\n' + + '6. **Validate.** Run the merge check and write-target validation ([tool-paths.md](../references/tool-paths.md)).\n' + + '\n' + + '## Test\n' + + '\n' + + '- Each target file is valid after the merge.\n' + + '- The new entry is present in each, and every prior sibling survives.\n' + + "- A script-backed handler's script exists at its path and is executable.\n" + + '- A second identical pass leaves config/script bytes and mtimes unchanged; user hooks and duplicates survive.\n' + + '- An invalid last target aborts before any write to an earlier target.\n' + + at TestContext. ($HOME/project/scripts/__tests__/context-hook-generation.test.js:32:10) + at Test.runInAsyncScope (node:async_hooks:227:14) + at Test.run (node:internal/test_runner/test:1402:25) + at Test.processPendingSubtests (node:internal/test_runner/test:974:18) + at Test.postRun (node:internal/test_runner/test:1542:19) + at Test.run (node:internal/test_runner/test:1467:12) + at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: '# 02 - Write hook\n\nReturn Kilo guidance and write only confirmed declarative targets after preflighting the entire fan-out.\n\n## Input\n\nFrom 01: the moment, action, script need, matcher, scope, confirmed tools, and write mode.\n\n## Output\n\n...', + expected: /preserve.*user.*duplicate/iu, + operator: 'match', + diff: 'simple' + } +KILLED: exit=1; expected test failed; 3 siblings passed. + +=== MUTATION invalid_late_target_aborts_before_write === +File: plugins/aidd-context/skills/08-hook-generate/actions/02-write-hook.md +Expected red assertion: mixed targets preflight before any script or config and preserve user hooks +Change: If any target is invalid, abort before any write; report the failing path and leave all targets unchanged. => If any target is invalid, report the failing path after writing and leave targets unchanged. +✔ Kilo hooks end with sourced guidance and no generated artifacts (2.361697ms) +✖ mixed targets preflight before any script or config and preserve user hooks (1.539182ms) +✔ mixed hook contract fixtures retain user duplicates and one AIDD entry per target (1.944979ms) +✔ invalid final mixed target is detectable before creating any earlier target (0.343663ms) +ℹ tests 4 +ℹ suites 0 +ℹ pass 3 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 28.926126 + +✖ failing tests: + +test at scripts/__tests__/context-hook-generation.test.js:26:1 +✖ mixed targets preflight before any script or config and preserve user hooks (1.539182ms) + AssertionError [ERR_ASSERTION]: The input did not match the regular expression /If any target is invalid, abort before any write/u. Input: + + ' Before any script or config write, resolve every selected non-Kilo destination. Read and parse each existing config in its own format, validate the event list/entry shape and every existing handler needed for the merge, and reject an invalid entry, collision, symlink escape, unsafe script path or unwritable target. Stage the intended entry and handler bytes in memory. If any target is invalid, report the failing path after writing and leave targets unchanged. A Kilo-only request exits without reaching this step.' + + at TestContext. ($HOME/project/scripts/__tests__/context-hook-generation.test.js:33:10) + at Test.runInAsyncScope (node:async_hooks:227:14) + at Test.run (node:internal/test_runner/test:1402:25) + at Test.processPendingSubtests (node:internal/test_runner/test:974:18) + at Test.postRun (node:internal/test_runner/test:1542:19) + at Test.run (node:internal/test_runner/test:1467:12) + at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: ' step....', + expected: /If any target is invalid, abort before any write/u, + operator: 'match', + diff: 'simple' + } +KILLED: exit=1; expected test failed; 3 siblings passed. + +=== MUTATION second_pass_is_byte_identical === +File: plugins/aidd-context/skills/08-hook-generate/actions/03-validate.md +Expected red assertion: mixed targets preflight before any script or config and preserve user hooks +Change: second pass is byte-identical => second pass rewrites bytes +✔ Kilo hooks end with sourced guidance and no generated artifacts (3.016019ms) +✖ mixed targets preflight before any script or config and preserve user hooks (2.102203ms) +✔ mixed hook contract fixtures retain user duplicates and one AIDD entry per target (2.012045ms) +✔ invalid final mixed target is detectable before creating any earlier target (0.399145ms) +ℹ tests 4 +ℹ suites 0 +ℹ pass 3 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 29.585616 + +✖ failing tests: + +test at scripts/__tests__/context-hook-generation.test.js:26:1 +✖ mixed targets preflight before any script or config and preserve user hooks (2.102203ms) + AssertionError [ERR_ASSERTION]: The input did not match the regular expression /second pass.*byte.identical/iu. Input: + + '# 03 - Validate\n' + + '\n' + + 'Check Kilo guidance and every written declarative hook against its own contract.\n' + + '\n' + + '## Input\n' + + '\n' + + 'The list of paths touched (from 02).\n' + + '\n' + + '## Output\n' + + '\n' + + 'A short pass or fail line per tool, plus the script.\n' + + '\n' + + '## Process\n' + + '\n' + + '1. **Kilo.** Confirm the guidance cites the official source and documented event or unsupported limit, and compare the full project tree before and after a Kilo-only request: no write. For mixed requests, confirm no Kilo hook/config/plugin was added.\n' + + '2. **Parse.** Confirm each non-Kilo target file still parses in its format and the entry sits at the right key for its scope.\n' + + "3. **Moment.** Confirm the event name is the right one for that tool's moment, and the matcher is well-formed ([tool-paths.md](../references/tool-paths.md)).\n" + + '4. **Blocking.** If the hook blocks, confirm the moment can block on that tool ([tool-paths.md](../references/tool-paths.md)).\n' + + '5. **Script.** For a script-backed handler, confirm the script exists, is executable, and reads stdin and signals per the contract.\n' + + '6. **Relance.** Execute a second pass with identical inputs: config and script bytes and mtimes must be identical; user entries, including duplicates, remain untouched. Test an invalid final target in mixed fan-out and confirm the full tree has no new files or changed bytes.\n' + + '\n' + + '## Test\n' + + '\n' + + '- Each target file parses with the entry under the correct key.\n' + + "- Each event name matches the tool's moment, and any blocking hook sits on a moment that can block.\n" + + '- The second pass rewrites bytes and leaves user hooks intact; invalid input fails before any write.\n' + + at TestContext. ($HOME/project/scripts/__tests__/context-hook-generation.test.js:34:10) + at Test.runInAsyncScope (node:async_hooks:227:14) + at Test.run (node:internal/test_runner/test:1402:25) + at Test.processPendingSubtests (node:internal/test_runner/test:974:18) + at Test.postRun (node:internal/test_runner/test:1542:19) + at Test.run (node:internal/test_runner/test:1467:12) + at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: '# 03 - Validate\n\nCheck Kilo guidance and every written declarative hook against its own contract.\n\n## Input\n\nThe list of paths touched (from 02).\n\n## Output\n\n...', + expected: /second pass.*byte.identical/iu, + operator: 'match', + diff: 'simple' + } +KILLED: exit=1; expected test failed; 3 siblings passed. +RESTORED sha256 6925543f28125e019f9eb876c5296375c9072903abfb15ef7973cd029e491d55 plugins/aidd-context/skills/08-hook-generate/references/tool-paths.md +RESTORED sha256 03cd0d7917e6613e580868c43cb0d17b8cd1e25e74aa80f2bb77143588d00af8 plugins/aidd-context/skills/08-hook-generate/actions/02-write-hook.md +RESTORED sha256 edbe76be0691c8708e68f2384a6e56173135e71849feab95ff86a9b2413c80a3 plugins/aidd-context/skills/08-hook-generate/actions/03-validate.md + +PASS: 7/7 targeted mutations killed; all three source files restored byte-for-byte. diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/security-fix/filesystem-access.md b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/security-fix/filesystem-access.md new file mode 100644 index 000000000..87fbc63bd --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/security-fix/filesystem-access.md @@ -0,0 +1,28 @@ +# Correction sécurité #992 — accès aux fichiers + +Cette correction réduit les substitutions de chemins observables dans le lecteur de configuration Kilo et le hook mémoire. Elle ne promet pas une protection contre un processus hostile qui modifie puis restaure les mêmes répertoires entre les vérifications. + +## Garanties mises en place + +- `kilo-config.cjs` ouvre chaque configuration existante en lecture seule, lit depuis le descripteur, vérifie qu'il désigne un fichier régulier et compare l'identité `dev`/`ino` du descripteur et de chaque composant du chemin avant et après l'ouverture. Un fichier absent reste absent et aucun fichier n'est créé. +- `update_memory.js` ouvre les destinations existantes en lecture/écriture sans `O_TRUNC`, vérifie les composants et le descripteur, puis conserve le descripteur validé pour la mise à jour. Il écrit le contenu complet à l'offset zéro avant de tronquer l'ancien suffixe. Il garde l'inode et les métadonnées attachées au fichier; les bits de permission ordinaires restent inchangés, tandis que le système peut effacer des bits spéciaux lors d'une écriture. Les destinations avec plusieurs liens physiques sont refusées pour ne pas modifier un alias extérieur au projet. +- Les liens symboliques finaux et intermédiaires observés sont refusés. En mode automatique, une README mémoire symlink reste ignorée si elle n'est pas une destination opt-in; en mode explicite, une README opt-in dangereuse interrompt le préflight avant toute écriture. +- Les erreurs de préflight explicite arrivent avant les écritures. Le mode automatique reste best-effort entre fichiers, comme défini en phase 1. + +## Modèle de menace et limites + +Le modèle couvre les symlinks statiques et les substitutions de fichier ou de répertoire qui persistent assez longtemps pour être observées pendant les vérifications. Les tests injectent ces changements immédiatement après `open` et vérifient que les témoins extérieurs ne changent pas. + +`O_NOFOLLOW` est utilisé sur les systèmes non Windows pour refuser un lien symbolique sur le dernier composant ouvert. Il ne protège pas les répertoires parents. Windows n'offre pas ce flag via les options portables de Node; l'identité du descripteur et les vérifications `lstat` avant/après sont donc les contrôles applicables. Node ne fournit pas ici un parcours portable de chaque parent relativement à un descripteur de répertoire (`openat`/équivalent). Un adversaire concurrent capable de substituer puis restaurer rapidement un parent peut encore gagner une fenêtre de course. Ce scénario n'est pas une garantie AIDD de cette fonctionnalité. + +L'écriture sur le descripteur évite la troncature prématurée, le suivi d'un nouveau symlink final et le remplacement de l'inode, mais une erreur d'E/S après le début de l'écriture ou une interruption brutale peut laisser un contenu partiel. Il n'y a pas de transaction multi-fichier ni de rollback après panne; les limites déjà documentées en phase 1 restent applicables. Le refus des hardlinks peut interrompre la synchronisation d'un fichier utilisateur lié physiquement à un autre chemin. + +## Preuves ciblées + +- Remplacements du fichier et du parent juste après ouverture, en automatique et explicite : `scripts/__tests__/update-memory.test.js`. +- Mode et inode conservés, hardlink refusé, témoins extérieurs inchangés : `scripts/__tests__/update-memory.test.js`. +- Échec d'écriture injecté avant le premier octet : destination et témoin extérieur inchangés, en automatique et explicite : `scripts/__tests__/update-memory.test.js`. +- Remplacements du fichier et du parent pendant l'ouverture du lecteur de configuration : `scripts/__tests__/kilo-rule-publication.test.js`. +- Assertions de destinations Markdown exactes et refus des textes URL/lookalike hosts : `scripts/__tests__/context-kilo-detection.test.js` et `scripts/__tests__/context-skill-artifacts.test.js`. + +La phase 2 de #914 reste `in-progress`. Cette correction sécurise seulement la lecture isolée de configuration; elle n'intègre pas le writer de #979 et ne valide aucun critère de publication finale. La phase 6 n'est pas exécutée par cette correction. diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/security-fix/platform-followup.md b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/security-fix/platform-followup.md new file mode 100644 index 000000000..61f1f8e41 --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/security-fix/platform-followup.md @@ -0,0 +1,38 @@ +# Suivi CI multiplateforme — correction sécurité #992 + +Ce suivi accompagne `filesystem-access.md`. Il conserve les constats des contrôles GitHub après le commit publié `d5f1309a`; il ne remplace ni ne modifie les preuves brutes antérieures. + +## Alertes CodeQL #125–#128 + +Le contrôle CodeQL de la PR #992 a signalé deux opérations de fichier sujettes à TOCTOU dans le test des hardlinks et deux créations de fichiers prévisibles dans la suite de substitutions (`scripts/__tests__/update-memory.test.js`, anciennes lignes 492, 498, 530 et 532). Les chemins directement sous le répertoire temporaire partagé pouvaient être précréés ou entrer en collision; le test des hardlinks réécrivait puis relisait des chemins après l'appel au hook au lieu de garder des témoins stables. + +- Les témoins de contenu sont maintenant ouverts avant l'opération et lus par leurs descripteurs. Les assertions comparent identité, liens, mode, taille, date de modification et octets via ces descripteurs. La vérification que le chemin d'origine désigne encore le témoin ouvre une fois le chemin puis inspecte le descripteur obtenu. +- Les fichiers et répertoires extérieurs sont créés sous des répertoires uniques issus de `mkdtempSync`, puis supprimés dans `finally`. Les descripteurs sont fermés avant la suppression, y compris pour les systèmes qui refusent de supprimer un fichier encore ouvert. +- Les tests de substitution vérifient le marqueur d'injection, le refus de production et l'absence de mutation des témoins; ils ne relisent pas les chemins contrôlés après la course. + +Le statut CodeQL de ces quatre alertes après modification reste à confirmer par une nouvelle analyse GitHub. Leur correction locale ne permet pas de conclure à leur clôture dans Code Scanning. + +## Échecs Windows observés + +Le job Windows avait signalé sept échecs répartis ainsi : assertions de frontmatter sensibles à LF dans les fixtures agent et skill; hashes calculés sur octets CRLF de checkout au lieu des octets canoniques LF; attente de refus d'accès fondée sur `chmod(0555)`; attente de bits de mode POSIX exacts; et deux variantes de substitution du répertoire parent dont les assertions dépendaient d'un renommage puis d'un lien symbolique et d'un chemin `.held`. + +- Les textes des fixtures sont normalisés CRLF vers LF seulement pour les assertions sémantiques et les hashes d'oracle; les hashes historiques restent ceux des octets canoniques LF. +- Le refus de préflight est testé par injection déterministe d'une erreur `EACCES`, sans prétendre que les ACL Windows reproduisent les permissions POSIX. +- Le test de mode exige `0640` sur POSIX et vérifie la conservation du mode effectivement rapporté sur Windows. +- POSIX teste les substitutions réelles par renommage et lien symbolique. Pour le parent sur Windows, où le remplacement physique du répertoire n'est pas portable, un hook de test injecte une identité de parent différente après l'ouverture. Dans les deux cas, le refus de production et les témoins par descripteur sont vérifiés. Ce scénario Windows teste la détection de changement d'identité, pas toutes les courses de répertoires possibles. + +Les échecs rapportés concernaient les hypothèses de tests et leurs oracles; aucun défaut de production n'a été démontré par ces logs. Une CI Windows verte reste nécessaire avant de conclure à la compatibilité Windows. macOS et Windows ne sont pas disponibles dans cette validation locale. + +## Statut AIDD et limites + +- La phase 2 de #914 reste `in-progress`, en attente de coordination avec le writer de #979. Aucun writer ni fichier d'intégration de #979 n'est modifié. +- La phase 6 reste inachevée et hors de cette correction. +- Les quatre alertes antérieures #116–#119 sont indiquées comme résolues ou obsolètes dans les commentaires de la PR, mais leur statut Code Scanning interne n'a pas pu être confirmé : la lecture de l'API Code Scanning a renvoyé HTTP 403. +- Les validations locales Linux ne prouvent pas les permissions, liens symboliques, identité de fichiers ni substitutions sous Windows/macOS. Les jobs GitHub Actions et l'analyse CodeQL doivent être examinés après la publication autorisée du correctif. + +## Validations locales + +- `node scripts/check-tests-leave-git-alone.js --watch .git/hooks -- node --test scripts/__tests__/update-memory.test.js scripts/__tests__/context-agent-command-artifacts.test.js scripts/__tests__/context-skill-artifacts.test.js scripts/__tests__/kilo-rule-publication.test.js scripts/__tests__/context-kilo-detection.test.js` — 118 tests réussis. +- `node scripts/check-tests-leave-git-alone.js -- node --test 'scripts/__tests__/**/*.test.js'` — 664 tests réussis, zéro échec ou skip. +- `pnpm test:changed` — code de sortie 0. +- `make check` a été lancé, mais Lefthook a sauté toutes ses commandes parce que les modifications étaient unstaged; ce résultat n'est pas compté comme validation. diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/stage-b-targeted-validation.md b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/stage-b-targeted-validation.md new file mode 100644 index 000000000..cfc73007e --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/stage-b-targeted-validation.md @@ -0,0 +1,48 @@ +# Targeted AC13–14 validation + +Validation on 2026-10-14 (Europe/Paris), working tree based on `b9b31cd48efdb0d6c40279f49357a7c44a29a4ac`. These checks ran the installed skill copies through Codex CLI 0.162.1 in isolated temporary projects. They did not run a Kilo host or an authenticated Claude session. + +## Behavioral cases + +Each case was run with `node scripts/skill-eval.mjs aidd-context --host=codex --case='' --judge --keep`. The final run of every listed case returned `PASS`, with zero `INCOMPLETE` results. `--keep` retained the temporary projects for local inspection; the hashes below preserve the relevant file observations in this report. + +| Case name | Final result | Decisive observation | +| --- | --- | --- | +| Kilo native skill uses one canonical tree | PASS | Native files exist, portable duplicate absent; harness second pass preserved project bytes and mtimes. | +| existing exact AIDD entry and user duplicates remain unchanged | PASS | Kilo hook file absent; existing Claude config, script and Kilo config unchanged; judge passed. | +| Kilo agent rerun preserves its destination and user files | PASS | One native agent, no legacy or Claude copy; user files unchanged; harness second pass preserved project bytes and mtimes. | +| Kilo workflow rerun preserves its destination and user files | PASS | One native command, no legacy or Claude copy; user files unchanged; harness second pass preserved project bytes and mtimes. | +| unsafe late Kilo target leaves earlier Claude target absent | PASS | Existing unsafe target unchanged; earlier Claude skill absent; judge passed. | +| last Kilo agent collision refuses before Claude write | PASS | User agent unchanged; earlier Claude agent absent; judge passed. | +| last Kilo workflow collision refuses before Claude write | PASS | User workflow unchanged; earlier Claude command absent; judge passed. | +| invalid final mixed target stops before prior writes | PASS | Invalid Copilot config and Kilo config unchanged; Claude config/script and Kilo hook absent; judge passed. | + +The initial run of the workflow collision case returned `FAIL` only on its LLM judge: the judge required a verbal assertion that the user file was unchanged, while the deterministic check had already verified unchanged bytes. The case's judge criterion was narrowed to the reported collision and absent Claude command. Its rerun passed, including the unchanged-file checks. A further rerun passed deterministic checks for refusal wording and an unchanged complete project tree, including the absence of any new output. No generator contract was changed for this result. + +## Retained snapshot checks + +For each refused operation, the listed fixture files were read back from the retained temporary project and compared byte-for-byte with the case setup. Every expected absent output was checked again. The SHA-256 value identifies the serialized setup file map used for that comparison. + +| Refused case | Setup files unchanged | Outputs absent | Setup SHA-256 | +| --- | ---: | ---: | --- | +| Unsafe late Kilo skill target | 2 | 1 | `b5dc1feeb9c64682f63a93fc6c3b73357398f6e95716141db43dd69f9451a860` | +| Last Kilo agent collision | 2 | 1 | `990da88a55c4256e14e3dd7937cb6527606348aab73464521728d88afb8e4355` | +| Last Kilo workflow collision | 2 | 1 | `539224bda200f5ab6ded345b8dd8f763a1b02fedf2355929d443c7a71ec320e7` | +| Invalid final mixed hook target | 2 | 3 | `db33b07d146390598b47474957f6110deac6c575ab04d9ec09cfcb99eb5f88d7` | + +Generated destination snapshots after successful reruns: + +| Destination | SHA-256 | +| --- | --- | +| `.kilo/skills/verify-payload/SKILL.md` | `cb6a884833dd64bcd4a16721f1e489c8662482ba7a475036e125473951cd4b24` | +| `.kilo/agents/verify-agent.md` | `b46a74c4f8b6c17c2e2cb3ee402a5f7186d8fa669fc3390620955c482bb37b68` | +| `.kilo/commands/verify-workflow.md` | `b992cf47b3e49d9bbe942bffc871147b41dd8510225a2cecd9470db2c6e8bc71` | +| Unchanged `.claude/settings.json` for the exact hook entry | `27d5116e163fe8763f721705554fc808d2986cedca6de33643af70cc3f29a8c0` | + +## Contract and publication checks + +- `node --test` on the seven targeted detection, artifact, hook, rule, memory and portability suites: 162 passed, 0 failed, 0 skipped. +- `node scripts/check-markdown-links.js`: 0 broken links in 951 files. +- `git diff --check`: passed. + +The contract tests verify available instructions and fixtures, not universal LLM compliance. The automatic memory hook intentionally remains best-effort and is outside the explicit-sync preflight guarantee. An I/O error after an authorized write starts remains tracked in #1018. diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/gather.md b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/gather.md new file mode 100644 index 000000000..8c56d77ee --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/gather.md @@ -0,0 +1,10 @@ +# Source recueillie + +Action `01-gather` de la capacité installée `aidd-dev:01-plan`, exécutée dans le contexte du caller Codex. + +- Source ticket : https://github.com/ai-driven-dev/framework/issues/914, corps courant récupéré le 10 octobre 2026, updated_at 2026-10-10T09:45:34Z ; un commentaire, sans changement de scope. +- Sources locales lues intégralement avant le nettoyage : le résumé de reprise, `user-request.md` et `issue-914.md`. Le résumé de reprise n’est pas conservé ; les instructions et exigences utiles restent dans le plan, la demande et l’issue. Les instructions actualisées du message utilisateur priment sur leur état historique. +- Résultat demandé : détection Kilo, mémoire partagée et génération native de règles, skills, agents et workflows ; guidance précise des plugins/hooks ; seize critères officiels. +- Contraintes : sources Claude canoniques ; préservation des autres outils, du contenu utilisateur et de JSONC ; idempotence ; aucune écriture partielle sur entrée invalide ou édition dangereuse ; preuves runtime. +- Frontières : #744 déjà livré, #868 flat hors périmètre ; aucune conversion générique de hooks ni implémentation de plugin runtime. +- Session autorisée : plan, phases, matrice, stratégie de tests et challenge uniquement. Aucun code produit, commit, push ou PR. Validation utilisateur nécessaire avant développement. diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/issue-914.md b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/issue-914.md new file mode 100644 index 000000000..d50094cba --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/issue-914.md @@ -0,0 +1,153 @@ +# Issue #914 + +Source : https://github.com/ai-driven-dev/framework/issues/914 + +Snapshot received 2026-10-10; updated_at: 2026-10-10T09:45:34Z + +> Summary: Make every applicable aidd-context workflow detect Kilo and emit artifacts through Kilo's documented native or config-backed surfaces, without overwriting user-owned context or inventing unsupported declarative hooks. + +## Outcome + +A Kilo-only project can onboard AIDD, wire project memory, and generate rules, skills, agents, and workflows that Kilo discovers without manual file moves or configuration repair. + +## Problem + +The CLI supports Kilo through #744, but aidd-context still omits Kilo from onboarding, project-memory wiring, and the skill, rule, agent, command, and hook generation contracts. + +A Kilo-only project is therefore proposed inconsistently. Some generators lack a Kilo target; others do not document the configuration required to make the generated artifact discoverable. + +## Official Kilo contract + +Verified on 2026-09-25. + +- Project memory: AGENTS.md at the project root is loaded automatically. AGENT.md is a fallback, and per-directory AGENTS.md files are loaded when Kilo reads files beneath them: + https://kilo.ai/docs/customize/agents-md +- Rules: Markdown files such as .kilo/rules/.md are loaded through the instructions array in project configuration. Entry order is significant: + https://kilo.ai/docs/customize/custom-rules +- Skills: Kilo implements Agent Skills and discovers project skills from .kilo/skills/ and compatibility locations including .agents/skills/: + https://kilo.ai/docs/customize/skills +- Agents: project Markdown agents live under .kilo/agents/. The filename defines the name; YAML frontmatter can define description, mode, model, temperature, and permissions: + https://kilo.ai/docs/customize/custom-subagents +- Workflows: project workflows are Markdown slash commands under .kilo/commands/: + https://kilo.ai/docs/customize/workflows +- Plugins and hooks: JavaScript or TypeScript modules live under .kilo/plugin/ or .kilo/plugins/ and expose typed hooks or the event bus, including session.created: + https://kilo.ai/docs/automate/extending/plugins +- Project configuration: Kilo reads kilo.json and kilo.jsonc at the project root or under .kilo/. The .kilo/ form has higher priority, and loaded configurations are deep-merged: + https://kilo.ai/docs/getting-started/settings +- Legacy compatibility: .kilocode/ remains a recognized fallback, while .kilo/ is canonical for new artifacts: + https://github.com/Kilo-Org/kilocode/blob/main/packages/opencode/src/kilocode/skills/kilo-config.md + +## Detection and configuration decision + +Detect Kilo from these unambiguous signals: + +- .kilo/ +- .kilocode/ as a legacy detection signal only +- kilo.json or kilo.jsonc at the project root +- .kilo/kilo.json or .kilo/kilo.jsonc + +Do not detect Kilo from opencode.json[c] alone because that signal is shared with OpenCode legacy compatibility. + +Generate all new Kilo-specific artifacts under canonical .kilo/ paths. Never generate new artifacts under .kilocode/. + +When a generator must update instructions: + +1. If no project Kilo config exists, create .kilo/kilo.jsonc. +2. If exactly one project Kilo config exists, update it. +3. If several project configs exist and one already owns the AIDD instruction entry, update that file idempotently. +4. Otherwise present the existing candidates in Kilo precedence order and let the user choose which file to edit. +5. Preserve every existing instructions entry, duplicate, order, JSONC comment, trailing comma, and unrelated formatting. +6. If a safe edit cannot be made, stop before writing and return an actionable error. + +Kilo deep-merges supported config files. Do not silently inherit the CLI's stricter dual-config refusal as if it were a Kilo runtime constraint. + +## Generation targets + +### Onboarding and project memory + +- Propose Kilo when any canonical or legacy Kilo signal is detected. +- Treat root AGENTS.md as Kilo's primary project-memory surface. +- Preserve user-authored content and update only the AIDD-owned memory block. +- Deduplicate AGENTS.md when another selected tool shares the same target. + +### Rules + +- Write .kilo/rules//.md. +- Add that exact relative path to the selected config's instructions array. +- Describe the directory as config-backed, not auto-discovered. +- Preserve instruction order because Kilo loads entries in declared order. + +### Skills + +- Generate Agent Skills format with a SKILL.md whose name matches its parent directory. +- Use .kilo/skills// for a Kilo-only target. +- Offer .agents/skills// only when the user explicitly selects a portable shared target. +- Never duplicate one skill into both locations during the same generation. + +### Agents + +- Write .kilo/agents/.md. +- Use the filename as the agent name. +- Emit documented YAML frontmatter with description and mode: subagent; preserve supported optional model, temperature, and permission fields when requested. + +### Workflows + +- Write .kilo/commands/.md. +- Preserve documented optional frontmatter such as description, agent, model, variant, and subtask. + +### Hooks + +- Do not emit Claude-style declarative hooks for Kilo. +- Explain that Kilo uses JavaScript or TypeScript plugins under .kilo/plugin/ or .kilo/plugins/. +- Name the documented event or typed hook that corresponds to the requested lifecycle only when the mapping is proven. +- Do not invent mappings for unsupported Claude hook events. +- Generic runtime plugin implementation remains out of scope. The existing CLI bridge may be referenced only for its captured session.created mapping. + +## Acceptance criteria + +- [ ] A Kilo-only fixture is detected by onboarding and every applicable generator. +- [ ] Detection covers .kilo/, root kilo.json[c], .kilo/kilo.json[c], and legacy .kilocode/. +- [ ] Legacy .kilocode/ detection still produces new artifacts under canonical .kilo/ paths. +- [ ] opencode.json[c] alone does not produce a false Kilo detection. +- [ ] AGENTS.md generation preserves user content and deduplicates a shared target. +- [ ] Rule generation writes .kilo/rules//.md and wires the exact path through instructions. +- [ ] Config selection respects Kilo's precedence and asks before choosing among ambiguous existing files. +- [ ] JSON and JSONC updates preserve existing entries, duplicates, order, comments, trailing commas, and unrelated formatting. +- [ ] Skill placement requires an explicit choice before using .agents/skills/. +- [ ] Agent paths and frontmatter match the documented Markdown-agent contract. +- [ ] Workflows are generated under .kilo/commands/ with supported frontmatter only. +- [ ] Hook generation returns precise plugin guidance and never emits an unsupported declarative hook. +- [ ] Re-running every generator is idempotent. +- [ ] Invalid or unsafe configuration causes no partial write. +- [ ] Runtime-oriented tests prove Kilo discovers each emitted artifact on supported platforms. +- [ ] Every Kilo path claim carries an official source and the 2026-09-25 verification date. + +## Repository evidence + +- CLI Kilo profile: cli/src/contexts/tools/domain/profiles/kilo/profile.ts +- Current config resolver: cli/src/contexts/tools/domain/profiles/kilo/kilo-paths.ts +- Existing captured SessionStart bridge: cli/src/contexts/tools/domain/profiles/kilo/kilo-hooks-bridge.ts +- aidd-context generators: plugins/aidd-context/skills/02-project-init and 04-skill-generate through 08-hook-generate + +## Relations and boundaries + +- #744 delivered the CLI Kilo profile. +- #868 owns Kilo flat rule distribution. +- #790 remains closed; this issue does not restore a generic session-start bridge. +- Supersedes the Kilo portion of #788. +- Flat archive emission, arbitrary hook conversion, and runtime plugin implementation are out of scope. + + +## Comments + +https://github.com/ai-driven-dev/framework/issues/914#issuecomment-6096252579 + +Hi @blafourcade, + +I'd like to start working on this issue. + +I'll follow the defined scope and acceptance criteria. + +I'll work on a dedicated branch targeting next. + +Thanks! diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/official-sources.md b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/official-sources.md new file mode 100644 index 000000000..e38f99ff0 --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/official-sources.md @@ -0,0 +1,18 @@ +# Contrats officiels et dates + +L'issue [#914](https://github.com/ai-driven-dev/framework/issues/914) déclare **Verified on 2026-09-25**. Cette date est l'affirmation historique du ticket ; le dépôt ne contient ni capture ni version archivée des pages permettant de la vérifier indépendamment. Ne pas la présenter comme une vérification effectuée par cette révision. Les pages officielles ci-dessous ont été relues le **2026-10-13**. Les références de génération renvoient à ce registre pour les sources et leur date de vérification actuelle. + +| Surface | Source officielle relue | Contrat retenu | +| --- | --- | --- | +| Mémoire | https://kilo.ai/docs/customize/agents-md | AGENTS.md racine principal, AGENT.md fallback ; contexte par répertoire au read ; autorisation utilisateur requise par Kilo pour modifier ces fichiers. Ne pas supposer que des liens Markdown sont des imports automatiques. | +| Règles | https://kilo.ai/docs/customize/custom-rules | Fichiers sous .kilo/rules/ reliés par instructions ; ordre déclaré significatif ; dossier canonique seul insuffisant. | +| Skills | https://kilo.ai/docs/customize/skills | Agent Skills, .kilo/skills/ ; .agents/skills/ compatible. Le choix explicite d'une cible portable est une contrainte AIDD #914, pas une restriction du loader. | +| Agents | https://kilo.ai/docs/customize/custom-subagents | .kilo/agents/ ; nom du fichier ; description et mode subagent ; options demandées model, temperature et permission (singulier dans le YAML). | +| Workflows | https://kilo.ai/docs/customize/workflows | .kilo/commands/ ; description, agent, model, variant, subtask optionnels. Ne pas injecter les champs Claude non supportés. | +| Plugins | https://kilo.ai/docs/automate/extending/plugins ; https://github.com/Kilo-Org/kilocode/blob/main/packages/opencode/src/kilocode/skills/kilo-config.md | La documentation nomme explicitement `.kilo/plugin/` pour un projet et `.kilocode/plugin/` en legacy ; elle décrit aussi génériquement les dossiers `plugin/` et `plugins/` dans les config dirs. La référence upstream liste les deux formes. Recommander `.kilo/plugin/` comme chemin explicitement illustré, sans qualifier `.kilo/plugins/` d'invalide. AIDD ne génère pas de plugin Kilo. | +| Configuration | https://kilo.ai/docs/getting-started/settings | kilo.json/jsonc racine ou .kilo/ ; .kilo/ prioritaire ; fichiers fusionnés. Aucun refus runtime général des deux formats. | +| Legacy et recherche | https://github.com/Kilo-Org/kilocode/blob/main/packages/opencode/src/kilocode/skills/kilo-config.md | .kilocode/ fallback ; variantes singulier/pluriel acceptées pour certains artefacts. Nouveaux artefacts AIDD dans les chemins pluriels canoniques du ticket. | + +Revérification technique complémentaire, également consultée le **2026-10-13** : [config.ts](https://github.com/Kilo-Org/kilocode/blob/main/packages/opencode/src/config/config.ts) et [paths.ts](https://github.com/Kilo-Org/kilocode/blob/main/packages/opencode/src/config/paths.ts). Ces sources `main` sont mouvantes et ne prouvent pas l'implémentation exacte du binaire 7.8.8. Elles montrent notamment une fusion d'instructions dédupliquée et un chargement de plusieurs fichiers. Les doublons conservés à l'écriture ne doivent donc pas être exigés dans le tableau runtime fusionné. L'ordre JSON/JSONC au même emplacement reste à établir par une sonde sur le binaire ciblé ; seule la priorité `.kilo/` sur la racine est explicitée dans la documentation officielle. Présenter les groupes à priorité connue et ne pas choisir silencieusement dans un groupe ambigu. + +Divergences conservées : le bridge #971 couvre maintenant Stop/PostToolUse en plus de SessionStart ; la génération #914 ne s'attribue que la correspondance capturée session.created. Certaines docs plugins mentionnent encore des chemins opencode/cache legacy ; ne pas reprendre ces passages pour les nouveaux artefacts. Les compétences compatible Claude ou les fallbacks OpenCode ne sont jamais des signaux suffisants de détection Kilo. diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/overlap-979.md b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/overlap-979.md new file mode 100644 index 000000000..30036ec7d --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/overlap-979.md @@ -0,0 +1,23 @@ +# Chevauchement avec #979 + +Lecture GitHub du 10 octobre 2026 : [#979](https://github.com/ai-driven-dev/framework/pull/979) open, draft=true, merged=false, base next, base_sha 7f0b344b4ae37c083aa9473e0e6347b196b630ef, head 303e17a1ba995dd19ec769381f7bbb594fbd1d7c. Le merge_commit_sha fourni pour une PR ouverte n'est pas une fusion réalisée. Base locale #914 : 41e91691b5837a8c27475d0115725131bb53bf41. Ne pas supposer que next distant correspond encore à cette base. + +Patch réellement inspecté : actions/02-write-rule.md et nouveau scripts/write-rule.cjs, après listing des fichiers modifiés (197). Métadonnées, description et listing archivés dans remote-state.json. Pas de revue exhaustive des 197 fichiers de #979 prétendue ici. + +Le writer de #979 produit sources aidd_docs/rules, sorties Claude/Cursor/Copilot et contribution signée dans AGENTS.md pour Codex/OpenCode. Il exclut Kilo. Préflight global des destinations et ownership, puis renommages atomiques par fichier ; sa description exclut explicitement le rollback global après panne disque. Donc extension par simple ajout de kilo dans TOOLS insuffisante : il faut gérer config + règle + source comme une publication cohérente. + +## Décision proposée + +Avant l’intégration de phase 2, recontrôler état/head de #979. Si fusionnée, projeter #914 sur le writer intégré et conserver intégralement ses sorties non Kilo. Tant que #979 reste Draft, l’autorisation du 10 octobre 2026 permet seulement les fixtures, tests, module isolé `kilo-config.cjs` et validations Kilo sans publication décrits dans phase-2.md. L’intégration au writer, la publication source + règle + configuration et ses régressions restent suspendues jusqu’à nouvelle décision explicite et revalidation de la base. Aucun cherry-pick, rebase ou changement de branche. Ne pas copier le writer Draft ou construire un writer concurrent. Une décision de poursuivre l’intégration sur l’ancienne base devra amender ce plan explicitement. + +Après disponibilité du writer : ajouter un rendu Kilo au même flux autonome, un module d'édition Kilo local au skill, préflight de toute entrée et staging avant publication. Réutiliser les 59 tests annoncés de #979 seulement après vérification de leur présence réelle dans la base ; ce chiffre est une déclaration de PR, pas notre résultat de test. N'importer ni le resolver CLI strict ni des dépendances du checkout dans un skill installé. + +## Risques et contrôles + +- Conflit élevé : actions et références de 05-rule-generate, tests de publication, ownership et docs ; aucun conflit attendu sur profils CLI. +- Régression élevée : contribution AGENTS.md des règles versus bloc mémoire ; invariants de deux blocs disjoints, user bytes intacts, sélection Kilo-only n'ajoute pas deux fois une contribution commune. +- Les anciens contrats .codex/rules et .opencode/rules dans HEAD ne deviennent pas corrects par un snapshot. Établir une baseline de la base effectivement retenue ; les corrections #979 sont des deltas acceptés, pas des régressions Kilo. +- Réexécuter les cas standalone copiés hors checkout, projet ESM, sans binaire aidd, native et flat existant ; vérifier seulement la livraison de nos assets, pas ajouter la distribution de règles flat #868. +- Aucun merge de #979, commentaire ou PR distante dans cette session. + +[#971](https://github.com/ai-driven-dev/framework/pull/971) : merged=true, base next, fusion le 8 octobre 2026 ; commit 12777d03808cee29f611e18d3816ae62ebc5c969 déjà dans HEAD. Garder ses adaptateurs et tests, sans nouvelle conversion de hooks. Ses preuves antérieures Kilo 7.7.5 ne remplacent pas les futures preuves #914 sur 7.8.8. diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/phase-1-report.md b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/phase-1-report.md new file mode 100644 index 000000000..8d7981911 --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/phase-1-report.md @@ -0,0 +1,50 @@ +# #914 — Rapport phase 1 + +Reconnaissance Kilo et mémoire partagée implémentées, tests et runtime mémoire validés. Phases 2–6 non commencées. Aucun push, PR ou changement de branche. Les commits locaux sont enregistrés séparément après validation de leur tâche. + +## Plan et autorisation + +[Ajustements ciblés](plan-adjustments.md) : progression tests/preuves/statut par tâche ; décision historique si #979 restait Draft ; phase 2 sans moteur transactionnel générique ; support plateformes/runner à vérifier avant toute extension CI. Le snapshot de plan antérieur, redondant avec le plan actuel et les ajustements résumés ici, a été retiré pendant le nettoyage. La sauvegarde locale B1 conserve l’état modifié pré-nettoyage, pas un original brut distinct ; aucun original brut séparé n’a été identifié. Seize AC conservés verbatim ; challenge 90 % demeure une appréciation historique du plan. Pas de nouvelle planification complète. + +La clarification ultérieure autorise les commits locaux par tâche validée et prime sur l’interdiction de commit dans la demande collée. Le contrat installé implement prévoyait un commit par phase ; la préférence utilisateur par TASK prévaut. Plan global in-progress, phase 1 seule livrée, autres phases pending ; aucune action finalize du plan complet ni step-end global. + +## Changement fonctionnel + +TASK 1 : deux contrats de reconnaissance, six signaux Kilo et fixtures positives/négatives. AGENTS et configs OpenCode seuls ne détectent pas Kilo. La détection OpenCode reste `.opencode/`. `.kilocode/` est seulement historique, jamais une nouvelle destination. + +TASK 2 : Kilo rejoint TOOL_FILES et le Set existant vers AGENTS ; TARGET_FILES inchangé. Sync prévalide avant Upsert, puis le hook prévalide le jeu complet avant Fill. Refus des marqueurs incomplets, doubles, imbriqués, inversés, mixtes et destinations non régulières, symlinks ou non inscriptibles. README contrôlé seulement si opt-in. Scanner partagé ; exemples hors bloc ignorés. Préservation des bytes utilisateur, BOM/CRLF/indentation, imports Claude et liens Copilot. Mode automatique best-effort et staging conservés. README plugin et cas skill-eval associés mis à jour. + +TASK 3 : sources/date et preuve de génération/consommation, détaillées dans [runtime](phase-1-runtime.md). Le caller natif utilise réellement le routeur installé et l’action candidate, sans mise à jour des plugins utilisateur. Kilo lit architecture.md via le lien et répond au token caché dans cette mémoire, coût 0. + +## Validation et limites + +| Commande ou preuve | Résultat | +| --- | --- | +| Tests détection avant changement | 24 échecs attendus : ligne Kilo absente | +| Tests mémoire initiaux | 16 pass / 18 fail dans sandbox ; restriction stderr identifiée | +| Rejeu hook initial Git, hors sandbox sur copie | 36 tests, 19 pass / 17 fail, exit 1 | +| Deux défauts de review avant correction | 2 tests, 2 fail : écriture préalable malgré chmod444 ; README sans opt-in refusé | +| Tests ciblés finaux | 64 tests : 40 mémoire + 24 détection, tous réussis | +| Mutation détection | omission signal imbriqué => 1 échec ciblé | +| Mutation mémoire | préflight désactivé => ancien contexte modifié, assertion échoue | +| make check final | exit 0, 606 tests / 604 pass / 2 skips / 0 fail | +| Architecture et JSON explicites | exit 0, 3 fichiers sans violation et 2 JSON valides | +| Runtime Kilo mémoire | read réel, réponse exacte, exit 0, deux coûts 0 | + +Les résultats de ce tableau sont des validations historiques rapportées. Leurs journaux détaillés ont été retirés pendant le nettoyage ; ces chiffres ne sont pas directement rejouables depuis l’archive conservée. + +Les assertions AIDD et la review détaillée ont été retirées ; leurs résultats utiles sont résumés dans ce rapport. Les deux skips sont des tests existants du formateur Biome, pas des validations Kilo converties en pass. Aucun fichier CLI modifié : lint/typecheck/knip CLI non requis ici. + +Claude : tests hors ligne des vrais artefacts, ESM, marqueurs, imports, migration et idempotence. Aucun runtime Claude avec modèle authentifié. CONTRIBUTING demande de tester dans Claude et un autre outil : sa partie Claude reste non accomplie, explicitement signalée pour la future contribution. Codex CLI a sélectionné le skill mais ses trois runs sont bloqués par `codex-code-mode-host` absent ; le code 0 n’est pas un succès. Génération effective = caller natif actuel. OpenCode absent ; contrats partagés vérifiés par sous-processus, aucun runtime OpenCode. Kilo Linux seulement ; il ajoute `$schema` au kilo.json de fixture au démarrage, sans changer AGENTS ni mémoire. + +Le préflight empêche les écritures sur les erreurs connues testées. Aucune transaction multi-fichiers face à crash, course concurrente ou erreur d’écriture imprévisible n’est promise. Aucun nouveau moteur générique. + +Deux cas skill-eval sont ajoutés au corpus existant ; son runner Claude authentifié n’a pas été exécuté ici. Le parcours positif est validé dans Codex natif et Kilo ; le refus avant création est validé par les sous-processus et la revue du contrat, sans prétendre une seconde inférence CLI réussie. + +## Traçabilité et suite + +AC5 validé ; AC1–4, 13–16 partiels ; AC6–12 non implémentés. [Matrice détaillée](coverage-matrix.md), sans valider les critères transversaux trop tôt. L’onboarding autonome et tous les autres générateurs restent à prouver dans leurs phases. + +#979 reste une dépendance future à revalider avant phase 2 ; pas d’attente ni de code Draft intégré en phase 1. Réutiliser sa version fusionnée ou obtenir une décision explicite si Draft. #971 déjà intégrée ; #744/#868 inchangées. Attendre une autorisation distincte avant toute autre phase. + +Commits locaux : `4eed91c3` (TASK 1 reconnaissance, 24 tests), `9179e60d` (TASK 2 mémoire, 40 tests et runtime), puis `docs(aidd-context): record validated Kilo memory phase` (TASK 3 preuves/reprise ; SHA = HEAD à la livraison). Le registre de commits détaillé a été retiré ; les commits restent consultables dans l’historique Git. Les deux premiers hooks ont passé 604 tests, 2 skips et commitlint ; le troisième conserve les documents approuvés et l’historique dans le même dossier. Aucun fichier fonctionnel des autres phases. diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/phase-1-runtime.md b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/phase-1-runtime.md new file mode 100644 index 000000000..789b4a745 --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/phase-1-runtime.md @@ -0,0 +1,37 @@ +# Phase 1 — Preuves runtime + +Les observations ci-dessous sont des résultats historiques conservés en résumé. Les captures détaillées, sorties de tests et reçus caller ont été retirés pendant le nettoyage ; ce document ne transforme donc pas ces validations passées en preuves directement rejouables. + +Validation effectuée le 10 octobre 2026, sur des données synthétiques dans `/tmp/aidd-914-runtime-codex-project`. Aucun compte Anthropic ni achat utilisé. Aucun runtime Claude authentifié exécuté. La preuve porte sur la mémoire de phase 1, pas sur les générateurs des phases suivantes. + +## Génération par Codex natif + +Le caller de cette session Codex a réellement invoqué le routeur installé `aidd-context:02-project-memory`, puis exécuté son action `sync` en liant explicitement les sources candidates `actions/04-sync.md`, `references/tools.md` et le hook actualisé. Il ne s'agit pas d'une modification de l'installation utilisateur. Le choix préalable est `kilo` seulement ; le seul signal Kilo est `kilo.json`. Le fichier AGENTS existant ne constitue pas un signal de détection. + +Le caller a exécuté `node hooks/update_memory.js --check kilo` avant l'Upsert, ajouté la section Memory Management à partir du template sans changer le préfixe utilisateur, exécuté `node hooks/update_memory.js kilo`, puis relu le lien exact vers `aidd_docs/memory/architecture.md` et vérifié le résultat. Les contrôles explicites ont renvoyé `0`. Le contenu produit et le hash du hook sont conservés dans les artefacts JSON. Le reçu des étapes caller complète ces artefacts. Les commandes et lectures natives du caller sont dans la conversation de cette session ; cette archive ne prétend pas être une trace CLI de cette génération. + +Trois essais supplémentaires de Codex CLI 0.162.0 ont réellement sélectionné le skill local de fixture, mais n'ont pu lire son action ni générer les fichiers : `codex-code-mode-host` absent. La désactivation transitoire du host et la conservation du modèle utilisateur `gpt-6.1-sol` n'ont pas corrigé ce blocage. Leurs sorties `0` ne sont pas une réussite fonctionnelle. Aucun outil réinstallé ni configuration utilisateur modifiée. Les traces sont conservées dans essai 1, essai 2 et essai 3. + +## Consommation réelle par Kilo + +Commande exécutée, avec XDG_CONFIG_HOME, XDG_DATA_HOME, XDG_STATE_HOME et XDG_CACHE_HOME isolés sous `/tmp/aidd-914-runtime-xdg` : + +```sh +kilo run --pure --dir /tmp/aidd-914-runtime-codex-project \ + --model kilo/cohere/north-mini-code:free --format json \ + 'What is the answer to the phase 1 memory verification question? Read the project instructions and relevant project memory, and apply its answer convention. Do not modify files.' +``` + +Le prompt ne contient ni le chemin mémoire ni la réponse attendue. AGENTS contient seulement le lien produit par AIDD ; la réponse et sa convention résident dans la mémoire liée. La trace montre que le modèle voit ce lien, appelle réellement `read` sur `aidd_docs/memory/architecture.md`, puis répond exactement `MEMORY_APPLIED:prune-6842`. Code de sortie `0`, deux étapes avec coût `0`, AGENTS et banque mémoire inchangés après exécution. Cela démontre la découverte du contexte AGENTS et l'utilisation effective de la mémoire liée pour accomplir la tâche. Voir trace runtime et assertions observées. + +Les métadonnées récupérées avant le test indiquent `isFree: true`, entrée/sortie/cache à `0`, et `mayTrainOnYourPrompts: true`. La fixture ne contient que des données synthétiques. `kilo auth list` rapporte zéro credential dans l'état isolé : l'accès gratuit effectif fonctionne sans connexion à un compte enregistré. Cette observation ne garantit pas la disponibilité future du modèle. Voir métadonnées. + +Deux lectures superflues de chemins `.kilo/command/*.md` et `.kilo/agent/*.md` ont échoué parce que ces artefacts ne font pas partie de la fixture. La lecture mémoire et la réponse sont réussies ; ces essais ne valident aucun générateur de commandes ou agents. Un premier diagnostic séparé, portant sur une autre fixture remplie directement par le hook, a atteint son timeout de 100 secondes sans événement runtime ; il est conservé dans trace du diagnostic. La preuve principale ci-dessus utilise bien l'artefact généré par le caller Codex. + +Kilo a ajouté automatiquement le champ `$schema` à son fichier de fixture `kilo.json` au démarrage. Le préfixe utilisateur, AGENTS et la mémoire sont restés identiques ; le changement de configuration avant/après est conservé dans les assertions JSON. Il ne faut pas attribuer cette écriture au générateur AIDD. + +Les hashes des sources finales permettent de vérifier que le hook de fixture correspond au candidat ; les hashes des essais CLI antérieurs restent archivés séparément. + +## Limites + +Cette preuve satisfait le parcours mémoire Kilo de phase 1. Elle ne valide pas intégralement les critères runtime de l'issue qui exigent des artefacts des phases 2 à 6. La génération par un sous-processus Codex CLI reste bloquée par le host absent ; la génération effective validée ici est celle du caller natif de la session actuelle. OpenCode n'est pas installé ; ses garanties de phase 1 doivent s'appuyer sur les tests automatisés. Claude peut être validé hors ligne pour les artefacts, mais aucun comportement de modèle Claude authentifié n'a été testé. diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/phase-1.md b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/phase-1.md new file mode 100644 index 000000000..5f5d74d05 --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/phase-1.md @@ -0,0 +1,79 @@ +--- +status: done +--- + +# Instruction: Reconnaître Kilo et partager la mémoire + +## Architecture projection + +> Arbre des fichiers finaux. ✅ créer · ✏️ modifier · aucune suppression. +> Les chemins conditionnels sont résolus à l’étape de préflight, jamais créés en doublon. + +```txt +. +✏️ plugins/aidd-context/skills/00-onboard/references/state/detection.md +✏️ plugins/aidd-context/skills/02-project-memory/references/tools.md +✏️ plugins/aidd-context/skills/02-project-memory/actions/04-sync.md +✏️ plugins/aidd-context/hooks/update_memory.js +✏️ scripts/__tests__/update-memory.test.js +✏️ scripts/skill-eval/cases.json +✅ scripts/__tests__/context-kilo-detection.test.js +✅ scripts/__tests__/fixtures/context-generation/detection/ +✏️ plugins/aidd-context/README.md +``` + +## User Journey + +```mermaid +flowchart TD + A[Demande et cibles confirmees] --> B[Preflight du contrat] + B --> C[Resultat de la phase] + C --> D[Verification observable et documentation] +``` + +## Test Scope + +```mermaid +--- +title: Test scope +--- +journey + section Setup + Preparer fixture et baseline => projet isole: 5: system + section Happy path + Plusieurs outils partagent AGENTS.md => une synchronisation et aucune perte de texte: 5: cli + Relancer action identique => resultat stable: 5: cli + section Edge case - refus + Marqueur invalide dans dernière cible => refus avant mutation de la première: 1: cli + section Teardown + Nettoyer projet et processus => baseline restauree: 5: system +``` + +## Tasks to do + +### `1)` Reconnaissance cohérente + +> Statut : done. 24 tests de contrats/fixtures réussis et mutation ciblée détectée. + +1. Étendre les deux contrats de détection avec les six signaux officiels, y compris JSON/JSONC explicites ; .kilocode est un signal historique, jamais une sortie. AGENTS.md seul et opencode.json[c] seuls ne distinguent pas Kilo. Ajouter fixtures positives et négatives réutilisables dans les phases suivantes, avec garde contractuelle et cas de génération réel. Ne pas modifier bootstrap ni ressusciter project-init. + +### `2)` Une seule surface de mémoire + +> Statut : done. 40 sous-processus réussis, préflight et conservation vérifiés ; runtime Kilo et review approve. + +1. Ajouter kilo à TOOL_FILES sans ajouter TARGET_FILES ; conserver Set pour Kilo/Codex/OpenCode/Cursor. Dans sync, confirmer les outils, dédupliquer les destinations et modifier seulement le bloc AIDD. Préflight des cibles explicitement sélectionnées et du README opt-in avant Upsert comme avant Fill, donc avant toute création de contexte ou mutation pour refuser des marqueurs incomplets/ambigus. Conserver le mode best-effort du hook automatique sans élargir le scope. Étendre les sous-processus existants et les six scénarios mémoire, préserver imports Claude et liens Copilot. + +### `3)` Contrat lisible et preuve + +> Statut : done. Codex natif et Kilo mémoire observés ; review approve ; preuves et reprise archivées. + +1. Ajouter les sources/date au contrat mémoire et documenter Kilo dans le README sans répéter toutes les tables. Exécuter un parcours Codex sur une fixture Kilo-only, puis valider lecture mémoire dans Kilo lors du test runtime de cette phase ; preuve authentifiée Claude non requise pour ces artefacts. + +## Test acceptance criteria + +| Task | Acceptance criteria | +| --- | --- | +| 1 | Les six signaux proposent Kilo ; fichiers OpenCode seuls et AGENTS.md seul ne le proposent pas. | +| 2 | Kilo + outils partageant AGENTS.md donnent un seul bloc ; contenu utilisateur et fichiers non sélectionnés identiques. | +| 2 | Deux synchronisations identiques ne réécrivent rien ; un marqueur invalide dans la dernière cible nommée laisse toutes les cibles et README inchangés. | +| 3 | CLAUDE.md conserve @imports, Copilot conserve ses liens relatifs ; Kilo charge AGENTS.md et lit une référence mémoire nécessaire à la tâche. | diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/phase-2-report.md b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/phase-2-report.md new file mode 100644 index 000000000..c62407409 --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/phase-2-report.md @@ -0,0 +1,25 @@ +# Rapport de phase 2 — Règles Kilo + +## Résultat et statut + +L’implémentation de phase 2 étend le writer partagé intégré par #979. Kilo utilise les sources canoniques existantes et le même cycle de publication, les mêmes contrôles de propriété, de préservation et de rollback. `kilo-config.cjs` sélectionne et prépare l’édition JSONC ; il n’effectue pas une seconde publication. + +La phase 2 reste **partielle selon ses critères ouverts**. Les critères techniques propres à la phase sont satisfaits, mais le statut reste **`in-progress`** : `CONTRIBUTING.md` demande `make reload`, le redémarrage des sessions et un test dans Claude et un autre outil. Kilo est l’autre outil testé ; aucune validation runtime Claude authentifiée n’est disponible. Aucune exception explicite du mainteneur n’est documentée. Ne pas marquer `completed` avant cette validation ou cette exception. Le plan global reste `in-progress`, la phase 6 étant toujours ouverte. + +## Tests et portée des preuves + +- Tests ciblés : **90/90 historiquement rapportés**. Ils couvraient le writer de #979 et l’intégration Kilo, génération/publication, préflight, refus sans mutation, préservation JSONC, idempotence, erreurs simulées et rollback. La sortie détaillée a été retirée ; ce résultat n’est pas une preuve directement rejouable à partir de l’archive conservée. +- Suite scripts : **795/795 historiquement rapportés**. La sortie détaillée a été retirée ; ce résultat reste une validation automatisée rapportée, pas une validation runtime d’autres hôtes. +- [Observation runtime Kilo](evidence/phase-2/runtime-report.md) : Kilo CLI 7.8.8 sous Linux a répondu avec le marqueur de la règle publiée ; en retirant uniquement `instructions`, une session neuve n’a pas reçu le marqueur. Cette preuve concerne cette fixture et cette version. Les sorties publiques sont expurgées ; les captures brutes restent hors des livrables. +- Les tests de fichier démontrent la préservation des commentaires, du formatage, des doublons, de l’ordre et des valeurs sans rapport. Ils ne constituent pas une preuve de résolution runtime dans Kilo. + +## Limites + +- Claude Code authentifié : non testé ; exigence `CONTRIBUTING.md` encore ouverte. +- Windows : non testé ; sémantique des modes de fichiers non validée sur Windows. +- Le mécanisme ne garantit pas l’absence de courses entre vérification et appel système, ni la restauration après mutation externe concurrente, arrêt brutal ou panne machine. Le fan-out multi-fichiers n’est pas une transaction globale. +- Le runtime Kilo n’est prouvé que sur Linux avec Kilo 7.8.8 et le modèle disponible au moment de l’essai. + +## Mise à jour du plan + +La matrice de couverture distingue les tests de fichier des observations runtime. La phase reste `in-progress` jusqu’au contrôle Claude exigé ou à une exception explicite du mainteneur ; la phase 6 est ouverte et distincte de ce bilan. diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/phase-2.md b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/phase-2.md new file mode 100644 index 000000000..f0b89a4b4 --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/phase-2.md @@ -0,0 +1,104 @@ +--- +status: in-progress +--- + +# Instruction: Publier une règle Kilo sans perdre sa configuration + +## Reprise du 12 octobre 2026 + +`upstream/next` contient le merge de #979 (`c827990f`). Le writer partagé de #979 a été étendu pour Kilo ; les tâches 1–4 ci-dessous sont implémentées et leurs critères techniques sont vérifiés par tests et par une observation runtime Kilo 7.8.8/Linux. La phase 6 reste hors périmètre. Le statut de phase reste `in-progress` jusqu’à la validation dans Claude demandée par `CONTRIBUTING.md`, ou une exception explicite du mainteneur ; aucune preuve d’une telle validation ou exception n’est disponible. + +## Décision historique du 10 octobre 2026 + +Cette tranche limitait initialement le travail tant que #979 était Draft. Elle est obsolète depuis la fusion de #979 dans `next` et la décision ultérieure d’étendre son writer. Elle ne décrit plus le périmètre courant. + +## Architecture projection + +> Arbre des fichiers finaux. ✅ créer · ✏️ modifier · aucune suppression. +> Les chemins conditionnels sont résolus à l’étape de préflight, jamais créés en doublon. + +```txt +. +✏️ plugins/aidd-context/skills/05-rule-generate/SKILL.md +✏️ plugins/aidd-context/skills/05-rule-generate/actions/01-capture-rule.md +✏️ plugins/aidd-context/skills/05-rule-generate/actions/02-write-rule.md +✏️ plugins/aidd-context/skills/05-rule-generate/actions/03-validate.md +✏️ plugins/aidd-context/skills/05-rule-generate/references/tool-paths.md +✏️ plugins/aidd-context/skills/05-rule-generate/scripts/write-rule.cjs (base fournie par #979) +✅ plugins/aidd-context/skills/05-rule-generate/scripts/kilo-config.cjs +✏️ scripts/__tests__/rule-generation.test.js (fourni par #979, revalider après intégration) +✅ scripts/__tests__/kilo-rule-publication.test.js +✅ scripts/__tests__/fixtures/context-generation/kilo-config/ +✏️ plugins/aidd-context/README.md +``` + +## User Journey + +```mermaid +flowchart TD + A[Demande et cibles confirmees] --> B[Preflight du contrat] + B --> C[Resultat de la phase] + C --> D[Verification observable et documentation] +``` + +## Test Scope + +```mermaid +--- +title: Test scope +--- +journey + section Setup + Preparer fixture et baseline => projet isole: 5: system + section Happy path + Règle confirmée et config choisie => publication native puis chargement par Kilo: 5: cli + Relancer action identique => resultat stable: 5: cli + section Edge case - refus + Deux propriétaires ou config invalide => demande ou refus sans écritures: 1: cli + section Teardown + Nettoyer projet et processus => baseline restauree: 5: system +``` + +## Tâches et résultats + +### `1)` Aligner le propriétaire des règles + +> Aligner le propriétaire des règles. + +1. **Done.** Réutiliser le writer partagé de #979, conserver ses contrats existants et y intégrer Kilo ; aucun deuxième writer n’est créé. + +### `2)` Sélection de configuration observable + +> Sélection de configuration observable. + +1. **Done.** Avant publication, inspecter les quatre configurations candidates ; utiliser le défaut documenté si aucune n’existe, la candidate unique si une seule existe, et le propriétaire unique de l’entrée exacte ou un choix explicite si plusieurs existent. Les cas ambigus refusent avant mutation. Les tests et la sonde Kilo 7.8.8 couvrent la sélection et la fusion observée ; aucun ordre total au-delà des configurations sondées n’est revendiqué. + +### `3)` Publication sans perte + +> Publication sans perte. + +1. **Done.** Publier la règle sous `.kilo/rules//.md` et ajouter son chemin exact à `instructions`. Préserver les octets JSON/JSONC hors insertion et refuser les états ambigus/dangereux avant écriture. Les tests vérifient le préflight, l’idempotence et la restauration pour les erreurs couvertes. Les courses entre contrôle et appel système, l’arrêt brutal et la panne machine ne sont pas garantis. + +### `4)` Tests et documentation de publication + +> Tests et documentation de publication. + +1. **Done.** Les tests du writer partagé et les tests Kilo couvrent génération/publication, idempotence, préservation, refus sans mutation et rollback des erreurs simulées. Une observation Kilo 7.8.8/Linux avec contrôle positif et négatif vérifie le chargement de cette règle. Les limites et la provenance sont consignées dans [le rapport de phase](phase-2-report.md) et [les preuves runtime expurgées](evidence/phase-2/runtime-report.md). + +## Test acceptance criteria + +| Task | Acceptance criteria | +| --- | --- | +| 3 | Règle canonique Kilo et chemin instructions exact existent ; Kilo charge le corps ; dossier seul ne suffit pas. | +| 2 | Aucun choix implicite en ambiguïté ; propriétaire unique réutilisé, annulation ne crée aucun fichier. | +| 3 | JSON/JSONC gardent doublons, ordre, commentaires, trailing commas et format hors insertion ; second passage byte-identique. | +| 3 | Configuration invalide ou cible dangereuse, y compris dernier fichier d’un fan-out, ne laisse aucun changement ; erreurs de publication testées ne laissent ni règle orpheline ni référence cassée. | +| 4 | Les publications Claude/Cursor/Copilot et partagées Codex/OpenCode du writer retenu gardent leurs contrats et les blocs de mémoire. | + +## Validations de clôture + +- Tests ciblés : **90/90 historiquement rapportés** ; la sortie détaillée a été retirée et ce résultat n’est pas directement rejouable depuis l’archive conservée. +- Suite scripts : **795/795 historiquement rapportés** ; la sortie détaillée a été retirée et ce résultat n’est pas directement rejouable depuis l’archive conservée. +- Kilo 7.8.8/Linux : observation positive et négative ; voir [rapport runtime](evidence/phase-2/runtime-report.md). Cela démontre le chargement de cette fixture dans cet environnement, pas une couverture générale de modèles ou plateformes. +- Claude authentifié : non exécuté. Windows : non exécuté. `CONTRIBUTING.md` demande `make reload`, redémarrage des sessions et test dans Claude et un autre outil. Le contrôle Claude reste donc ouvert ; une clôture sans ce test nécessite une exception explicite du mainteneur, non obtenue à ce jour. +- Limites : pas de garantie face aux courses entre contrôle et appel système, à une mutation externe durant la restauration, à l’arrêt brutal ou à une panne machine. diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/phase-3-report.md b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/phase-3-report.md new file mode 100644 index 000000000..bf3b2fc26 --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/phase-3-report.md @@ -0,0 +1,36 @@ +# #914 — Rapport phase 3 + +Phase 3 implémentée et revue `approve`, quatre critères de phase couverts. Phase 1 clôturée par l’utilisateur avec ses limites conservées. Plan global in-progress ; phases 2 et 4–6 pending. Aucun writer #979, push, PR distante ou changement de branche. + +## Cohérence et périmètre + +La phase 3 dépend de la reconnaissance acquise en phase 1, sans dépendance au writer de règles #979. Le plan validé et les seize AC restent les références ; seule sa phrase d’autorisation a été actualisée. Workflow natif `aidd-dev:02-implement` : prepare sur branche existante propre, execute limité à phase 3 selon autorisation, assertion passing avant done. Aucun finalize global ni step-end global, les autres phases restant ouvertes. + +Les trois tâches sont done : sélection de surface, rendu/préservation, vérification des parcours. Commit local unique de phase conformément au contrat implement, code + tests + docs + statut. Le registre de commits détaillé a été retiré ; les commits restent consultables dans l’historique Git. + +## Comportement livré + +Le générateur reconnaît les six signaux Kilo, garde OpenCode distinct et utilise le chemin canonique pour legacy. Il propose le natif ; le portable exige un choix explicite. Destinations partagées dédupliquées, copies utilisateur soumises à résolution. Kilo natif + Codex ou optionnels Kilo incompatibles avec les champs d’une destination partagée demandent une résolution avant écriture. + +Rendu Kilo Agent Skills name égal au dossier, description valide, optionnels documentés seulement si demandés, aucun argument-hint Claude injecté. Template et autres contrats hosts conservés. Préflight de toutes les cibles/références avant création, refus des chemins dangereux, symlinks et conflits ; modify préserve les actions/assets utilisateur hors demande, relance identique conserve bytes/mtime. Ce contrat interprété ne promet aucune transaction crash/concurrence. + +## Critères et preuves + +- Kilo-only natif : une copie canonique, YAML valide ; legacy produit sous .kilo sans toucher .kilocode. +- Portable : accord explicite dans les entrées synthétiques, copie unique ; absence d’accord, duplication, choix mixtes incompatibles et champs partagés non résolus refusés. +- Préservation : sept arbres générés via le caller natif, modify limité à sept actions, ressources utilisateur inchangées, rerun arbre/hashes/mtime identique, huit refus avec snapshots inchangés. +- Régression et usage : sorties Claude/Codex/OpenCode contrôlées hors ligne ; catalogues Kilo natif/portable et deux invocations réelles avec lectures action/payload et réponse exacte. + +AC9 validé. AC5 demeure validé par phase 1. AC1–4 et AC13–16 progressent mais restent partiels ; AC6–8 et AC10–12 non implémentés. Aucun critère transversal clos sur la seule présence d’un fichier. [Matrice](coverage-matrix.md), [runtime](phase-3-runtime.md) ; les dossiers détaillés de review/assertions ont été retirés. + +Tests : 24 ciblés réussis ; `make check` final exit 0, 630 tests / 628 pass / 2 skips existants / 0 fail. Mutations détection et préflight rouges, architecture et JSON explicites réussis. Kilo 7.8.8 Linux : natif et portable découverts au chemin exact, skill invoqué, action et payload lus, résultat exact, quatre étapes coût 0 chacun, bytes/mtime du skill préservés. Première réponse native avec espace exclue, puis rejeu exact réussi sans modifier les artefacts. + +Ces résultats sont historiques. Leurs journaux, traces Kilo et sorties de tests détaillées ont été retirés pendant le nettoyage ; ils ne sont pas directement rejouables depuis l’archive conservée. + +## Limites et reprise + +Les fixtures sont générées par le caller Codex actuel après lecture du routeur installé et des actions candidates. Les choix sont synthétiques et explicités dans le reçu ; ce n’est pas une nouvelle réponse interactive utilisateur ni une seconde inférence Codex CLI. Les refus sont interprétés par ce caller, sans writer déterministe livré. Les sept cas skill-eval du runner Claude authentifié sont ajoutés mais non exécutés. + +Absence de runtime Claude authentifié conservée ; CONTRIBUTING reste incomplet sur ce point. OpenCode runtime non exécuté, macOS/Windows Kilo non exécutés. Le blocage CLI Codex de phase 1 n’est pas requalifié ni réévalué. Kilo ajoute `$schema` à son config de fixture : delta runtime conservé et distinct d’AIDD. Aucun crash, course concurrente ou panne imprévisible multi-fichiers garanti. Les espaces terminaux des logs bruts sont conservés comme preuve exacte, pas introduits dans les sources. + +Arrêt après phase 3. Phase 2 attend toujours coordination #979 ; aucune autre phase autorisée. Attendre l’accord utilisateur avant de poursuivre. diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/phase-3-runtime.md b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/phase-3-runtime.md new file mode 100644 index 000000000..3bf98ceee --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/phase-3-runtime.md @@ -0,0 +1,38 @@ +# #914 — Runtime phase 3 + +Les observations ci-dessous sont des résultats historiques conservés en résumé. Les traces détaillées, scripts caller, catalogues et sorties de tests ont été retirés pendant le nettoyage ; ce document ne transforme donc pas ces validations passées en preuves directement rejouables. + +## Génération dans le caller Codex + +Capacité native installée `aidd-context:04-skill-generate` 2.8.1 réellement chargée. Ses actions ont été lues dans l’ordre scope, plan, write, validate, avec les références/actions candidates du workspace pour le comportement Kilo. Aucun plugin utilisateur mis à jour. Les templates canoniques lus ont été remplis par le caller, puis rendus selon le contrat de chaque host. Les fichiers attendus n’ont pas été copiés depuis un golden ni produits par un renderer de test. Les sorties effectivement générées sous `/tmp/aidd-914-p3-generation` ont ensuite été archivées dans le corpus fixtures. + +Les scripts ponctuels du caller et leurs snapshots détaillés ont été retirés. Les résultats résumés ci-dessous sont historiques ; ils ne sont pas directement rejouables depuis cette archive et ces scripts n’étaient pas des composants distribués. + +Les choix rapportés sont des entrées synthétiques du caller autorisées par les parcours du plan : nom verify-payload, action verify, payload bundled, Kilo natif, portable explicitement choisi, legacy natif, Claude, Codex, OpenCode, puis Kilo portable + Codex partageant une destination. Aucun accord interactif supplémentaire de l’utilisateur n’est prétendu. Le reçu détaillé contenant frame, choix, plan atomique, fichiers relus, hashes initiaux et revalidation après review a été retiré. + +Le modify rapporté change uniquement les sept actions verify. Les ressources utilisateur ajoutées comme entrées de fixture restent intactes. La seconde exécution de la même demande n’écrit rien selon les hashes/modes/mtime archivés à l’époque ; la carte détaillée a été retirée. Le caller a refusé six demandes dangereuses avant mkdir/write : portable non confirmé, copies existantes, dernière cible non régulière, symlink, lecture seule, référence hors skill. Deux refus complémentaires ont réglé les contradictions Kilo natif + Codex et optionnel Kilo + champ Codex partagé ; leurs snapshots ont été retirés. + +Ces refus prouvent les décisions et états du caller natif présent. Ils ne sont ni une seconde inférence autonome CLI ni un writer déterministe livré. Les sept nouveaux cas du runner skill-eval authentifié restent non exécutés. Le blocage Codex CLI documenté en phase 1 n’a pas été réévalué et ne devient pas une preuve réussie ici. + +## Découverte et usage Kilo + +Kilo 7.8.8 Linux, XDG_CONFIG_HOME/DATA_HOME/STATE_HOME/CACHE_HOME isolés dans `/tmp/aidd-914-p3-xdg`. Le catalogue modèle courant a été interrogé avant l’inférence ; modèle explicite `kilo/cohere/north-mini-code:free`, `isFree: true`, coûts entrée/sortie/cache nuls dans métadonnées. Données synthétiques seulement ; la disponibilité future du modèle n’est pas garantie. + +```sh +# CWD = fixture native, puis portable +kilo debug skill --pure +kilo run --pure --dir /tmp/aidd-914-p3-generation/native \ + --model kilo/cohere/north-mini-code:free --format json \ + 'Use the verify-payload skill to verify the phase three fixture. Read and follow its action. Return only its exact verification code, with no inserted whitespace or commentary. Do not modify files.' +kilo run --pure --dir /tmp/aidd-914-p3-generation/portable \ + --model kilo/cohere/north-mini-code:free --format json \ + 'Use the verify-payload skill to verify the phase three fixture. Follow its action and return its verification code. Do not modify files.' +``` + +Les catalogues exposent une seule entrée verify-payload au chemin exact natif ou portable. Les traces prouvent appel skill completed, lecture réelle de actions/01-verify.md puis assets/payload.txt, réponse exacte `PAYLOAD_APPLIED:cedar-7391`, exit 0 et quatre étapes coût 0 par parcours : natif, portable. Le prompt ne contient ni le payload ni le code attendu. + +Premier essai natif : même chargement complet, mais espace ajouté dans la réponse. Trace conservée, exclue de l’oracle de format exact ; le second essai précise l’absence de whitespace, sans modifier le skill. Vérification : tous les fichiers générés du skill gardent bytes et mtime. Kilo ajoute lui-même `$schema` à kilo.json ; ce delta de configuration n’est pas attribué à AIDD. Fixtures finales archivées avant ce changement runtime. + +## Limites + +Linux seulement ; macOS/Windows non exécutés. Aucun runtime Claude authentifié, OpenCode absent. Leurs sorties générées par Codex ont été contrôlées hors ligne pour YAML, champs propres aux hosts, arbre et liens. Aucun runtime IDE Cursor/Copilot prétendu. Aucun nouveau moteur de transaction multi-fichiers ; erreurs imprévisibles, crash et concurrence ne sont pas couverts par les refus interprétés. Le cleanup supprime seulement les fixtures/XDG de cette phase après archivage, pas les plugins utilisateur ni les fixtures historiques de phase 1. diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/phase-3.md b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/phase-3.md new file mode 100644 index 000000000..c8dae427b --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/phase-3.md @@ -0,0 +1,80 @@ +--- +status: done +--- + +# Instruction: Générer un skill natif ou portable choisi + +## Architecture projection + +> Arbre des fichiers finaux. ✅ créer · ✏️ modifier · aucune suppression. +> Les chemins conditionnels sont résolus à l’étape de préflight, jamais créés en doublon. + +```txt +. +✏️ plugins/aidd-context/skills/04-skill-generate/references/tool-detect.md +✏️ plugins/aidd-context/skills/04-skill-generate/references/tool-write.md +✏️ plugins/aidd-context/skills/04-skill-generate/references/scope-frame.md +✏️ plugins/aidd-context/skills/04-skill-generate/actions/01-scope.md +✏️ plugins/aidd-context/skills/04-skill-generate/actions/03-write.md +✏️ plugins/aidd-context/skills/04-skill-generate/actions/04-validate.md +✅ scripts/__tests__/context-skill-artifacts.test.js +✅ scripts/__tests__/fixtures/context-generation/skills/ +✏️ scripts/skill-eval/cases.json +✏️ plugins/aidd-context/README.md +``` + +## User Journey + +```mermaid +flowchart TD + A[Demande et cibles confirmees] --> B[Preflight du contrat] + B --> C[Resultat de la phase] + C --> D[Verification observable et documentation] +``` + +## Test Scope + +```mermaid +--- +title: Test scope +--- +journey + section Setup + Preparer fixture et baseline => projet isole: 5: system + section Happy path + Cible native confirmée => skill découvert puis action utilisée: 5: cli + Relancer action identique => resultat stable: 5: cli + section Edge case - refus + Cible portable non confirmée ou collision utilisateur => aucune copie implicite: 1: cli + section Teardown + Nettoyer projet et processus => baseline restauree: 5: system +``` + +## Tasks to do + +### `1)` Choisir la surface + +> Statut : done. Preuves : [rapport phase 3](phase-3-report.md). + +1. Ajouter mêmes signaux Kilo ; capter cible native .kilo/skills/name ou portable .agents/skills/name seulement sur choix explicite. Dédupliquer destinations partagées et collisions de noms ; ne jamais écrire les deux arbres pour le même skill. En cas de copies déjà présentes, demander une résolution avant écriture, sans supprimer une copie utilisateur. + +### `2)` Rendre et préserver + +> Statut : done. Preuves : [rapport phase 3](phase-3-report.md). + +1. Garder skill-template.md Claude inchangé. Rendu Kilo Agent Skills avec name égal au dossier, description et champs supportés seulement ; retirer argument-hint propre à Claude des sorties Kilo selon contrat. Préflight de toutes cibles et références avant écriture ; préserver actions/assets utilisateur lors d’une mise à jour. Réexécution conserve contenu identique, pas régénération aléatoire. + +### `3)` Vérifier les parcours + +> Statut : done. Preuves : [rapport phase 3](phase-3-report.md). + +1. Fixtures sorties Claude/Codex/OpenCode et Kilo, générées réellement via skill Codex ; vérification YAML/arbre/liens et collisions sans mutation. Catalogue réel kilo debug skill puis invocation gratuite du skill et lecture d’une action avec résultat observable. Tests et sources/date au même changement ; aucun lien vers un skill frère. + +## Test acceptance criteria + +| Task | Acceptance criteria | +| --- | --- | +| 1 | Kilo-only produit uniquement .kilo/skills/name avec name/description valides. | +| 1 | Portable sans accord explicite ne produit jamais .agents/skills ; accord portable produit exactement une copie. | +| 2 | Mise à jour préserve user assets ; conflit/refus laisse tous les arbres inchangés ; rerun identique. | +| 3 | Les formats et champs des sorties Claude/Codex/OpenCode restent conformes ; Kilo découvre et utilise le skill livré. | diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/phase-4-report.md b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/phase-4-report.md new file mode 100644 index 000000000..4cf1ad435 --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/phase-4-report.md @@ -0,0 +1,21 @@ +# #914 — Rapport phase 4 + +Phase 4 implémentée dans le périmètre autorisé. Plan global reste `in-progress`; phases 2, 5 et 6 restent pending. Aucun writer #979, push, PR distante ou changement de branche. + +## Livré + +Les générateurs agents et commandes reconnaissent les six signaux Kilo. Ils produisent respectivement `.kilo/agents/.md` et `.kilo/commands/.md`; legacy `.kilocode/` détecte Kilo mais ne reçoit pas de nouvelle sortie. L’agent utilise son nom de fichier, `description`, `mode: subagent` et seulement les options demandées. Le workflow émet seulement `description`, `agent`, `model`, `variant` et `subtask` demandés. Aucun champ Claude n’est injecté et Codex commands reste un skip explicite. + +Les actions imposent un préflight sur toutes les cibles avant création : chemins hors workspace, symlinks, collisions, fichiers non inscriptibles, champs inconnus ou état modifié sont refusés sans mutation. Les écritures identiques sont sautées, préservant bytes et mtime. C’est un contrat interprété, pas une transaction face à crash ou concurrence. + +## Critères couverts + +- AC10 validé pour le contrat agent Kilo et sa découverte runtime Linux. +- AC11 validé pour le contrat workflow Kilo et son invocation runtime Linux. +- AC1–4, AC13–16 progressent mais restent partiels; AC5 et AC9 restent validés par phases précédentes; AC6–8 et AC12 restent non implémentés. + +## Validation et runtime + +Le corpus ciblé passe 5/5. Les artefacts générés via les actions AIDD, leurs hashes et le préflight YAML sont enregistrés dans [evidence/phase-4](evidence/phase-4/README.md). Après confirmation que le sandbox bloquait la résolution réseau, un rejeu autorisé hors sandbox a réussi sous Kilo 7.8.8 Linux et modèle explicitement gratuit. Le JSONL prouve workflow → sous-agent, lecture du chemin payload, réponse exacte `AGENT_APPLIED:willow-5836`, coût nul et hashes avant/après identiques. Les deux échecs DNS précédents restent archivés séparément; ils ne sont pas présentés comme des passes. Voir assertions et [runtime](phase-4-runtime.md). + +Claude authentifié, runtime OpenCode, macOS et Windows Kilo restent non exécutés. Les tests hors ligne ne deviennent pas une preuve runtime. La phase s’arrête ici, en attente d’autorisation utilisateur. diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/phase-4-runtime.md b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/phase-4-runtime.md new file mode 100644 index 000000000..011495cd2 --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/phase-4-runtime.md @@ -0,0 +1,34 @@ +# #914 — Runtime phase 4 + +## Reproduction and successful run + +The evidence was produced with the existing replay script, executed outside the Codex sandbox after explicit user authorization: + +```sh +bash aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/phase-4/replay-kilo.sh attempt-04-outside-sandbox +``` + +The script copies the caller-generated fixture to a fresh `/tmp` project, assigns fresh temporary `XDG_CONFIG_HOME`, `XDG_DATA_HOME`, `XDG_STATE_HOME` and `XDG_CACHE_HOME`, and removes both temporary roots on exit. Kilo 7.8.8 ran with `--pure`; the workflow explicitly selects `kilo/cohere/north-mini-code:free`, with no fallback. Catalog metadata labels the model free and reports zero input/output/cache cost. + +The original `attempt-04-outside-sandbox/` run recorded command outputs, stderr, exit codes, sanitized JSONL and session exports. This cleanup retains its successful workflow JSONL, workflow exit code, selected free-model output, and artifact hashes before and after the run; other command outputs and session exports were removed. The archived report recorded exit code 0 for Kilo version, model catalog, agent debug, main workflow, and both exports. The retained JSONL proves: + +- `verify-workflow` invoked task `verify-agent`; task status completed and the subtask result was `AGENT_APPLIED:willow-5836`. +- A completed `read` event accessed `$RUNTIME_PROJECT/.kilo/agents/assets/agent-payload.txt`; its observed content was `AGENT_APPLIED:willow-5836`. +- The final response contained that exact payload. All recorded Kilo step costs were 0. +- Artifact hashes before and after the runtime match the generation receipt for agent, payload and workflow; Kilo left all three unchanged. + +The transcript sanitizer removes session identifiers, workspace paths and model reasoning while retaining the task result, read path/content, final response and statuses. The session exports that formerly corroborated the `verify-agent` session/read event were removed; their absence does not invalidate or extend the retained JSONL's scope. + +## DNS diagnosis + +Two earlier sandbox runs historically reported `getaddrinfo ETIMEOUT api.kilo.ai`, exit 1. The same `getent` and `curl` probes reportedly failed inside the sandbox but succeeded outside it: DNS resolved, TLS verification passed and the endpoint returned HTTP 307. Their detailed diagnostic files were removed. The separately retained successful runtime trace is not used to recast those failures as passes. No network or global configuration was changed. + +## Generation and artifact provenance + +`evidence/phase-4/caller-receipt.json` records generation by the current Codex caller following the installed `aidd-context` router and phase-4 capture/write/validate actions at HEAD `19c81c4f`. Inputs are explicitly synthetic, not interactive user answers. `generation-validation.json` records YAML, canonical path, allowed fields and body validation. `source-contract-hashes.txt` records the candidate sources; `generated-artifact-hashes.txt` records the generated artifacts. + +`generated-project/` is the exact generated fixture: `.kilo/agents/verify-agent.md`, its bundled payload, and `.kilo/commands/verify-workflow.md`. The payload SHA-256 is `55630dd7c27452fb05892541f2cf1c2f0748853417b1bbe00c5e4345243bad6e`. The large full model catalog is not duplicated; the selected free entry is preserved as filtered stdout. See the [evidence inventory](evidence/phase-4/README.md). + +## Scope and limits + +No authenticated Claude runtime, OpenCode runtime, or Kilo runtime on macOS/Windows is claimed. Offline regressions are not substitutes for those runtimes. This evidence validates phase 4's agent/workflow Kilo runtime on Linux only; it does not close cross-platform AC15. diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/phase-4.md b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/phase-4.md new file mode 100644 index 000000000..943af43f0 --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/phase-4.md @@ -0,0 +1,84 @@ +--- +status: done +--- + +# Instruction: Générer agents et workflows natifs + +## Architecture projection + +> Arbre des fichiers finaux. ✅ créer · ✏️ modifier · aucune suppression. +> Les chemins conditionnels sont résolus à l’étape de préflight, jamais créés en doublon. + +```txt +. +✏️ plugins/aidd-context/skills/06-agent-generate/references/tool-paths.md +✏️ plugins/aidd-context/skills/06-agent-generate/references/agent-authoring.md +✏️ plugins/aidd-context/skills/06-agent-generate/actions/01-capture-agent.md +✏️ plugins/aidd-context/skills/06-agent-generate/actions/02-write-agent.md +✏️ plugins/aidd-context/skills/06-agent-generate/actions/03-validate.md +✏️ plugins/aidd-context/skills/07-command-generate/references/tool-paths.md +✏️ plugins/aidd-context/skills/07-command-generate/references/command-authoring.md +✏️ plugins/aidd-context/skills/07-command-generate/actions/01-capture-command.md +✏️ plugins/aidd-context/skills/07-command-generate/actions/02-write-command.md +✏️ plugins/aidd-context/skills/07-command-generate/actions/03-validate.md +✅ scripts/__tests__/context-agent-command-artifacts.test.js +✅ scripts/__tests__/fixtures/context-generation/agents-commands/ +✏️ scripts/skill-eval/cases.json +✏️ plugins/aidd-context/README.md +``` + +## User Journey + +```mermaid +flowchart TD + A[Demande et cibles confirmees] --> B[Preflight du contrat] + B --> C[Resultat de la phase] + C --> D[Verification observable et documentation] +``` + +## Test Scope + +```mermaid +--- +title: Test scope +--- +journey + section Setup + Preparer fixture et baseline => projet isole: 5: system + section Happy path + Agent et workflow confirmés => artefacts natifs découverts puis invoqués: 5: cli + Relancer action identique => resultat stable: 5: cli + section Edge case - refus + Champ incompatible ou fichier utilisateur existant => refus préflight: 1: cli + section Teardown + Nettoyer projet et processus => baseline restauree: 5: system +``` + +## Tasks to do + +### `1)` Agent par contrat cible + +> Statut : done. Contrat Kilo canonique, fixture et debug runtime validés. + +1. Détection Kilo commune ; fichier .kilo/agents/name.md donne le nom, description et mode subagent, options demandées model/temperature/permission sans defaults inventés. Scoper la validation name YAML actuelle au format canonique Claude ; template canonique inchangé. Capturer et vérifier types/options avant writes ; pas de droits nouveaux implicites. + +### `2)` Workflow par contrat cible + +> Statut : done. Contrat Kilo canonique, fixture et commande runtime validés. + +1. Détection Kilo ; .kilo/commands/name.md et liste description/agent/model/variant/subtask uniquement selon demande. Ne pas imposer la convention nested location Claude à Kilo ni copier les injections Claude. Commande reste one-shot ; router 03 existant peut déjà déléguer le type commande, aucune modification nécessaire au routeur pour un alias non requis par AC. + +### `3)` Préservation et preuves associées + +> Statut : done. Préflight documenté, corpus et non-régressions automatisés; limites runtime conservées. + +1. Préflight multi-cibles, collision user file, champs inconnus, échappements YAML/TOML, idempotence. Tester agents Claude, TOML Codex, OpenCode et commandes unsupported Codex explicitement. Kilo catalogue/debug, invocation sous-agent réelle et slash-command réelle avec modèle gratuit ; sources/date dans refs, docs même phase. + +## Test acceptance criteria + +| Task | Acceptance criteria | +| --- | --- | +| 1 | Nom agent Kilo vient du fichier ; frontmatter ne reçoit pas name Claude ; mode subagent et options demandées corrects. | +| 2 | Workflow Kilo au chemin canonique ; uniquement champs officiellement supportés. | +| 3 | Formats Claude/Codex/OpenCode acceptés et bodies intacts, cibles unsupported sautées explicitement. | +| 3 | Invalides ou collisions refusés sans écrire une autre cible ; relance identique ; Kilo charge et utilise agent et workflow. | diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/phase-5-report.md b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/phase-5-report.md new file mode 100644 index 000000000..4a5a5d7e9 --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/phase-5-report.md @@ -0,0 +1,22 @@ +# #914 — Phase 5 result + +## Delivered + +The hook generator now returns sourced terminal guidance for Kilo. It recommends the documented `.kilo/plugin/` path and names a Kilo event only when the plugin API proves it. The plural path named by #914 is explicitly marked unconfirmed in the current documentation. An unsupported moment gets no invented fallback. Mixed requests continue for declarative targets after preflighting all destinations; exact existing AIDD entries are skipped while user entries and duplicates remain. + +## Acceptance and evidence + +| Criterion | Result | Evidence | +| --- | --- | --- | +| Kilo-only guidance with no write | Met for caller execution | [Source contract](../../../../plugins/aidd-context/skills/08-hook-generate/references/tool-paths.md), retained mutation evidence below | +| Unproved event unsupported | Met | [Kilo evaluation cases](../../../../scripts/skill-eval/cases.json) | +| Mixed fan-out keeps other formats and no Kilo config | Met for authored sample | Retained mutation evidence below; authored fixture details were summarized before the detailed archive was removed | +| Exact rerun, user hooks and invalid final target | Met for caller replay and contract assertions | Retained mutation evidence below; rerun and validation outputs were summarized before the detailed archive was removed | + +## Validation and limits + +Targeted source-contract mutations: 7/7 killed by their expected assertions; all three mutated contract files restored byte-for-byte ([retained log](evidence/phase-5/source-contract-mutations.log)). Two initial probes exposed overly broad path and preflight assertions; the test-only guards were narrowed to the exact Kilo recommendation and Preflight clause. Final focused sweep: 4 pass, 0 fail ([retained log](evidence/phase-5/source-contract-final-sweep.log)). The two retained logs have local `$HOME` paths normalized; their recorded assertions and outcomes were not changed. Historically reported: `pnpm test:changed` exit 0; `make check` exit 0 with 639 scripts tests and staged-file gates; doc-link sweep 0 broken links in 893 files; `git diff --check` exit 0; Claude package validator printed `Validation passed` after CLI build. Detailed logs were removed during this archive cleanup, so these historical results are not directly reproducible from retained logs. + +Historically reported: the earlier full CLI suite had 6,834 passes, two Persona failures from missing `/usr/bin/expect`, and one skipped Kilo runtime test; targeted reruns passed Persona 1 and 5 with `expect` 5.45.4, while the other three Persona cases were skipped and the full suite was not rerun. The opt-in Kilo 7.7.5 reference smoke passed 1/1 after an earlier loopback `EPERM` in the sandbox; the global Kilo was 7.8.8. Detailed supporting logs were removed, so these are historical report entries, not retained replay evidence. + +The authenticated Claude skill evaluation harness was not run. The cross-host hook scenario for Claude/Codex/Cursor/Copilot was not validated because the Codex child sandbox prevented that scenario. No Kilo plugin is generated by this phase; no live Kilo plugin claim is made. Phase 6 retains its separate platform/runtime consolidation scope. diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/phase-5.md b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/phase-5.md new file mode 100644 index 000000000..1936dfd0b --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/phase-5.md @@ -0,0 +1,80 @@ +--- +status: done +--- + +# Instruction: Guider les hooks sans conversion inventée + +## Architecture projection + +> Arbre des fichiers finaux. ✅ créer · ✏️ modifier · aucune suppression. +> Les chemins conditionnels sont résolus à l’étape de préflight, jamais créés en doublon. + +```txt +. +✏️ plugins/aidd-context/skills/08-hook-generate/SKILL.md +✏️ plugins/aidd-context/skills/08-hook-generate/references/tool-paths.md +✏️ plugins/aidd-context/skills/08-hook-generate/references/hook-authoring.md +✏️ plugins/aidd-context/skills/08-hook-generate/actions/01-capture-hook.md +✏️ plugins/aidd-context/skills/08-hook-generate/actions/02-write-hook.md +✏️ plugins/aidd-context/skills/08-hook-generate/actions/03-validate.md +✅ scripts/__tests__/context-hook-generation.test.js +✅ scripts/__tests__/fixtures/context-generation/hooks/ +✏️ scripts/skill-eval/cases.json +✏️ plugins/aidd-context/README.md +``` + +## User Journey + +```mermaid +flowchart TD + A[Demande et cibles confirmees] --> B[Preflight du contrat] + B --> C[Resultat de la phase] + C --> D[Verification observable et documentation] +``` + +## Test Scope + +```mermaid +--- +title: Test scope +--- +journey + section Setup + Preparer fixture et baseline => projet isole: 5: system + section Happy path + Lifecycle demandé pour Kilo => guidance vérifiée et zéro artefact hook: 5: cli + Relancer action identique => resultat stable: 5: cli + section Edge case - refus + Lifecycle non documenté => limite explicite sans équivalent supposé: 1: cli + section Teardown + Nettoyer projet et processus => baseline restauree: 5: system +``` + +## Tasks to do + +### `1)` Terminer Kilo par guidance + +> Terminer Kilo par guidance. + +1. Ajouter détection et résultat terminal guidance avant décisions de script/config. Décrire JS/TS .kilo/plugin ou plugins, événement ou typed hook seulement prouvé ; session.created peut référencer le bridge existant. Override explicite pour Kilo de la règle de rabattre un événement absent. Aucun hooks.json Claude sous .kilo, aucun script/plugin générique produit. + +### `2)` Conserver le fan-out et les relances + +> Conserver le fan-out et les relances. + +1. Cible mixte : guidance Kilo séparée, artefact des autres outils selon contrat existant ; preflight avant tout script/config. Dédupliquer uniquement entrée AIDD exacte déjà générée, conserver doublons/hooks utilisateur. Cette correction ciblée répond à AC13 ; aucun refactor d’un moteur de hooks ni modification bridge #971. + +### `3)` Démontrer le résultat + +> Démontrer le résultat. + +1. Parcours Kilo-only puis mixed, événements prouvé/non prouvé, second passage. Comparer arbre complet pour guidance sans write. Tester structures hooks Claude/Codex/Cursor/Copilot, guidance OpenCode conservée, body utilisateur et idempotence. Sources/date et explication limites dans références et README. + +## Test acceptance criteria + +| Task | Acceptance criteria | +| --- | --- | +| 1 | Kilo-only renvoie guidance sourcée précise et aucune écriture. | +| 1 | Événement non prouvé déclaré unsupported, aucune correspondance inventée. | +| 2 | Fan-out mixte conserve les contrats des outils supportés et n’ajoute pas de config hooks Kilo. | +| 3 | Deux passages identiques ne dupliquent pas l’entrée AIDD ; hooks utilisateur intacts ; une entrée invalide bloque avant script/config. | diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/phase-6-report.md b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/phase-6-report.md new file mode 100644 index 000000000..fc9949728 --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/phase-6-report.md @@ -0,0 +1,51 @@ +# Phase 6: current verification + +The commands and outcomes below are the historical phase 6 report. Their detailed test/runtime logs were removed during archive cleanup, except for the explicitly retained phase 4 and phase 5 evidence. They were not rerun during cleanup and are not directly reproducible from this report alone. + +Status: **in-progress**. Verification performed on Linux on 12 October 2026. This report records only commands actually run during this phase 6 continuation. Existing phase 1–5 runtime reports retain their original scope and dates. No session export or provider transcript was added. + +## Three evidence levels + +| Level | Result | Evidence and limit | +| --- | --- | --- | +| Generation by an agent using AIDD skills | **Verified for the authorized live case** | One approved Codex CLI 0.162.1 `codex exec` completed with exit 0. It generated the native Kilo router, verify action, and bundled synthetic payload under the isolated workspace; the case assertions passed. The invocation explicitly requested `xeval-kilo-skill`; no raw session transcript was retained. | +| Kilo discovery and loading | **Verified for the authorized phase-6 skill case** | Kilo 7.8.8 discovered exactly one `verify-payload` at `.kilo/skills/verify-payload/SKILL.md`. The focused runtime test invoked Kilo's actual `skill` tool; the local provider derived its argument field from the observed Kilo tool schema, and Kilo returned the hash-pinned generated router. Kilo then executed `read` for the generated action. | +| Effective use of content | **Verified for the authorized phase-6 skill case** | Kilo returned the generated action through `read`, then the unique synthetic payload through a second `read`. The local provider constructed its final answer from the payload read result, and the CLI output matched it. The provider is deterministic and local: this proves Kilo's native loading and tool execution, not an external model's autonomous choice to invoke the skill. Hook generation produces guidance, not a Kilo plugin. | + +## Phase 6 acceptance criteria + +| Criterion from `phase-6.md` | Status | Remaining proof | +| --- | --- | --- | +| Skills generate the exact outputs that real Kilo discovers and uses | **Partial overall; phase-6 native skill path verified** | The one authorized Codex agent run generated the three phase-6 files with recorded hashes. The focused Kilo 7.8.8 test re-created byte-identical router/action files by those hashes, substituted only a unique payload in the temporary workspace, and verified discovery, router loading, action read, payload read, and a response derived from the read result. The broader all-surface generation/runtime journey remains incomplete; earlier phase receipts retain their own scope. | +| Positive and negative controls, second pass, and refusal | **Partial** | The native E2E has positive/negative Kilo configuration and catalogue checks plus a captured positive/negative rule request. Earlier phase tests cover idempotence and refusal for each generator. A single live all-generator rerun/refusal sequence was not executed. | +| Offline Claude and automated multi-tool regressions pass; no authenticated Claude claim | **Met for the executed offline controls; authenticated Claude remains blocked** | The scripts and CLI suites pass, as do the Claude package validator, lint, typecheck, and knip. `claude auth status` reports `loggedIn: false`; no authenticated Claude session ran. OpenCode was absent from `PATH`; Cursor and Copilot IDE runtime was not run. | +| Report states OS, versions, skipped/blocked cases and AC15 platform limit | **Met as reporting; AC15 remains partial** | Kilo 7.8.8 native E2E and isolated 7.7.5 smoke pass on Linux. No macOS or Windows Kilo run occurred. The added CI command remains Ubuntu-only and has not itself produced a CI result. | + +## Commands and results + +- `node scripts/check-tests-leave-git-alone.js -- node --test 'scripts/__tests__/**/*.test.js'`: 800 passed, 0 failed, 0 skipped. Run with access to the test wrapper's Git fixture; the initial sandbox attempt could not resolve `.git/hooks`. +- `pnpm --dir cli test`: 6,842 passed, 2 skipped, 0 failed. Run with fixture Git and loopback access. This full run preceded extraction of the local-model helper; both affected Kilo tests were rerun successfully afterward. The opt-in Kilo tests are among the skips in the full run and are listed separately below. +- `pnpm --dir cli test:e2e:kilo:native`: 1 passed on Linux, Kilo 7.8.8, with isolated XDG directories and a deterministic local provider. +- `PATH=:$PATH pnpm --dir cli test:e2e:kilo`: 1 passed on Linux with an isolated Kilo 7.7.5 installation. An earlier attempt with Kilo 7.8.8 failed its exact-version assertion; an earlier sandbox attempt could not listen on loopback (`EPERM`). Neither failure is a product regression. +- `pnpm --dir cli typecheck`, `pnpm --dir cli knip`, `node scripts/check-claude-accepts-build.cjs`, and `git diff --check`: passed. The validator printed `Validation passed`. +- `pnpm --dir cli lint`: passed after formatting the touched E2E files; one existing warning remains in `uninstall-use-case.ts` for an unused private constructor member. +- `node --test scripts/__tests__/context-generation-evidence.test.js scripts/__tests__/model-selection.test.js`: passed after the final runner change (2 files; the model-selection file contains four cases). +- `node scripts/check-markdown-links.js`: 0 broken links in 957 files. +- Isolated approved `codex exec` case, Codex CLI 0.162.0, model `gpt-6-sol`: **blocked, no artifact generated**. The decisive error was `failed to spawn code-mode host ... codex-code-mode-host: No such file or directory`. The command used `workspace-write` and `--ephemeral`, without a bypass flag. Codex reported effective `approval: never` from its existing configuration; that setting was not changed. Codex made its own single retry; after its final refusal, no further attempt was made. +- Follow-up live case, standalone Codex CLI **0.162.1**: `codex login status` exited 0 and reported `Logged in using ChatGPT`. The CLI and its executable `codex-code-mode-host` sibling resolved from the same standalone release directory; `code_mode_host`, `shell_tool`, `skill_search`, and `unified_exec` are enabled. The single `codex exec` invocation used `workspace-write`, `--ephemeral`, `--skip-git-repo-check`, and `approval_policy="on-request"`, with no escalation or bypass option. It failed before agent/session startup: `failed to open memories db` followed by `failed to initialize in-process app-server client: Read-only file system (os error 30)`. No model request, skill discovery, or generation occurred. The temporary input contained exactly 14 skill files plus `kilo.json`; it was removed after confirming there was no output or `.kilo` artifact. No raw session trace was retained. +- One authorized follow-up after that failure used Codex CLI **0.162.1**, the same executable sibling host, `CODEX_SQLITE_HOME` in a separate writable `/tmp` directory, `--ignore-user-config`, `workspace-write`, `--ephemeral`, and `approval_policy="on-request"`. It made one `codex exec` invocation and no retry. SQLite files were created in the temporary state directory, but the command ended with `failed to initialize in-process app-server client: Read-only file system (os error 30)`; this run did not report the failing path. Read-only inspection of the binary found a diagnostic string for `$CODEX_SQLITE_HOME` being overridden by a required `sqlite_home` value, but that diagnostic did not appear in the run output, so its applicability is unconfirmed. No sandbox or approval policy was changed. The invocation stopped before session/authentication, skill loading, or model request. The worktree contained 14 skill files, `kilo.json`, and one synthetic `assets/payload.txt`; no other repository files. No Kilo or portable artifact and no final response were produced. Both temporary directories were removed; no raw trace or credentials were retained. +- Authorized one-time parent permission escalation for the same isolated case used the official `require_escalated` mechanism for the `codex exec` process only. Codex CLI **0.162.1** and its executable sibling host ran with child `workspace-write`, `--ephemeral`, and `approval_policy="on-request"`; no bypass option or persistent permission change was used. The single invocation exited 0, produced a final response, and wrote `.kilo/skills/verify-payload/SKILL.md`, `actions/01-verify.md`, and `assets/payload.txt`. Case checks passed: native files exist, portable copy is absent, frontmatter has the Kilo name/description and omits `argument-hint`/`allowed-tools`, and the bundled payload equals the synthetic input. SHA-256: router `ad472e5fa9663cc9a291525d8b9528e07d05411680a1eca9f9183dfab3185945`; action `583404c6f75ae05db061fbfbe90f0688636aba574ae832ec565282b056a7e534`; payload `e201189cdbacfb224377377efa94b45898c37abdf2145efa36a8926af772a5d7`. The isolated input held 14 skill files plus `kilo.json` and the synthetic payload; no other repository files were staged. The Codex home, tokens, credentials, and raw session trace were not copied into evidence. +- Manual terminal run of the temporary runtime script, using Kilo **7.8.8**, isolated HOME/XDG state, the existing local-model helper, and the three hash-checked synthetic artifacts: Kilo discovery found exactly one `verify-payload` at the expected native path. Discovery created `.kilo/.gitignore`; the validated temporary workspace then contained five files and remained unchanged through the run. The helper bound only to `127.0.0.1`, received two requests, and used no external model. Result fields: `routerLoaded=false`, `actionLoaded=false`, `payloadReturnedByKiloReadTool=false`, `answerExactlyMatchesPayload=true`. The latter is the helper's configured fixed answer and does not depend on a successful read. Therefore this run proves Kilo startup, discovery, and local request/response plumbing, but does not prove that Kilo loaded or used the generated skill/action/payload. No retry was made. The earlier sandboxed attempt still failed at loopback bind with `EPERM`; the manual terminal run passed that bind stage. +- The first attempt at the focused corrective test inside the managed sandbox failed at the loopback bind (`listen EPERM`) before `kilo run`. Vitest timed out because the helper did not propagate that error; the helper was corrected. A subsequent run from the Coder terminal reached Kilo and is recorded below. +- `KILO_NATIVE_GENERATION=1 pnpm --dir cli exec vitest run --project=e2e tests/e2e/kilo-native-generation.e2e.test.ts -t "loads the native skill, follows its action, and answers from the read payload"`: **1 passed, 1 skipped** on Linux with Kilo **7.8.8**. The test verified the phase-6 router and action against their recorded SHA-256 hashes, discovered the skill once, and used the `skill` schema present in Kilo's actual provider request to build the invocation. Kilo returned the router, read the action and unique payload, then emitted the payload-derived answer. In the negative run, the skill was removed; Kilo returned an explicit missing-skill result, no `read` call followed, and the payload marker was absent from the response. The provider was local on `127.0.0.1`; no external model was used. The isolated workspace inventory was exactly the three skill files, `kilo.jsonc`, and Kilo's `.kilo/.gitignore`; `external_directory` and all non-skill tools remained denied. Kilo required workspace `read` permission because the earlier exact file patterns did not match in 7.8.8; the five-file workspace and scripted provider constrain the exercised reads to the action and payload. +- A direct local smoke check of the helper's legacy positional API passed: it emitted the configured `read` call with `filePath: probe.txt`, then the configured fixed final response, on loopback. The existing Kilo 7.7.5 runtime regression suite was not rerun because its temporary 7.7.5 installation is absent in this session. This does not replace a full Kilo 7.7.5 compatibility run. + +The first full CLI run inside the sandbox was interrupted after fixture operations failed with `spawnSync git EPERM` and loopback restrictions. It is not counted as a passing run. The successful full run occurred outside that sandbox. No macOS, Windows, authenticated Claude, OpenCode, Cursor IDE, or Copilot IDE execution is inferred from these results. The Codex call used the temporary directory as its working root, added no repository directory, and staged only the authorized skill files and synthetic fixture files. Inspection found no credential patterns, personal identifiers, or historical PR evidence in the staged files or synthetic prompt. The skill's AIDD/Kilo authoring rules were within the expressly authorized skill content. No additional file was requested by Codex. + +## Remaining gates and review decision + +Phase 2 and phase 6 both remain `in-progress`. AC15 is partial because Kilo has only been exercised on Linux. The authorized live Codex generation and Kilo 7.8.8 discovery/loading/content-consumption stages pass for the single native skill case with a deterministic local provider. The former fixed-answer false positive is removed from this test. Other all-generator positive/negative, idempotence, refusal, multi-host, and platform gates retain their statuses above. `CONTRIBUTING.md` still requires Claude plus another host; Claude authentication is unavailable and no exception has been granted. **Verdict: NOT READY.** + +Draft request for the maintainer, not posted: “For PR #992, automated/offline Claude checks pass, but there is no authenticated Claude Code session. Could you run the required Claude validation against this candidate, or explicitly approve a scoped exception to the `CONTRIBUTING.md` host test requirement? Kilo 7.8.8/Linux now verifies the generated native skill's discovery, loading and payload use through a deterministic local provider. We will keep phase 2 and phase 6 in progress until that decision and the remaining runtime/platform evidence are recorded.” + +No commit, push, GitHub change, or historical evidence cleanup was performed. The pre-existing modified historical files and the two untracked phase 2 logs remain outside this phase 6 work. diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/phase-6.md b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/phase-6.md new file mode 100644 index 000000000..ab9e17306 --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/phase-6.md @@ -0,0 +1,89 @@ +--- +status: in-progress +--- + +# Instruction: Consolider les preuves de génération et de runtime + +## Architecture projection + +> Arbre des fichiers finaux. ✅ créer · ✏️ modifier · aucune suppression. +> Les chemins conditionnels sont résolus à l’étape de préflight, jamais créés en doublon. + +```txt +. +✏️ scripts/skill-eval.mjs +✏️ scripts/skill-eval/cases.json +✏️ scripts/skill-eval/README.md +✅ scripts/__tests__/context-generation-evidence.test.js +✅ cli/tests/e2e/kilo-native-generation.e2e.test.ts +✏️ cli/tests/e2e/kilo-runtime.e2e.test.ts (seulement extraction minimale de helper réutilisable si nécessaire) +✅ cli/tests/e2e/helpers/kilo-runtime.ts (seulement si extraction nécessaire) +✏️ cli/package.json (commande opt-in de test natif, sans modifier test:e2e:kilo existant) +✏️ .github/workflows/cli-ci.yml (étendre le job Kilo existant, conserver son identité de gate) +✅ aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/evidence/ +✏️ plugins/aidd-context/README.md +``` + +## User Journey + +```mermaid +flowchart TD + A[Demande et cibles confirmees] --> B[Preflight du contrat] + B --> C[Resultat de la phase] + C --> D[Verification observable et documentation] +``` + +## Test Scope + +```mermaid +--- +title: Test scope +--- +journey + section Setup + Preparer fixture et baseline => projet isole: 5: system + section Happy path + Sorties réelles et versions figées => preuves par critère et plateforme: 5: cli + Relancer action identique => resultat stable: 5: cli + section Edge case - refus + Runtime absent ou quota gratuit épuisé => statut non validé sans fallback payant: 1: cli + section Teardown + Nettoyer projet et processus => baseline restauree: 5: system +``` + +## Tasks to do + +### `1)` Runner réel ciblé + +> Runner réel ciblé. + +1. Réutiliser les fixtures et les oracles établis dans phases 1 à 5. Extension minimale du runner existant : choix explicite host Codex pour produire aussi des artefacts Claude, conserver parcours Claude existant, pas de --judge Claude obligatoire. Si un lancement séparé est nécessaire pour l’isolation Codex, l’isoler dans le runner, sans réinventer une orchestration. Lire docs/flags host avant code. Transcript des choix ambigus et de l’accord portable ; skips distinguent impossibilité de pass. + +### `2)` Tests runtime sur sorties réelles + +> Tests runtime sur sorties réelles. + +1. Construire le test natif Kilo à partir des sorties enregistrées d’une exécution de skill livrée, avec hashes/provenance, pas de fixtures manuellement fabriquées présentées comme outputs. Réutiliser helpers #971 après lecture, backend local capturant payloads et usage du modèle gratuit séparé. Chaque artefact a positif/négatif ; guidance hooks validée comme absence d’artefact, bridge existant séparé. Kill processus, ports et nettoyer temp dirs. + +### `3)` Plateformes et régressions finales + +> Plateformes et régressions finales. + +1. Le job cli-kilo-runtime actuel tourne seulement sur Ubuntu avec Kilo 7.7.5 ; le job Windows ne constitue pas une preuve Kilo. Vérifier les plateformes réellement supportées par Kilo/AIDD et les contraintes des runners avant d’adapter de façon ciblée le job Kilo existant aux plateformes justifiées ; aucune matrice élargie présumée. Figer la version retenue et utiliser la suite locale déterministe sans compte. Conserver le smoke existant et son pin tant que son passage sur nouvelle version n’est pas établi. Usage gratuit réel local en complément ; CI sans modèle externe sert la preuve de découverte/chargement. Prévoir les sorties réelles enregistrées/hashées comme inputs CI, aucun générateur LLM supposé gratuit/authentifié en CI. Aucune preuve multi-OS déduite de Linux. Exécuter suites root/CLI pertinentes, golden et validateur offline Claude, mutations ciblées. Relire les changements générés par make check. OpenCode runtime supplémentaire seulement dans environnement déjà équipé. Résultat global reste incomplet si AC15 plateforme non exécuté. + +### `4)` Rapport et limites de reprise + +> Rapport et limites de reprise. + +1. Renseigner la matrice par preuves, pas cocher selon présence fichier. Documenter offline Claude vs runtime authentifié indisponible, hôtes/versions réellement exécutés et #979 retenue. Review/assertion AIDD après implémentation dans session autorisée. Aucun commit/push/PR automatique dans le runner. + +## Test acceptance criteria + +| Task | Acceptance criteria | +| --- | --- | +| 1 | Traces démontrent que les skills livrés ont généré les sorties testées et que le vrai Kilo les découvre/utilise. | +| 2 | Contrôles négatifs détectent artefact absent/non relié ; second passage et refus laissent les états attendus. | +| 3 | Tous tests offline Claude et régressions automatisées des cibles passent ; aucun runtime Claude authentifié prétendu. | +| 4 | Rapport décrit OS/versions, cas skipped/blocked et limites ; AC15 ne passe que sur la couverture runtime prévue réellement exécutée. | + +Résultats de la reprise : [rapport de phase 6](phase-6-report.md). Le statut reste `in-progress` tant que les validations obligatoires manquent. diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/plan-adjustments.md b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/plan-adjustments.md new file mode 100644 index 000000000..25f30667f --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/plan-adjustments.md @@ -0,0 +1,7 @@ +# Ajustements avant phase 1 + +Plan général approuvé ; phase 1 uniquement autorisée. Commit local par tâche validée autorisé ensuite, sans push/PR/changement de branche. Le snapshot de plan antérieur a été retiré comme doublon après conservation de ses décisions utiles dans plan.md et ce résumé ; planning-validation.md et challenge initial restent des preuves historiques. + +Déjà présents : réutilisation writer #979, indépendance des autres phases, JSONC sans perte, preflight multi-fichiers, limites crash, tests/non-régression par phase, phase 6 de consolidation. Précisions réellement ajoutées : progression et preuves obligatoires par tâche dans plan.md ; décision explicite si #979 reste Draft dans phase-2.md et overlap-979.md ; pas de moteur transactionnel générique en phase 2 ; support plateforme et contraintes runner à vérifier avant extension CI dans phase-6.md/test-strategy.md. Pas de changement des exigences des phases 3/4/5 ni des seize critères officiels. + +Le challenge historique porte sur le plan et n’est pas une preuve de l’implémentation. Modifications de clarification, sans changement substantiel de solution : aucun rerun complet de planification nécessaire. La source actuelle et les limites d’autorisation sont résumées dans [user-request.md](user-request.md). diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/plan.md b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/plan.md new file mode 100644 index 000000000..9f182d6b1 --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/plan.md @@ -0,0 +1,58 @@ +--- +objective: "Un projet Kilo peut utiliser les artefacts natifs générés par AIDD selon les seize critères de #914, sans perte de contenu utilisateur ni régression des autres cibles." +status: in-progress +--- + +# Plan: Génération native Kilo dans aidd-context + +## Overview + +| Field | Value | +| --- | --- | +| **Goal** | Détection et mémoire Kilo, règles liées à la config, skills, agents, workflows et guidance hooks avec preuves de conservation et runtime. | +| **Source** | [Issue #914](https://github.com/ai-driven-dev/framework/issues/914), [demande conservée](./user-request.md), instructions actualisées du 10 octobre 2026 et [gather](./gather.md). | + +## Phases + +| # | Phase | File | +| --- | --- | --- | +| 1 | Reconnaissance et mémoire partagée | [phase-1.md](./phase-1.md) | +| 2 | Publication des règles et configuration sans perte | [phase-2.md](./phase-2.md) | +| 3 | Skills natifs ou portables choisis | [phase-3.md](./phase-3.md) | +| 4 | Agents et workflows natifs | [phase-4.md](./phase-4.md) | +| 5 | Guidance hooks et relances sûres | [phase-5.md](./phase-5.md) | +| 6 | Preuves de génération et runtime sur plateformes supportées | [phase-6.md](./phase-6.md) | + +Plan général validé avec les précisions de la demande phase 1. Phase 1 clôturée par l’utilisateur avec ses limites conservées. Les demandes du 10 octobre 2026 autorisent séparément les phases 4 et 5. Le 12 octobre, l’utilisateur a autorisé la reprise de la seule phase 2 après fusion de #979 dans `next`, y compris l’extension du writer et la publication règle + configuration. Les critères techniques de phase 2 sont vérifiés ; son statut reste `in-progress` car le test Claude demandé par `CONTRIBUTING.md` n’est pas documenté et aucune exception du mainteneur n’est disponible. Le statut global reste `in-progress`. Aucun writer concurrent. + +Chaque tâche suit la même progression : test pertinent rouge avant changement lorsque possible, implémentation minimale, documentation associée, validations automatisées et runtime pertinent, preuves conservées puis statut réel actualisé. Les régressions des cibles concernées se vérifient dès cette tâche. Preuve obligatoire manquante => tâche non entièrement validée. La phase 6 consolide, sans remplacer ces vérifications. Commits locaux autorisés pour la tranche indépendante ; aucun push, PR ou changement de branche autorisé. + +Documents de cadrage conservés : [matrice des 16 critères](./coverage-matrix.md), [stratégie de tests](./test-strategy.md), [contrats officiels](./official-sources.md), [chevauchement #979](./overlap-979.md), [preuves d’environnement](./environment-evidence.md) et [challenge](./challenge.md). Les notes d’exploration détaillées ont été retirées après synthèse dans ces documents. + +## Resources + +| Source | Verified | +| --- | --- | +| https://github.com/ai-driven-dev/framework/issues/914 | Corps courant et seize critères, date contractuelle 2026-09-25. | +| https://github.com/ai-driven-dev/framework/pull/971 | Fusionnée dans next et présente dans HEAD ; bridge déjà étendu. | +| https://github.com/ai-driven-dev/framework/pull/979 | Fusionnée dans `next` en `c827990f` ; writer autonome étendu en phase 2 pour Kilo. | +| https://kilo.ai/docs/customize/agents-md | Surface mémoire et approbation de modification. | +| https://kilo.ai/docs/customize/custom-rules | Règles config-backed. | +| https://kilo.ai/docs/customize/skills | Agent Skills et chemins compatibles. | +| https://kilo.ai/docs/customize/custom-subagents | Agents Markdown, nom fichier et champs supportés. | +| https://kilo.ai/docs/customize/workflows | Commandes Markdown et options. | +| https://kilo.ai/docs/automate/extending/plugins | Plugins JS/TS et événements prouvés. | +| https://kilo.ai/docs/getting-started/settings | Fusion des configs et priorité .kilo. | +| https://github.com/Kilo-Org/kilocode/blob/main/packages/opencode/src/kilocode/skills/kilo-config.md | Legacy et chemins compatibles. | + +## Decisions + +| Decision | Why | +| --- | --- | +| Production Kilo dans aidd-context, aucun nouveau profil/distributeur CLI | #744 livré, #868 hors scope ; préserver les frontières d’architecture. | +| Sources/templates Claude conservés ; branches de rendu et validation par cible | Évite d’affaiblir les contrats canoniques pour ajouter Kilo. | +| Extension du writer règles #979 après alignement de base | Évite deux mécanismes concurrents de publication et protège les sorties Codex/OpenCode corrigées. | +| JSONC édité localement et publication config/règle prévalidée ensemble | Préserver octets utilisateur et éviter artefacts non découvrables après erreur. | +| Mémoire partagée AGENTS.md dédupliquée ; portable skills uniquement sur choix | Contrat explicite #914 et absence de copies concurrentes. | +| Hooks Kilo donnent guidance terminale sourcée ; bridge existant seulement dans sa portée prouvée | Aucun hook déclaratif ou runtime plugin générique inventé. | +| Artefacts Claude offline + génération réelle via Codex ; runtime Claude authentifié déclaré indisponible | Couverture rigoureuse sans achat Anthropic ni fausse équivalence de preuves. | diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/planning-validation.md b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/planning-validation.md new file mode 100644 index 000000000..2ed3918e1 --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/planning-validation.md @@ -0,0 +1,14 @@ +# Vérification des livrables de planification + +Validation effectuée le 10 octobre 2026 par assertions Python sur les artefacts écrits : + +- Seize AC comparés verbatim au corps officiel du snapshot distant, exactement une ligne par numéro 1 à 16. +- Plan et six phases présents, status pending, ordre exact des sections des templates, aucune variable de template laissée dans les phases. +- Chaque phase contient projection, parcours, scope de tests avec Setup/Happy path/Teardown, tâches et comportements d’acceptation observables. +- Challenge : ordre exact des trois sections, pourcentage 90 %, Deal breakers vide conformément au rubric. +- Le premier run de plan avait produit un artefact de liaison vers ai-driven-dev/framework#914 (auteur : aidd-dev:01-plan). Cet artefact a été retiré ; le lien d’issue reste dans issue-914.md. +- git diff --name-only vide pour les fichiers suivis ; git branch --show-current donne feat/kilo-native-generation-914. Seuls documents du dossier de tâche non suivis. + +Commande de vérification des liens : `node scripts/check-markdown-links.js aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914`, zéro lien cassé. Contrôle de planification seulement ; aucun test fonctionnel ni génération native #914 exécuté. Aucun résultat de régression Claude/Kilo/Codex/OpenCode revendiqué. + +La projection et les critères ont été relus dans le caller avant challenge ; le score de revue du plan a été présenté en chat, sans être inscrit dans plan.md. Le marker step-end est émis seulement après production et vérification du plan/phases. diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/remote-state.json b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/remote-state.json new file mode 100644 index 000000000..ba0d8ac23 --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/remote-state.json @@ -0,0 +1,310 @@ +{ + "verified_at": "2026-10-10", + "issue": { + "issue": { + "url": "https://github.com/ai-driven-dev/framework/issues/914", + "title": "feat(aidd-context): add Kilo-native generation targets", + "issue_number": 914, + "body": "> Summary: Make every applicable aidd-context workflow detect Kilo and emit artifacts through Kilo's documented native or config-backed surfaces, without overwriting user-owned context or inventing unsupported declarative hooks.\n\n## Outcome\n\nA Kilo-only project can onboard AIDD, wire project memory, and generate rules, skills, agents, and workflows that Kilo discovers without manual file moves or configuration repair.\n\n## Problem\n\nThe CLI supports Kilo through #744, but aidd-context still omits Kilo from onboarding, project-memory wiring, and the skill, rule, agent, command, and hook generation contracts.\n\nA Kilo-only project is therefore proposed inconsistently. Some generators lack a Kilo target; others do not document the configuration required to make the generated artifact discoverable.\n\n## Official Kilo contract\n\nVerified on 2026-09-25.\n\n- Project memory: AGENTS.md at the project root is loaded automatically. AGENT.md is a fallback, and per-directory AGENTS.md files are loaded when Kilo reads files beneath them:\n https://kilo.ai/docs/customize/agents-md\n- Rules: Markdown files such as .kilo/rules/.md are loaded through the instructions array in project configuration. Entry order is significant:\n https://kilo.ai/docs/customize/custom-rules\n- Skills: Kilo implements Agent Skills and discovers project skills from .kilo/skills/ and compatibility locations including .agents/skills/:\n https://kilo.ai/docs/customize/skills\n- Agents: project Markdown agents live under .kilo/agents/. The filename defines the name; YAML frontmatter can define description, mode, model, temperature, and permissions:\n https://kilo.ai/docs/customize/custom-subagents\n- Workflows: project workflows are Markdown slash commands under .kilo/commands/:\n https://kilo.ai/docs/customize/workflows\n- Plugins and hooks: JavaScript or TypeScript modules live under .kilo/plugin/ or .kilo/plugins/ and expose typed hooks or the event bus, including session.created:\n https://kilo.ai/docs/automate/extending/plugins\n- Project configuration: Kilo reads kilo.json and kilo.jsonc at the project root or under .kilo/. The .kilo/ form has higher priority, and loaded configurations are deep-merged:\n https://kilo.ai/docs/getting-started/settings\n- Legacy compatibility: .kilocode/ remains a recognized fallback, while .kilo/ is canonical for new artifacts:\n https://github.com/Kilo-Org/kilocode/blob/main/packages/opencode/src/kilocode/skills/kilo-config.md\n\n## Detection and configuration decision\n\nDetect Kilo from these unambiguous signals:\n\n- .kilo/\n- .kilocode/ as a legacy detection signal only\n- kilo.json or kilo.jsonc at the project root\n- .kilo/kilo.json or .kilo/kilo.jsonc\n\nDo not detect Kilo from opencode.json[c] alone because that signal is shared with OpenCode legacy compatibility.\n\nGenerate all new Kilo-specific artifacts under canonical .kilo/ paths. Never generate new artifacts under .kilocode/.\n\nWhen a generator must update instructions:\n\n1. If no project Kilo config exists, create .kilo/kilo.jsonc.\n2. If exactly one project Kilo config exists, update it.\n3. If several project configs exist and one already owns the AIDD instruction entry, update that file idempotently.\n4. Otherwise present the existing candidates in Kilo precedence order and let the user choose which file to edit.\n5. Preserve every existing instructions entry, duplicate, order, JSONC comment, trailing comma, and unrelated formatting.\n6. If a safe edit cannot be made, stop before writing and return an actionable error.\n\nKilo deep-merges supported config files. Do not silently inherit the CLI's stricter dual-config refusal as if it were a Kilo runtime constraint.\n\n## Generation targets\n\n### Onboarding and project memory\n\n- Propose Kilo when any canonical or legacy Kilo signal is detected.\n- Treat root AGENTS.md as Kilo's primary project-memory surface.\n- Preserve user-authored content and update only the AIDD-owned memory block.\n- Deduplicate AGENTS.md when another selected tool shares the same target.\n\n### Rules\n\n- Write .kilo/rules//.md.\n- Add that exact relative path to the selected config's instructions array.\n- Describe the directory as config-backed, not auto-discovered.\n- Preserve instruction order because Kilo loads entries in declared order.\n\n### Skills\n\n- Generate Agent Skills format with a SKILL.md whose name matches its parent directory.\n- Use .kilo/skills// for a Kilo-only target.\n- Offer .agents/skills// only when the user explicitly selects a portable shared target.\n- Never duplicate one skill into both locations during the same generation.\n\n### Agents\n\n- Write .kilo/agents/.md.\n- Use the filename as the agent name.\n- Emit documented YAML frontmatter with description and mode: subagent; preserve supported optional model, temperature, and permission fields when requested.\n\n### Workflows\n\n- Write .kilo/commands/.md.\n- Preserve documented optional frontmatter such as description, agent, model, variant, and subtask.\n\n### Hooks\n\n- Do not emit Claude-style declarative hooks for Kilo.\n- Explain that Kilo uses JavaScript or TypeScript plugins under .kilo/plugin/ or .kilo/plugins/.\n- Name the documented event or typed hook that corresponds to the requested lifecycle only when the mapping is proven.\n- Do not invent mappings for unsupported Claude hook events.\n- Generic runtime plugin implementation remains out of scope. The existing CLI bridge may be referenced only for its captured session.created mapping.\n\n## Acceptance criteria\n\n- [ ] A Kilo-only fixture is detected by onboarding and every applicable generator.\n- [ ] Detection covers .kilo/, root kilo.json[c], .kilo/kilo.json[c], and legacy .kilocode/.\n- [ ] Legacy .kilocode/ detection still produces new artifacts under canonical .kilo/ paths.\n- [ ] opencode.json[c] alone does not produce a false Kilo detection.\n- [ ] AGENTS.md generation preserves user content and deduplicates a shared target.\n- [ ] Rule generation writes .kilo/rules//.md and wires the exact path through instructions.\n- [ ] Config selection respects Kilo's precedence and asks before choosing among ambiguous existing files.\n- [ ] JSON and JSONC updates preserve existing entries, duplicates, order, comments, trailing commas, and unrelated formatting.\n- [ ] Skill placement requires an explicit choice before using .agents/skills/.\n- [ ] Agent paths and frontmatter match the documented Markdown-agent contract.\n- [ ] Workflows are generated under .kilo/commands/ with supported frontmatter only.\n- [ ] Hook generation returns precise plugin guidance and never emits an unsupported declarative hook.\n- [ ] Re-running every generator is idempotent.\n- [ ] Invalid or unsafe configuration causes no partial write.\n- [ ] Runtime-oriented tests prove Kilo discovers each emitted artifact on supported platforms.\n- [ ] Every Kilo path claim carries an official source and the 2026-09-25 verification date.\n\n## Repository evidence\n\n- CLI Kilo profile: cli/src/contexts/tools/domain/profiles/kilo/profile.ts\n- Current config resolver: cli/src/contexts/tools/domain/profiles/kilo/kilo-paths.ts\n- Existing captured SessionStart bridge: cli/src/contexts/tools/domain/profiles/kilo/kilo-hooks-bridge.ts\n- aidd-context generators: plugins/aidd-context/skills/02-project-init and 04-skill-generate through 08-hook-generate\n\n## Relations and boundaries\n\n- #744 delivered the CLI Kilo profile.\n- #868 owns Kilo flat rule distribution.\n- #790 remains closed; this issue does not restore a generic session-start bridge.\n- Supersedes the Kilo portion of #788.\n- Flat archive emission, arbitrary hook conversion, and runtime plugin implementation are out of scope.\n", + "state": "open", + "user": { + "login": "blafourcade", + "name": "blafourcade", + "email": null, + "avatar_url": "https://avatars.githubusercontent.com/u/119650761?v=4", + "id": 119650761 + }, + "assignees": null, + "labels": null, + "milestone": 19, + "state_reason": null, + "comments": 1, + "created_at": "2026-09-23T08:37:46Z", + "updated_at": "2026-10-10T09:45:34Z", + "closed_at": null, + "display_url": "https://github.com/ai-driven-dev/framework/issues/914", + "display_title": "feat(aidd-context): add Kilo-native generation targets" + }, + "url": "https://github.com/ai-driven-dev/framework/issues/914", + "title": "feat(aidd-context): add Kilo-native generation targets", + "display_url": "https://github.com/ai-driven-dev/framework/issues/914", + "display_title": "feat(aidd-context): add Kilo-native generation targets" + }, + "pr971": { + "url": "https://github.com/ai-driven-dev/framework/pull/971", + "number": 971, + "state": "closed", + "merged": true, + "mergeable": false, + "draft": false, + "body": "## What & why\nOpenCode V2 rejected context/telemetry plugins. Both now load and execute, preserving V1 >= 1.18.29. Kilo hooks now cover a complete turn.\n\n## How it works\nDefault `id/server/setup`; one shared V2 adapter outside discovery. Install/update repair missing helpers, including already-current plugins, while preserving user files. Kilo deduplicates session/tool/idle hooks. Mutation assets load as text; Windows checkout retains LF. Obsolete parser removed; thresholds and bundle budget unchanged.\n\nArchitecture follows marketplace → plugin → components; adapter details belong to CLI documentation. A Claude-only rule keeps durable documentation concise without dropping contracts or safeguards. Project memories remain unchanged.\n\n## How to verify\n- [Final-head checks](https://github.com/ai-driven-dev/framework/pull/971/checks): CLI suites, architecture, Knip, mutations, Windows, real Kilo and CodeQL.\n- Independent code/behavior/relevance review found two issues, both corrected and rechecked: same-version helper restoration and repository-relative fixture resolution.\n- Restoration regression failed before the fix; 36 targeted tests passed afterward. The compiled CLI also restored exact helper bytes without changing user configuration, installed plugin or unique ownership.\n- Real OpenCode 1.18.29/2.0.22 and Kilo 7.7.5 verified memory, read, hooks and cleanup with local deterministic inference. [Evidence and limits](https://github.com/ai-driven-dev/framework/tree/fix/opencode-plugin-loading/aidd_docs/tasks/2026_10/2026_10_07_opencode-plugin-loading).\n\n## Heads-up\nKilo telemetry unsupported. Paid providers, global profiles, hot reload and Windows Kilo runtime untested. Historical research/implementation chronology was not independently reconstructed in the final review.\n\n## Linked issue\nCloses #953\n\n## I certify\n- [ ] **I DO CERTIFY I READ EACH LINE OF THE PULL REQUEST BECAUSE I AM A SOFTWARE ENGINEER, NOT A AI PUPPY.**\n", + "title": "fix(cli): restore OpenCode plugins and verify Kilo runtime hooks", + "base": "next", + "base_sha": "22c541a70b9829305863a4c38ecac931b2914274", + "head": "fix/opencode-plugin-loading", + "head_sha": "bf400ebb1256c85b65f56274f82273738d0f6810", + "head_repo_id": 1151742228, + "head_repo_full_name": "ai-driven-dev/framework", + "merge_commit_sha": "12777d03808cee29f611e18d3816ae62ebc5c969", + "user": { + "login": "blafourcade", + "name": "blafourcade", + "email": null, + "avatar_url": "https://avatars.githubusercontent.com/u/119650761?v=4", + "id": 119650761 + }, + "requested_reviewers": null, + "requested_team_reviewers": null, + "diff": null, + "comments": null, + "created_at": "2026-10-07T14:54:15Z", + "updated_at": "2026-10-08T18:19:53Z", + "closed_at": "2026-10-08T18:19:52Z", + "merged_at": "2026-10-08T18:19:52Z", + "commits": 23, + "changed_files": 75, + "additions": 2016, + "deletions": 526, + "display_url": "https://github.com/ai-driven-dev/framework/pull/971", + "display_title": "fix(cli): restore OpenCode plugins and verify Kilo runtime hooks" + }, + "pr979": { + "url": "https://github.com/ai-driven-dev/framework/pull/979", + "number": 979, + "state": "open", + "merged": false, + "mergeable": true, + "draft": true, + "body": "## 🎯 What & why\n\nCreating a project rule should work from the installed skill and preserve its full content across Claude Code, Cursor, GitHub Copilot, Codex and OpenCode V2. The skill now owns publication without requiring the AIDD CLI, and shared guidance uses readable titles and concise scopes.\n\n## 🛠️ How it works\n\nThe [standalone writer](plugins/aidd-context/skills/05-rule-generate/scripts/write-rule.cjs#L1) accepts explicit JSON metadata and the complete Markdown body. Editable canonical sources live in `aidd_docs/rules/`. Claude, Cursor and Copilot receive native files; Codex and OpenCode share a signed contribution in root `AGENTS.md`.\n\nThe skill resolves the writer beside its loaded `SKILL.md`, so native and flat installations support create, update, publish and delete. Validation happens before mutation; signatures protect generated outputs, while user guidance, memory, exact literals and body bytes survive. Shared publication omits taxonomy identifiers from added headings, avoids adding a title before a leading ATX/single-line Setext title, and preserves literal glob text.\n\nThe [official Codex contract](https://learn.chatgpt.com/docs/agent-configuration/agents-md) and [OpenCode V2 instructions](https://opencode.ai/v2/docs/instructions) underpin the active surfaces. `cli/` is unchanged from `12777d03`; the initial CLI coupling and budget increase are removed.\n\n## 🧪 How to verify\n\n- `node scripts/check-tests-leave-git-alone.js -- node --test 'scripts/__tests__/**/*.test.js'`: 613 tests pass, including 59 rule-generation tests executing copied skills without AIDD in ESM projects.\n- [Portable sandbox](aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/reproduction/README.md): six projects, ten identical delivered writers, 78 lifecycle operations, 12 refusals with unchanged project bytes, 54 application tests and cleanup ownership checks.\n- Current Codex/OpenCode binaries exercised creation, update and deletion using isolated local mock providers. Actual model-request payloads contain the exact current body and omit stale/deleted content.\n- [Current verification](aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/rule-presentation/verification.md) and [independent review](aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/rule-presentation/review.md): five of five criteria fulfilled, no open finding. Indexed Git blobs match all 121 archived current/historical evidence hashes.\n\n## ⚠️ Heads-up\n\nCurrent mock-provider captures prove host input loading, not semantic model enforcement. [Historical subscription-model evidence](aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/REPORT.md) retains its previous writer hash and incomplete Cursor quota-limited control; it is not presented as a complete five-host model test of this revision. No Linux/Windows runtime claim.\n\nCodex/OpenCode scopes are model guidance rather than native glob filtering. Mixed hosts may discover both shared and native guidance. The local Codex size guard cannot measure the combined global/project budget. Publication preserves bodies without implementing a full Markdown parser.\n\nNo automatic legacy migration, multi-file disk-failure rollback or concurrent-writer guarantee. Comma-containing globs are refused; use separate patterns. OpenCode V1, lifecycle synchronization, standalone-rule flat archives (#789) and Kilo (#914) remain outside scope. Archived CRLF fixtures and raw receipts have scoped Git attributes preserving their exact bytes.\n\n## 🔗 Linked issue\n\nRefs #913. This PR implements the selected V2 contract and the five-host autonomous skill; it does not claim the original V1 `configuration.instructions`/JSONC criteria.\n\n## ✅ I certify\n\n- [ ] **I DO CERTIFY I READ EACH LINE OF THE PULL REQUEST BECAUSE I AM A SOFTWARE ENGINEER, NOT A AI PUPPY.**\n", + "title": "fix(aidd-context): generate rules through a standalone skill script", + "base": "next", + "base_sha": "7f0b344b4ae37c083aa9473e0e6347b196b630ef", + "head": "fix/opencode-rule-discovery", + "head_sha": "303e17a1ba995dd19ec769381f7bbb594fbd1d7c", + "head_repo_id": 1151742228, + "head_repo_full_name": "ai-driven-dev/framework", + "merge_commit_sha": "3769711dd60737d48b44534e9e85c238911d1779", + "user": { + "login": "blafourcade", + "name": "blafourcade", + "email": null, + "avatar_url": "https://avatars.githubusercontent.com/u/119650761?v=4", + "id": 119650761 + }, + "requested_reviewers": null, + "requested_team_reviewers": null, + "diff": null, + "comments": null, + "created_at": "2026-10-09T03:23:14Z", + "updated_at": "2026-10-09T08:39:45Z", + "closed_at": null, + "merged_at": null, + "commits": 3, + "changed_files": 197, + "additions": 17515, + "deletions": 59, + "display_url": "https://github.com/ai-driven-dev/framework/pull/979", + "display_title": "fix(aidd-context): generate rules through a standalone skill script" + }, + "pr979_files": { + "filenames": [ + "aidd_docs/tasks/2026_10/2026_10_08_opencode-rule-discovery/backlog-link.json", + "aidd_docs/tasks/2026_10/2026_10_08_opencode-rule-discovery/challenge.md", + "aidd_docs/tasks/2026_10/2026_10_08_opencode-rule-discovery/implementation-verification.md", + "aidd_docs/tasks/2026_10/2026_10_08_opencode-rule-discovery/official-verification.md", + "aidd_docs/tasks/2026_10/2026_10_08_opencode-rule-discovery/phase-1.md", + "aidd_docs/tasks/2026_10/2026_10_08_opencode-rule-discovery/phase-2.md", + "aidd_docs/tasks/2026_10/2026_10_08_opencode-rule-discovery/plan.md", + "aidd_docs/tasks/2026_10/2026_10_08_opencode-rule-discovery/review.md", + "aidd_docs/tasks/2026_10/2026_10_08_opencode-rule-discovery/revision-plan.md", + "aidd_docs/tasks/2026_10/2026_10_08_opencode-rule-discovery/runtime-results.json", + "aidd_docs/tasks/2026_10/2026_10_08_opencode-rule-discovery/spec.md", + "aidd_docs/tasks/2026_10/2026_10_08_opencode-rule-discovery/verification.md", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/backlog-link.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/challenge.md", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/official-verification.md", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/phase-1.md", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/phase-2.md", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/phase-3.md", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/plan.md", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/.gitattributes", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/REPORT.md", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/deterministic-results.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/final-checks.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/final-git-status.txt", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/final-review.md", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/fixture/.claude/rules/00-architecture/0-user-boundaries.md", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/fixture/.cursor/rules/00-architecture/0-user-boundaries.mdc", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/fixture/.github/instructions/00-user-boundaries.instructions.md", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/fixture/AGENTS.md", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/fixture/README.md", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/fixture/aidd_docs/memory/architecture.md", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/fixture/docs/api.md", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/fixture/docs/contributing.md", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/fixture/package.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/fixture/src/domain/invoice-store.ts", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/fixture/src/domain/invoice.ts", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/fixture/src/http/app.ts", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/fixture/src/http/server.ts", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/fixture/tests/http.test.ts", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/fixture/tests/invoice.test.ts", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/focused-direct-final-tests.log", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/focused-final-tests.log", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/focused-node26-final-tests.log", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/focused-two-retry.log", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/local-changes.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/claude/REPORT.md", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/claude/additional/claude-native-03/tests-retry-stdout.txt", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/claude/additional/claude-native-03/tests-retry.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/claude/additional/claude-native-03/tests-stdout.txt", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/claude/additional/claude-native-additional-report.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/claude/additional/delete-proof.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/claude/additional/local-doc-refresh.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/claude/additional/old-consumer-proof.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/claude/additional/old-rebuild.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/claude/additional/quota-after.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/claude/additional/sanitization.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/claude/journey-report.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/claude/summary.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/codex/REPORT.md", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/codex/observations.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/codex/preflight.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/codex/summary.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/copilot/journey-report.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/copilot/model-copilot-delete-retry/delete/summary.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/copilot/model-copilot-delete-retry/deleted-domain/independent-tests.log", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/copilot/model-copilot-delete-retry/deleted-domain/summary.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/copilot/model-copilot-delete-retry/preservation.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/copilot/model-copilot-exact-retry/update/summary.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/copilot/model-copilot-exact-retry/updated-domain/independent-tests.log", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/copilot/model-copilot-exact-retry/updated-domain/summary.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/cursor/cursor-evidence-summary.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/cursor/cursor-exact-author-revision.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/cursor/cursor-exact-consumer-proof.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/cursor/cursor-exact-current-assets.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/cursor/cursor-exact-current-metadata-refresh.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/cursor/cursor-journey-report.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/cursor/cursor-report.md", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/cursor/examples/src/domain/discount.ts", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/cursor/examples/src/domain/fee.ts", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/cursor/examples/tests/discount.test.ts", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/cursor/examples/tests/fee.test.ts", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/cursor/private-auth-cleanup.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/final-reproduction/cleanup.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/final-reproduction/results.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/index.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/openai-quota.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/opencode/01-author-coexistence/app-tests.log", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/opencode/01-author-coexistence/prompt.txt", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/opencode/01-author-coexistence/summary.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/opencode/01-author-retry/app-tests.log", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/opencode/01-author-retry/prompt.txt", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/opencode/01-author-retry/summary.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/opencode/01-author/app-tests.log", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/opencode/01-author/prompt.txt", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/opencode/01-author/summary.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/opencode/02-domain-retry/app-tests.log", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/opencode/02-domain-retry/prompt.txt", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/opencode/02-domain-retry/summary.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/opencode/02-domain/app-tests.log", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/opencode/02-domain/prompt.txt", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/opencode/02-domain/summary.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/opencode/03-docs/app-tests.log", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/opencode/03-docs/prompt.txt", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/opencode/03-docs/summary.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/opencode/04-update/app-tests.log", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/opencode/04-update/prompt.txt", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/opencode/04-update/summary.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/opencode/05-updated-domain/app-tests.log", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/opencode/05-updated-domain/prompt.txt", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/opencode/05-updated-domain/summary.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/opencode/06-delete-coexistence/app-tests.log", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/opencode/06-delete-coexistence/prompt.txt", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/opencode/06-delete-coexistence/summary.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/opencode/06-delete/app-tests.log", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/opencode/06-delete/prompt.txt", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/opencode/06-delete/summary.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/opencode/07-deleted-domain/app-tests.log", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/opencode/07-deleted-domain/prompt.txt", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/opencode/07-deleted-domain/summary.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/opencode/examples/aidd_docs/memory/architecture.md", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/opencode/examples/initial-consumer.ts", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/opencode/examples/post-delete-consumer.ts", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/opencode/examples/published-AGENTS.md", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/opencode/examples/updated-consumer.ts", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-evidence/opencode/observations.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/native-results.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/reproduction-final-smoke.log", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/reproduction/README.md", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/reproduction/cleanup-ownership.cjs", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/reproduction/common.cjs", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/reproduction/install.cjs", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/reproduction/lifecycle.cjs", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/reproduction/negative-pipeline.cjs", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/reproduction/run.cjs", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/realistic-sandbox/test-validation.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/review.md", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/rule-presentation/evidence/.gitattributes", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/rule-presentation/evidence/index.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/rule-presentation/evidence/installed-writers.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/rule-presentation/evidence/probes/codex.py", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/rule-presentation/evidence/probes/opencode.py", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/rule-presentation/evidence/rendered/create/AGENTS.txt", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/rule-presentation/evidence/rendered/delete/AGENTS.txt", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/rule-presentation/evidence/rendered/update/AGENTS.txt", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/rule-presentation/evidence/repository-gates.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/rule-presentation/evidence/root-tests.log", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/rule-presentation/evidence/runtime-checks.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/rule-presentation/evidence/runtime/codex-create/requests.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/rule-presentation/evidence/runtime/codex-create/stderr.log", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/rule-presentation/evidence/runtime/codex-create/stdout.log", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/rule-presentation/evidence/runtime/codex-create/summary.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/rule-presentation/evidence/runtime/codex-delete/requests.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/rule-presentation/evidence/runtime/codex-delete/stderr.log", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/rule-presentation/evidence/runtime/codex-delete/stdout.log", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/rule-presentation/evidence/runtime/codex-delete/summary.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/rule-presentation/evidence/runtime/codex-update/requests.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/rule-presentation/evidence/runtime/codex-update/stderr.log", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/rule-presentation/evidence/runtime/codex-update/stdout.log", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/rule-presentation/evidence/runtime/codex-update/summary.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/rule-presentation/evidence/runtime/opencode-create/model-requests.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/rule-presentation/evidence/runtime/opencode-create/stderr.log", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/rule-presentation/evidence/runtime/opencode-create/stdout.log", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/rule-presentation/evidence/runtime/opencode-create/summary.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/rule-presentation/evidence/runtime/opencode-delete/model-requests.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/rule-presentation/evidence/runtime/opencode-delete/stderr.log", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/rule-presentation/evidence/runtime/opencode-delete/stdout.log", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/rule-presentation/evidence/runtime/opencode-delete/summary.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/rule-presentation/evidence/runtime/opencode-update/model-requests.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/rule-presentation/evidence/runtime/opencode-update/stderr.log", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/rule-presentation/evidence/runtime/opencode-update/stdout.log", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/rule-presentation/evidence/runtime/opencode-update/summary.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/rule-presentation/evidence/sandbox/cleanup-results.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/rule-presentation/evidence/sandbox/install-commands.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/rule-presentation/evidence/sandbox/installations.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/rule-presentation/evidence/sandbox/lifecycle-commands.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/rule-presentation/evidence/sandbox/lifecycle-results.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/rule-presentation/evidence/sandbox/negative-commands.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/rule-presentation/evidence/sandbox/negative-results.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/rule-presentation/evidence/sandbox/reproduction-results.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/rule-presentation/evidence/write-rule.cjs", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/rule-presentation/plan.md", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/rule-presentation/review.md", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/rule-presentation/verification.md", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/runtime-results.json", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/spec.md", + "aidd_docs/tasks/2026_10/2026_10_09_autonomous-rule-generation/verification.md", + "plugins/aidd-context/CATALOG.md", + "plugins/aidd-context/skills/05-rule-generate/SKILL.md", + "plugins/aidd-context/skills/05-rule-generate/actions/01-capture-rule.md", + "plugins/aidd-context/skills/05-rule-generate/actions/02-write-rule.md", + "plugins/aidd-context/skills/05-rule-generate/actions/03-validate.md", + "plugins/aidd-context/skills/05-rule-generate/assets/rule-template.md", + "plugins/aidd-context/skills/05-rule-generate/references/rule-authoring.md", + "plugins/aidd-context/skills/05-rule-generate/references/tool-paths.md", + "plugins/aidd-context/skills/05-rule-generate/scripts/write-rule.cjs", + "plugins/aidd-context/skills/11-explore/references/ai-mapping.md", + "scripts/__tests__/rule-generation.test.js" + ] + } +} diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/test-strategy.md b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/test-strategy.md new file mode 100644 index 000000000..f51441318 --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/test-strategy.md @@ -0,0 +1,61 @@ +# Stratégie de validation + +## Séparer ce qui est prouvé + +1. Contrat de source : assertions structurelles et références, avec mutations ciblées qui rendent la garde rouge. Nécessaire, insuffisant pour un skill interprété. +2. Artefact : exécuter le vrai writer/hook dans des projets temporaires, inspecter bytes, YAML/TOML/JSONC, arbres et hashes. Pas de mocks de filesystem pour les preuves de conservation. +3. Génération comportementale : Codex principal charge les skills effectivement livrés, exécute leurs actions sur fixtures avec réponses utilisateur enregistrées. La sortie attendue n'est jamais copiée manuellement comme résultat du skill. Couvrir toutes les cibles, dont Claude sans Anthropic. Séparer intervention nécessaire et sélection implicite interdite. +4. Loader Kilo : vrai binaire consomme ces sorties ; observations du catalogue/config résolue et requêtes de contexte. Inférence locale déterministe réutilisée de #971 pour rendre visibles les payloads. C'est un runtime réel avec backend contrôlé, pas une preuve de raisonnement d'un modèle externe. +5. Usage Kilo : modèle gratuit effectivement accessible, invocation des skills/agents/commands et tâches nécessitant règles/mémoire. Traces tool/command et résultats contrôlés, pas réponse sentinelle seulement. Aucun paiement ou fallback payant. + +## Fixtures et oracles + +Suite temporaire commune : .kilo seule ; chacun des quatre fichiers config seul ; .kilocode seule ; opencode.json et opencode.jsonc seuls ; mélange Kilo/Claude/Codex/OpenCode/Cursor/Copilot ; projet sans config. Frontières de root, noms avec échappements, ESM, LF/CRLF, espaces/tabs, Unicode. + +Configs : zéro fichier, unique, plusieurs avec propriétaire unique de l'entrée exacte, plusieurs sans propriétaire, propriétaires multiples, sélection explicitement refusée/annulée. L'existence du chemin exact .kilo/rules/category/slug.md dans un instructions valide identifie un candidat propriétaire ; glob voisin, commentaire ou chaîne JSON imbriquée ne le font pas. Plusieurs propriétaires restent ambigus. Ne pas ajouter de champ privé non documenté au config pour l'ownership. + +Édition : instructions absent, tableau vide ou rempli, répétitions préexistantes y compris de l'entrée AIDD, espaces/commentaires entre tokens, trailing commas, URL avec //, chaînes contenant accolades, clés échappées, commentaires bloc. Insertion minimale seulement ; aucun JSON.parse/JSON.stringify global. Conserver tous doublons et ordre existants ; si entrée exacte déjà présente, aucune insertion. Invalides : JSONC non fermé, instructions scalaire, éléments non strings, clés instructions dupliquées, UTF-8 invalide, collision de fichier utilisateur, lien symbolique/ancêtre hors root, config choisie disparue/modifiée. + +Oracles : carte complète chemin -> hash avant/après, octets hors plage éditée identiques, parsing indépendant après édition, résultat runtime cohérent avec la config sélectionnée, second passage bytes et mtime inchangés. Erreur/annulation : aucun nouvel artefact, aucune config/règle/source modifiée ; pas de temporaires restants. Pour les erreurs multi-destinations, invalider le dernier candidat pour détecter une validation tardive. Pour publication règle/config, injecter échec de staging/rename avec restauration observable ; un renommage par fichier ne suffit pas pour promettre zéro écriture partielle. Concurrence/crash machine hors garantie générale : détecter changement entre préflight et commit, refuser avant publication ; documenter toute limite résiduelle, ne pas cocher AC14 si l'erreur réellement testée laisse une écriture partielle. + +## Claude hors ligne et comportement + +Réutiliser scripts/__tests__/update-memory.test.js et fixtures Claude du CLI. Renforcer par vrais sous-processus ; CLAUDE.md garde ses imports @ et son contenu hors bloc. Règles .claude/rules avec paths seulement si scope ; skills .claude/skills avec frontmatter canonique ; agents .claude/agents avec name/description/options ; commandes .claude/commands avec champs acceptés ; hooks déclaratifs conservés pour Claude et dédupliqués sur relance, sans effacer les hooks utilisateur. + +Tester sorties génération Claude par Codex et scripts déterministes disponibles, ainsi que snapshots CLI de distribution : ces preuves ont des scopes différents. Parsing des formats, chemins, contenu complet, idempotence, ownership, refus sans mutation. Le validateur installé `claude plugin validate` peut vérifier les packages sans demander une inférence ; `scripts/check-claude-accepts-build.cjs` lit Validation passed/failed, pas seulement le code de sortie. Il ne valide pas le comportement LLM de tous les générateurs. + +Indisponible : claude -p avec modèle Claude authentifié, y compris le runner actuel scripts/skill-eval.mjs. CONTRIBUTING demande des tests dans Claude et un autre outil ; validateur + artefacts ne remplacent pas ce parcours authentifié. À obtenir d'un mainteneur/environnement disposant déjà de l'accès avant une affirmation de validation runtime Claude complète, sans exiger un abonnement de l'utilisateur. Aucun des 16 critères n'exige lui-même une inférence Claude ; AC15 vise Kilo. Limite supplémentaire de non-régression, pas excuse pour omettre un critère Kilo. + +## Codex, OpenCode et autres cibles + +Codex : génération réelle des cibles, découverte de skills traduits, TOML agents correctement échappé, instructions issues de la publication active de la base #979 choisie, conservation AGENTS.md et distinction des blocs. Captures des entrées host prouvent chargement, pas enforcement sémantique universel. + +OpenCode absent ici : fixtures et suites CLI existantes, plugin opencode tests, golden snapshots. Aucun nouvel outil installé pour le plan. Runtime version(s) supportée(s) à exécuter dans environnement déjà équipé/CI ; OpenCode V1/V2 ne sont pas interchangeables, figer les versions. Si manquant, publier ce manque au rapport final ; pas de revendication runtime. + +Cursor/Copilot : garder formats règle/agent/command/hook réellement supportés, sorties skip explicites quand unsupported, mémoire Copilot liens ../ ; matrices mixtes vérifient que Kilo n'altère pas les sorties non sélectionnées. Pas d'exécution IDE prétendue. + +## Preuve runtime Kilo par artefact + +- Mémoire : AGENTS.md chargé automatiquement ; demander une tâche nécessitant un fichier mémoire lié et vérifier sa lecture effective. Un lien n'est pas un import. Respecter l'approbation de modification AGENTS.md par le host. +- Règle : config résolue contient le chemin exact ; contexte envoyé contient un marqueur de corps unique ; négatif sans instructions ne charge pas ce fichier ; tâche applique la règle avec modèle gratuit. +- Skill : kilo debug skill expose nom/description/chemin exact ; usage réel trace le chargement du SKILL.md et de son action, plus résultat ; portable choisi explicitement une seule fois. +- Agent : liste/debug expose nom issu du fichier et options ; invocation via tâche/subagent supportée produit un résultat et prouve le bon corps chargé. Ne pas confondre invocation primary et subagent. +- Commande : catalogue du runtime et invocation kilo run --command avec modèle gratuit ; observer expansion et corps de .kilo/commands. Aucun champ Claude non supporté. +- Hooks : guidance #914 n'émet pas de plugin ; pas de test d'un plugin inventé. Vérifier absence d'écriture, refus précis pour event non prouvé, et réutiliser le bridge existant pour session.created si invoqué dans une fixture séparée. + +Linux local 7.8.8 d'abord. Utiliser la CI existante Windows/macOS selon matrice supportée du projet ; publier OS/version et résultats séparés. Windows Kilo runtime non prouvé par les preuves #971. Les critères runtime de plateforme restent ouverts jusqu'à exécution sur ces plateformes ; un skip n'est jamais pass. Conserver commandes, stdout/stderr expurgés, payloads sans secrets, hashes d'artefacts générés et négatifs, versions et nettoyage. + +## Commandes prévues après approbation + +```sh +node scripts/check-tests-leave-git-alone.js -- node --test 'scripts/__tests__/**/*.test.js' +pnpm test:changed +pnpm --dir cli test +pnpm --dir cli test:e2e:kilo +node scripts/check-claude-accepts-build.cjs +make check +``` + +Le CLI doit être construit pour les checks qui le consomment ; vérifier les prérequis existants sans réinstaller aveuglément. Ajouter une commande documentée opt-in au harness comportemental et au runtime natif phase 6, après lecture des APIs/flags exposés. Pas de nom de commande future présenté comme déjà disponible. Les tests Kilo existants peuvent dépendre d'un serveur local et d'une élévation du sandbox ; ne pas masquer une erreur d'environnement. + +`make check` régénère et stage certains docs : le lancer seulement pendant l'implémentation autorisée, inspecter ses deltas et ne jamais commit/push automatiquement. Gates CLI lint/typecheck/architecture/knip et mutations ciblées si CLI/tests/scripts impactés ; pas de hausse de seuil. Test rouge avant implémentation pour chaque nouveau comportement ; cas négatif doit échouer quand l'invariant est volontairement cassé. Tests/docus livrés dans chaque phase, phase 6 consolide seulement les preuves de bout en bout. diff --git a/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/user-request.md b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/user-request.md new file mode 100644 index 000000000..70d39d39e --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_10_kilo-native-generation-914/user-request.md @@ -0,0 +1,468 @@ +# Handoff Codex — Contribution AIDD #914 + +## Mission + +Développer l'issue officielle : + +https://github.com/ai-driven-dev/framework/issues/914 + +**feat(aidd-context): add Kilo-native generation targets** + +Objectif : permettre à AIDD de détecter les projets Kilo Code et de générer correctement leurs artefacts natifs : règles, skills, agents, workflows, Project Memory et indications de hooks. + +**Impératif : utiliser le framework AIDD lui-même pour planifier, développer, tester et reviewer cette contribution.** + +La fonctionnalité doit être entièrement conforme aux critères d'acceptation de l'issue et ne doit introduire aucune régression sur Claude Code ni sur les autres outils déjà supportés. + +## 1. Environnement existant + +Le dépôt est déjà cloné dans : + +`~/project` + +La branche actuellement ouverte est `main`. + +Les remotes Git sont déjà configurés : + +- `origin` : `[private fork remote redacted]` +- `upstream` : `git@github.com:ai-driven-dev/framework.git` + +Ne recrée pas ces remotes et ne reclone pas le dépôt. + +AIDD n'est pas encore installé dans l'environnement de développement Codex. Son installation et sa vérification font donc partie de cette mission. + +L'IDE utilisé est VS Code avec l'extension Codex, dans un workspace distant Coder. + +## 2. Préparer Git en sécurité + +Avant toute opération : + +1. Vérifier `git status`, la branche courante et les remotes. +2. Vérifier les éventuelles modifications non commitées. +3. Exécuter `git fetch upstream`. +4. Vérifier la présence de `upstream/next`. +5. Créer une branche dédiée issue de `upstream/next` : + + `feat/kilo-native-generation-914` + +6. Ne jamais écraser les modifications locales. +7. Ne jamais effectuer de `reset --hard`, de nettoyage destructif ou de push forcé. +8. Vérifier l'état actuel de l'issue #914 et rechercher d'éventuelles PR concurrentes. + +La future PR devra cibler `ai-driven-dev/framework:next`, jamais `main`. + +## 3. Installer AIDD dans l'environnement Codex + +Cette étape est obligatoire avant la planification. + +Lire en priorité : + +- `README.md` +- `CONTRIBUTING.md` +- `AGENTS.md` +- `docs/ARCHITECTURE.md` +- Les instructions d'installation et de compatibilité Codex. +- Les instructions des plugins et skills AIDD. + +### Préparer les prérequis + +Vérifier les versions et la disponibilité de : + +- Node.js 22.12+ +- pnpm +- jq +- Python 3 +- pipx +- make +- Git + +Installer uniquement les dépendances manquantes en respectant le mécanisme recommandé par le dépôt. + +### Installer le framework + +Utiliser la procédure officielle documentée dans le dépôt. + +La commande de référence pour préparer l'environnement contributeur est : + +`make setup` + +Vérifier son comportement actuel avant exécution, notamment les éventuelles modifications des configurations utilisateur et les installations dans Claude Code ou Codex. + +Utiliser ensuite : + +`make doctor` + +### Vérifier les skills dans Codex + +Ne considère pas l'installation comme réussie uniquement parce que `make setup` termine sans erreur. + +Vérifier concrètement : + +- Où Codex découvre les skills AIDD. +- Quels plugins et skills sont disponibles. +- Si les skills nécessaires peuvent réellement être invoqués depuis la session VS Code. +- Si les chemins et dépendances sont correctement résolus. +- Si un redémarrage de la session Codex est nécessaire. + +Ne pas confondre présence des fichiers et découverte effective par Codex. + +Ne pas inventer de syntaxe d'invocation. + +Si un redémarrage est nécessaire, fournir un état de reprise précis avec les instructions permettant de reprendre le travail dans une nouvelle session. + +En cas d'incompatibilité, documenter le problème et proposer le mécanisme officiellement supporté avant de poursuivre. + +## 4. Charger et utiliser AIDD + +Utiliser effectivement les capacités AIDD disponibles. + +Workflow visé : + +1. `aidd-context:00-onboard`, si pertinent. +2. `aidd-context:02-project-memory`, seulement si une initialisation ou actualisation est nécessaire. +3. `aidd-dev:01-plan`. +4. `aidd-refine:02-challenge`, lorsque ses préconditions sont satisfaites. +5. `aidd-dev:02-implement`. +6. `aidd-dev:06-test`. +7. `aidd-dev:03-assert`. +8. `aidd-dev:05-review`. +9. Les capacités AIDD de gestion des commits et PR, lorsque leur utilisation est appropriée. + +Vérifier les noms exacts et les contrats des skills dans la version installée. + +Ne pas improviser un workflow parallèle si les capacités AIDD existent. + +Respecter la séparation des responsabilités entre planification, implémentation, tests, assertions et review. + +## 5. Analyser intégralement l'issue #914 + +Lire : + +https://github.com/ai-driven-dev/framework/issues/914 + +Utiliser sa version courante, incluant les commentaires et éventuelles modifications de périmètre. + +Examiner les implémentations existantes, en particulier : + +- `cli/src/contexts/tools/domain/profiles/kilo/profile.ts` +- `cli/src/contexts/tools/domain/profiles/kilo/kilo-paths.ts` +- `cli/src/contexts/tools/domain/profiles/kilo/kilo-hooks-bridge.ts` +- Les générateurs du plugin `aidd-context`. +- Les tests, fixtures et utilitaires existants. +- Les mécanismes équivalents pour Claude Code et OpenCode. + +Vérifier également les issues liées : + +- #744 : support CLI Kilo livré. +- #868 : distribution des règles Kilo en mode flat, hors périmètre. +- Les éventuelles évolutions récentes affectant les mêmes générateurs. + +Ne pas développer de fonctionnalité déjà implémentée. + +## 6. Contraintes architecturales impératives + +Le framework AIDD utilise les formats sources canoniques Claude Code pour ses skills, agents et règles. + +**L'ajout de Kilo ne doit pas modifier ni dégrader ces contrats existants.** + +Principes : + +- Préserver Claude Code comme cible pleinement fonctionnelle. +- Préserver les autres cibles supportées, notamment Codex et OpenCode. +- Réutiliser les mécanismes existants. +- Éviter de dupliquer un générateur complet pour Kilo. +- Isoler les adaptations propres aux outils aux bons niveaux d'abstraction. +- Ne pas introduire de dépendance injustifiée entre plugins. +- Respecter les frontières définies par `docs/ARCHITECTURE.md`. +- Ne pas modifier le mécanisme de distribution flat relevant de #868. + +Ne pas introduire de refactoring global sans justification directement liée à #914. + +## 7. Critères d'acceptation à couvrir + +Construire une matrice de traçabilité entre chaque critère de l'issue, les fichiers modifiés, les tests et les preuves de validation. + +### A. Détection de Kilo + +Prendre en charge les signaux documentés : + +- `.kilo/` +- `.kilocode/` comme signal historique uniquement +- `kilo.json` et `kilo.jsonc` à la racine +- `.kilo/kilo.json` et `.kilo/kilo.jsonc` + +Ne pas détecter Kilo sur la seule présence de `opencode.json[c]`. + +Générer les nouveaux artefacts dans `.kilo/`, jamais dans `.kilocode/`. + +### B. Project Memory + +Utiliser `AGENTS.md` à la racine comme surface principale de Project Memory pour Kilo. + +Préserver les contenus écrits par l'utilisateur. + +Modifier uniquement les sections réellement contrôlées par AIDD. + +Éviter les duplications lorsque plusieurs outils partagent le même fichier. + +### C. Rules + +Produire les règles sous les chemins canoniques `.kilo/rules/`. + +Référencer explicitement les fichiers dans le tableau `instructions` de la configuration Kilo. + +Ne pas considérer le répertoire des règles comme automatiquement découvert. + +Préserver l'ordre des instructions. + +### D. Configuration JSON / JSONC + +Respecter les règles de sélection de fichier définies dans l'issue. + +En particulier : + +- Créer `.kilo/kilo.jsonc` lorsqu'aucune configuration projet n'existe. +- Modifier l'unique configuration existante lorsqu'elle est non ambiguë. +- Réutiliser le propriétaire actuel d'une entrée AIDD lorsqu'il peut être identifié. +- En présence de plusieurs configurations ambiguës, demander une sélection explicite. +- Respecter la précédence documentée de Kilo. +- Préserver les instructions existantes, leur ordre, leurs doublons, les commentaires JSONC, les virgules finales et les autres éléments de formatage. +- Garantir l'idempotence. +- Ne pas effectuer d'écriture partielle lorsqu'une modification sûre est impossible. + +Ne pas appliquer aveuglément au runtime Kilo les restrictions spécifiques du CLI AIDD. + +### E. Skills + +Générer des skills conformes au format Agent Skills. + +Pour Kilo uniquement, utiliser `.kilo/skills/`. + +N'utiliser `.agents/skills/` que si l'utilisateur choisit explicitement une cible partagée portable. + +Ne jamais générer deux copies du même skill. + +### F. Agents + +Utiliser les emplacements natifs `.kilo/agents/`. + +Respecter le contrat de frontmatter documenté. + +Préserver les champs optionnels officiellement supportés lorsqu'ils sont demandés. + +### G. Workflows + +Générer les commandes Markdown dans `.kilo/commands/`. + +N'utiliser que les propriétés de frontmatter effectivement supportées par Kilo. + +### H. Hooks + +Ne jamais produire de hooks déclaratifs Claude pour Kilo. + +Utiliser les contrats documentés des plugins JavaScript/TypeScript Kilo. + +Ne citer un équivalent d'événement ou de hook que lorsqu'il est officiellement vérifié. + +Ne pas étendre le périmètre à une conversion générique des hooks. + +### I. Compatibilité et traçabilité + +Chaque comportement spécifique à Kilo doit reposer sur un contrat vérifié. + +Respecter les sources documentaires mentionnées dans l'issue et leur date de vérification. + +Vérifier les éventuels changements récents et signaler toute divergence avec l'issue plutôt que modifier silencieusement son contrat. + +## 8. Planification avec AIDD + +Utiliser `aidd-dev:01-plan`. + +Produire un plan découpé en tâches fonctionnelles cohérentes, incluant leurs tests et les mises à jour documentaires associées. + +Ne pas découper artificiellement le code, les tests et la documentation d'une même fonctionnalité en commits séparés. + +Prévoir des jalons de validation intermédiaires. + +La matrice d'acceptation doit couvrir l'intégralité de #914. + +Examiner explicitement les risques de régression sur : + +- Claude Code. +- Codex. +- OpenCode. +- Les autres outils supportés. +- Les formats sources canoniques. +- L'onboarding AIDD. +- Les générateurs existants. +- Les configurations multi-outils. + +Utiliser `aidd-refine:02-challenge` pour challenger le plan si applicable. + +Présenter le plan avant implémentation. + +## 9. Implémentation + +Après validation du plan, utiliser `aidd-dev:02-implement`. + +Respecter les phases prévues, le périmètre de l'issue et les conventions du dépôt. + +Pour chaque tâche : + +1. Implémenter le comportement. +2. Ajouter ou adapter les tests pertinents. +3. Mettre à jour la documentation nécessaire. +4. Exécuter les validations adaptées. +5. Corriger les problèmes identifiés. +6. Conserver des preuves vérifiables du résultat. + +Aucun changement ne doit introduire une duplication injustifiée ni dégrader le fonctionnement des autres outils. + +Ne pas commit ni push sans autorisation. + +## 10. Tests et non-régression obligatoires + +Utiliser les contrats AIDD de tests et d'assertions. + +Construire des tests représentatifs pour chaque critère de #914. + +### Tests Kilo + +- Détection des différents signaux. +- Absence de faux positifs OpenCode. +- Génération de Project Memory. +- Préservation du contenu `AGENTS.md`. +- Génération des règles et référencement dans `instructions`. +- Génération des skills. +- Génération des agents. +- Génération des workflows. +- Comportement des hooks. +- Gestion des configurations multiples. +- Préservation JSONC. +- Idempotence. +- Échecs sans écriture partielle. +- Découverte effective des artefacts dans Kilo. + +### Tests de non-régression + +Vérifier explicitement que les comportements existants des générateurs restent inchangés pour Claude Code. + +Conserver les tests existants et en ajouter lorsque les chemins modifiés peuvent affecter Claude. + +Vérifier également les contrats pertinents de Codex et OpenCode. + +Éviter de créer des tests se contentant de reproduire des valeurs configurées dans les mocks. + +Tester les comportements observables, avec des fixtures réalistes. + +### Validation runtime + +Ne pas considérer les tests unitaires comme une preuve d'intégration runtime. + +Exécuter les scénarios de découverte d'artefacts avec Kilo lorsqu'un runtime compatible est disponible. + +Suivre également les exigences de `CONTRIBUTING.md`, dont les validations locales et les tests avec Claude et un autre outil. + +Si Claude ou Kilo ne sont pas disponibles dans l'environnement, identifier les validations manquantes et ne pas les présenter comme réussies. + +Les changements impliquant des configurations ou fichiers partagés doivent faire l'objet de tests de préservation des données utilisateur. + +### Commandes projet + +Découvrir les commandes applicables dans le dépôt. + +Vérifier notamment : + +- `make check` +- `make reload` +- Les tests spécifiques au périmètre modifié. +- Les éventuelles validations additionnelles définies par le workflow contributeur. + +Ne pas inventer de résultat de test. + +## 11. Review avec AIDD + +Utiliser `aidd-dev:05-review`. + +La review doit analyser : + +- Le respect de chaque critère d'acceptation. +- La qualité de l'architecture. +- La préservation des comportements Claude Code. +- La non-régression Codex / OpenCode. +- Les risques de perte de configuration utilisateur. +- L'idempotence. +- La qualité et l'efficacité des tests. +- Les preuves de validation runtime. +- Les écarts entre l'implémentation et les spécifications de #914. +- L'absence de changements relevant de #868. + +Corriger les problèmes, puis relancer les validations concernées. + +## 12. Stratégie Git et Pull Request + +La contribution doit cibler `upstream/next`. + +Respecter les conventions de commits du projet. + +Conserver des unités de travail cohérentes, comprenant le comportement, ses tests et sa documentation associée. + +### Stratégie de PR + +Ne pas ouvrir de PR immédiatement après la création de la branche. + +Séquence recommandée : + +1. Plan validé. +2. Première tranche fonctionnelle implémentée et testée. +3. Proposition de commits cohérents. +4. Après mon autorisation explicite : commit et push vers mon fork `[private fork]`. +5. Après mon autorisation explicite : création d'une Draft PR ciblant `ai-driven-dev/framework:next`. +6. Poursuite des développements avec retours et tests. +7. Review AIDD complète et validation des critères d'acceptation. +8. Après mon autorisation : passage de la PR en Ready for review. + +Ne jamais ouvrir une PR sur la branche `main`. + +Utiliser le template de PR officiel. + +Référencer #914 et documenter les tests réellement exécutés. + +Évaluer si le volume de #914 justifie plusieurs PR cohérentes. Ne modifier la stratégie de découpage qu'après validation. + +## 13. Règles d'autonomie + +Tu peux : + +- Lire les fichiers et la documentation. +- Installer les prérequis nécessaires au développement, après vérification de leur portée. +- Exécuter les commandes de diagnostic et de test. +- Préparer une branche dédiée. +- Réaliser les modifications autorisées après validation du plan. + +Tu ne peux pas, sans mon accord explicite : + +- Écraser des modifications locales. +- Modifier durablement des paramètres utilisateur sans nécessité validée. +- Commit. +- Push. +- Créer ou modifier une PR distante. +- Modifier le périmètre officiel de l'issue. + +Si le contexte de la conversation est compacté ou si une nouvelle session est nécessaire, conserver les décisions et l'état du travail dans les artefacts prévus par AIDD. + +## 14. Première étape à exécuter maintenant + +Commence uniquement par : + +1. Vérifier mon environnement Git et les modifications locales. +2. Préparer la branche de contribution depuis `upstream/next`. +3. Installer et vérifier AIDD dans Codex. +4. Lire les instructions du dépôt. +5. Lire intégralement l'issue #914. +6. Identifier les écarts actuels et les risques de régression Claude Code. +7. Utiliser `aidd-dev:01-plan` pour produire le plan. +8. Challenger le plan lorsque le workflow AIDD le prévoit. +9. Présenter la matrice de couverture des critères d'acceptation et le plan détaillé. + +**Arrête-toi à la validation du plan. N'entame pas encore l'implémentation, ne commit pas et ne crée pas de PR.** \ No newline at end of file diff --git a/cli/package.json b/cli/package.json index f88b4807f..5e7a88b3b 100644 --- a/cli/package.json +++ b/cli/package.json @@ -48,6 +48,7 @@ "test:e2e": "vitest run --project=e2e", "test:foreign-clock": "node scripts/test-foreign-clock.mjs", "test:e2e:kilo": "KILO_RUNTIME_SMOKE=1 vitest run --project=e2e tests/e2e/kilo-runtime.e2e.test.ts", + "test:e2e:kilo:native": "KILO_NATIVE_GENERATION=1 vitest run --project=e2e tests/e2e/kilo-native-generation.e2e.test.ts", "test:coverage": "vitest run --coverage", "test:kanban": "pnpm --dir ../kanban test", "test:watch": "vitest", diff --git a/cli/tests/e2e/fixtures/kilo-native-generation/.kilo/agents/assets/agent-payload.txt b/cli/tests/e2e/fixtures/kilo-native-generation/.kilo/agents/assets/agent-payload.txt new file mode 100644 index 000000000..446b5d015 --- /dev/null +++ b/cli/tests/e2e/fixtures/kilo-native-generation/.kilo/agents/assets/agent-payload.txt @@ -0,0 +1 @@ +AGENT_APPLIED:maple-4820 diff --git a/cli/tests/e2e/fixtures/kilo-native-generation/.kilo/agents/verify-agent.md b/cli/tests/e2e/fixtures/kilo-native-generation/.kilo/agents/verify-agent.md new file mode 100644 index 000000000..c6a1fdf65 --- /dev/null +++ b/cli/tests/e2e/fixtures/kilo-native-generation/.kilo/agents/verify-agent.md @@ -0,0 +1,16 @@ +--- +description: Verify the phase four agent payload. +mode: subagent +temperature: 0 +permission: + read: allow +--- + +# Role + +Verify the bundled phase four agent payload. + +# Behavior + +1. Read `.kilo/agents/assets/agent-payload.txt`. +2. Return only its exact content. diff --git a/cli/tests/e2e/fixtures/kilo-native-generation/.kilo/commands/assets/workflow-payload.txt b/cli/tests/e2e/fixtures/kilo-native-generation/.kilo/commands/assets/workflow-payload.txt new file mode 100644 index 000000000..bb790c839 --- /dev/null +++ b/cli/tests/e2e/fixtures/kilo-native-generation/.kilo/commands/assets/workflow-payload.txt @@ -0,0 +1 @@ +WORKFLOW_APPLIED:birch-9173 diff --git a/cli/tests/e2e/fixtures/kilo-native-generation/.kilo/commands/verify-workflow.md b/cli/tests/e2e/fixtures/kilo-native-generation/.kilo/commands/verify-workflow.md new file mode 100644 index 000000000..f340e8791 --- /dev/null +++ b/cli/tests/e2e/fixtures/kilo-native-generation/.kilo/commands/verify-workflow.md @@ -0,0 +1,8 @@ +--- +description: Verify the phase four workflow payload. +agent: verify-agent +variant: minimal +subtask: true +--- + +Use verify-agent to read `.kilo/commands/assets/workflow-payload.txt` and return only its exact content. diff --git a/cli/tests/e2e/fixtures/kilo-native-generation/.kilo/kilo.jsonc b/cli/tests/e2e/fixtures/kilo-native-generation/.kilo/kilo.jsonc new file mode 100644 index 000000000..9a7899552 --- /dev/null +++ b/cli/tests/e2e/fixtures/kilo-native-generation/.kilo/kilo.jsonc @@ -0,0 +1,5 @@ +{ + "instructions": [ + ".kilo/rules/01-standards/1-naming.md", + ], +} diff --git a/cli/tests/e2e/fixtures/kilo-native-generation/.kilo/rules/01-standards/1-naming.md b/cli/tests/e2e/fixtures/kilo-native-generation/.kilo/rules/01-standards/1-naming.md new file mode 100644 index 000000000..2fb7d97f5 --- /dev/null +++ b/cli/tests/e2e/fixtures/kilo-native-generation/.kilo/rules/01-standards/1-naming.md @@ -0,0 +1,3 @@ +# Naming fixture + +COBALT_914 diff --git a/cli/tests/e2e/fixtures/kilo-native-generation/.kilo/skills/verify-payload/SKILL.md b/cli/tests/e2e/fixtures/kilo-native-generation/.kilo/skills/verify-payload/SKILL.md new file mode 100644 index 000000000..facccbb03 --- /dev/null +++ b/cli/tests/e2e/fixtures/kilo-native-generation/.kilo/skills/verify-payload/SKILL.md @@ -0,0 +1,28 @@ +--- +name: verify-payload +description: Verify a bundled payload. Use when the user wants to verify the phase three fixture. +license: MIT +compatibility: Requires a host that can read local files. +metadata: + fixture: "phase-three" +--- + +# Verify Payload + +```mermaid +flowchart LR + request([verification request]) --> verify --> answer([verification code]) +``` + +## Actions + +Run the flow above. Read only the next action file. + +| Action | Does | +| --- | --- | +| verify | verify the bundled payload | + +## Transversal rules + +- Read [01-verify.md](actions/01-verify.md) before answering. +- Leave project files unchanged. diff --git a/cli/tests/e2e/fixtures/kilo-native-generation/.kilo/skills/verify-payload/actions/01-verify.md b/cli/tests/e2e/fixtures/kilo-native-generation/.kilo/skills/verify-payload/actions/01-verify.md new file mode 100644 index 000000000..7ef37d89a --- /dev/null +++ b/cli/tests/e2e/fixtures/kilo-native-generation/.kilo/skills/verify-payload/actions/01-verify.md @@ -0,0 +1,22 @@ +# 01 - Verify + +Verify the bundled payload. + +## Input + +A request to verify the phase three fixture. + +## Output + +The verification code. + +## Process + +1. **Read.** Read [payload.txt](../assets/payload.txt) for this request; do not guess its content. +2. **Reply.** Return exactly `PAYLOAD_APPLIED:` followed by the trimmed payload, with no extra text. + +## Test + +| Case | Pass | +| --- | --- | +| A verification request is answered | the payload read is traced and the exact verification code is returned | diff --git a/cli/tests/e2e/fixtures/kilo-native-generation/.kilo/skills/verify-payload/actions/02-user.md b/cli/tests/e2e/fixtures/kilo-native-generation/.kilo/skills/verify-payload/actions/02-user.md new file mode 100644 index 000000000..33c809839 --- /dev/null +++ b/cli/tests/e2e/fixtures/kilo-native-generation/.kilo/skills/verify-payload/actions/02-user.md @@ -0,0 +1,3 @@ +# User action + +USER_ACTION_KEEP diff --git a/cli/tests/e2e/fixtures/kilo-native-generation/.kilo/skills/verify-payload/assets/payload.txt b/cli/tests/e2e/fixtures/kilo-native-generation/.kilo/skills/verify-payload/assets/payload.txt new file mode 100644 index 000000000..0bf66b5e6 --- /dev/null +++ b/cli/tests/e2e/fixtures/kilo-native-generation/.kilo/skills/verify-payload/assets/payload.txt @@ -0,0 +1 @@ +cedar-7391 diff --git a/cli/tests/e2e/fixtures/kilo-native-generation/.kilo/skills/verify-payload/assets/user.txt b/cli/tests/e2e/fixtures/kilo-native-generation/.kilo/skills/verify-payload/assets/user.txt new file mode 100644 index 000000000..f52c38830 --- /dev/null +++ b/cli/tests/e2e/fixtures/kilo-native-generation/.kilo/skills/verify-payload/assets/user.txt @@ -0,0 +1 @@ +USER_RESOURCE_DO_NOT_REPLACE diff --git a/cli/tests/e2e/fixtures/kilo-native-generation/AGENTS.md b/cli/tests/e2e/fixtures/kilo-native-generation/AGENTS.md new file mode 100644 index 000000000..cca5889a0 --- /dev/null +++ b/cli/tests/e2e/fixtures/kilo-native-generation/AGENTS.md @@ -0,0 +1,7 @@ +# Project context + +User content must survive sync unchanged. + + +[aidd_docs/memory/architecture.md](aidd_docs/memory/architecture.md) + diff --git a/cli/tests/e2e/fixtures/kilo-native-generation/aidd_docs/memory/architecture.md b/cli/tests/e2e/fixtures/kilo-native-generation/aidd_docs/memory/architecture.md new file mode 100644 index 000000000..3ed8c2446 --- /dev/null +++ b/cli/tests/e2e/fixtures/kilo-native-generation/aidd_docs/memory/architecture.md @@ -0,0 +1,3 @@ +# Architecture fixture + +MEMORY_APPLIED:prune-6842 diff --git a/cli/tests/e2e/fixtures/kilo-native-generation/manifest.json b/cli/tests/e2e/fixtures/kilo-native-generation/manifest.json new file mode 100644 index 000000000..4db50aec0 --- /dev/null +++ b/cli/tests/e2e/fixtures/kilo-native-generation/manifest.json @@ -0,0 +1,16 @@ +{ + ".kilo/agents/assets/agent-payload.txt": "9c3390da19f19eb9b6a2c18d39507968645b9f72a39994fdd3106fae88e896d2", + ".kilo/agents/verify-agent.md": "56c2594c982078fd9ee1d5d97876f7aca64c0d477efd28e3601469097a812559", + ".kilo/commands/assets/workflow-payload.txt": "0a78981384c13c40dacca78620f032effb4e4ae4ed32793e8f840174ae7a9029", + ".kilo/commands/verify-workflow.md": "294ea171924f5f8c9f845eaf7d2fc111c4a7a6b727c81512926ead00b2a7671e", + ".kilo/kilo.jsonc": "a4255bc07e3b98e49e1274e0c1cc798e1de19e3f101147d672333efddd2ad16d", + ".kilo/rules/01-standards/1-naming.md": "bc4dfe2d38cd1579331d629ffdd19227a23bd2e873ca73f731262bb8c2c58461", + ".kilo/skills/verify-payload/SKILL.md": "ebab5bcc394e82d96a3d128872adb909fcedbeb0b29ef40ac45c16798afa7aff", + ".kilo/skills/verify-payload/actions/01-verify.md": "60a386a114507203fd59706de5ba0b9ca541cd30b0a0b074e16c8289b25d6c68", + ".kilo/skills/verify-payload/actions/02-user.md": "126a3488d7e981c0fe9ac0211bbebceb6799ec14e03bc02d2acb2e6d44b3ef95", + ".kilo/skills/verify-payload/assets/payload.txt": "7c9e202409a70393756a0ab9c0203c49f8d2c3862ab787b7d2879b3dc80e12b8", + ".kilo/skills/verify-payload/assets/user.txt": "2f80347010a4451aed834ff0923f837e069fd5f5f3bf79d48c28fe060a0a5f59", + "AGENTS.md": "fd08a2f1f2ad59ed834dd22177fb56d355dcd1a8ee8d7b89eb3a290db84a08f5", + "aidd_docs/memory/architecture.md": "a2145c9822239ade675c57b230e9655c4ad70b29cc86c4250102b48e86a3ded0", + "negative-config.jsonc": "7c241f295ed17c6992ce2e672b75d318a89a5a02d8e00bf18e0bc457ee023811" +} diff --git a/cli/tests/e2e/fixtures/kilo-native-generation/negative-config.jsonc b/cli/tests/e2e/fixtures/kilo-native-generation/negative-config.jsonc new file mode 100644 index 000000000..80d226e23 --- /dev/null +++ b/cli/tests/e2e/fixtures/kilo-native-generation/negative-config.jsonc @@ -0,0 +1,3 @@ +{ + "instructions": [], +} diff --git a/cli/tests/e2e/helpers/kilo-local-model.ts b/cli/tests/e2e/helpers/kilo-local-model.ts new file mode 100644 index 000000000..a19428501 --- /dev/null +++ b/cli/tests/e2e/helpers/kilo-local-model.ts @@ -0,0 +1,120 @@ +import { createServer } from "node:http"; + +export type KiloChatMessage = { + role: string; + content: unknown; + name?: string; + tool_call_id?: string; +}; + +export type KiloChatTool = { + type?: string; + function: { + name: string; + parameters?: unknown; + }; +}; + +export type KiloChatRequest = { + messages: KiloChatMessage[]; + tools?: KiloChatTool[]; +}; + +export type LocalModelReply = + | { content: string } + | { toolCall: { id: string; name: string; arguments: Record } }; + +type ScriptedLocalModel = { + respond: (request: KiloChatRequest, requestIndex: number) => LocalModelReply; +}; + +export async function localModel( + readPathOrScript: string | ScriptedLocalModel = "probe.txt", + finalAnswer = "KILO_LOCAL_READ_CONFIRMED" +) { + const script = typeof readPathOrScript === "string" ? undefined : readPathOrScript; + const readPath = typeof readPathOrScript === "string" ? readPathOrScript : "probe.txt"; + const requests: KiloChatRequest[] = []; + const replies: LocalModelReply[] = []; + let readSent = false; + const server = createServer((request, response) => { + let body = ""; + request.on("data", (chunk) => { + body += String(chunk); + }); + request.on("end", () => { + if (request.url !== "/v1/chat/completions") { + response.writeHead(404).end(); + return; + } + + const input = JSON.parse(body) as KiloChatRequest; + const requestIndex = requests.push(input) - 1; + let reply: LocalModelReply; + if (script !== undefined) { + reply = script.respond(input, requestIndex); + } else if (!readSent && input.tools?.some((tool) => tool.function.name === "read")) { + readSent = true; + reply = { + toolCall: { + id: "call_local_read", + name: "read", + arguments: { filePath: readPath }, + }, + }; + } else { + reply = { content: finalAnswer }; + } + replies.push(reply); + + response.writeHead(200, { "content-type": "text/event-stream" }); + const chunk = (delta: unknown, finish_reason: string | null = null) => { + response.write( + `data: ${JSON.stringify({ id: "chatcmpl-local", object: "chat.completion.chunk", created: 1, model: "fake", choices: [{ index: 0, delta, finish_reason }] })}\n\n` + ); + }; + chunk({ role: "assistant" }); + if ("toolCall" in reply) { + chunk({ + tool_calls: [ + { + index: 0, + id: reply.toolCall.id, + type: "function", + function: { + name: reply.toolCall.name, + arguments: JSON.stringify(reply.toolCall.arguments), + }, + }, + ], + }); + chunk({}, "tool_calls"); + } else { + chunk({ content: reply.content }); + chunk({}, "stop"); + } + response.end("data: [DONE]\n\n"); + }); + }); + await new Promise((resolve, reject) => { + const onListening = () => { + server.off("error", onError); + resolve(); + }; + const onError = (error: Error) => { + server.off("listening", onListening); + reject(error); + }; + server.once("error", onError); + server.once("listening", onListening); + server.listen(0, "127.0.0.1"); + }); + const address = server.address(); + if (address === null || typeof address === "string") throw new Error("Missing model port"); + return { + server, + requests, + replies, + url: `http://127.0.0.1:${address.port}/v1`, + }; +} diff --git a/cli/tests/e2e/kilo-native-generation.e2e.test.ts b/cli/tests/e2e/kilo-native-generation.e2e.test.ts new file mode 100644 index 000000000..69ca7b999 --- /dev/null +++ b/cli/tests/e2e/kilo-native-generation.e2e.test.ts @@ -0,0 +1,759 @@ +import { createHash, randomUUID } from "node:crypto"; +import { cp, mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises"; +import { dirname, join, relative } from "node:path"; +import { describe, expect, it } from "vitest"; +import { REPOSITORY_ROOT } from "../helpers/repository-root.js"; +import { + type KiloChatRequest, + type KiloChatTool, + type LocalModelReply, + localModel, +} from "./helpers/kilo-local-model.js"; +import { createTestEnv, execFileAsync } from "./helpers.js"; + +const describeNative = process.env.KILO_NATIVE_GENERATION === "1" ? describe : describe.skip; +const fixtures = join(REPOSITORY_ROOT, "cli", "tests", "e2e", "fixtures", "kilo-native-generation"); +const skill = join(fixtures, ".kilo", "skills", "verify-payload"); +const codeFence = "```"; +const phase6GeneratedSkill = { + "SKILL.md": `--- +name: verify-payload +description: Reads the bundled payload file and replies with its exact content. Use when the user wants to verify the payload or retrieve the contents of assets/payload.txt. +--- + +# Verify Payload + +${codeFence}mermaid +flowchart LR + start([request]) --> verify --> done([payload content returned]) +${codeFence} + +## Actions + +Read [01-verify.md](actions/01-verify.md) to run the verify action. + +| Action | Does | +| --- | --- | +| verify | read the payload and reply with its content | +`, + "actions/01-verify.md": `# 01 - Verify + +## Input + +The bundled payload file. + +## Output + +A reply containing the exact payload file content. + +## Process + +1. **Read.** Read [payload.txt](../assets/payload.txt) relative to this action file. +2. **Reply.** Return the complete content exactly as read, preserving whitespace and adding no commentary or formatting. + +## Test + +| Case | Pass | +| --- | --- | +| Run verify with the bundled payload | The reply matches the content of the payload file exactly | +`, + "assets/payload.txt": "PHASE6_SYNTHETIC_PAYLOAD\n", +} as const; +const phase6ArtifactHashes: Record = { + "SKILL.md": "ad472e5fa9663cc9a291525d8b9528e07d05411680a1eca9f9183dfab3185945", + "actions/01-verify.md": "583404c6f75ae05db061fbfbe90f0688636aba574ae832ec565282b056a7e534", + "assets/payload.txt": "e201189cdbacfb224377377efa94b45898c37abdf2145efa36a8926af772a5d7", +}; + +type ToolParameter = { type?: string; description?: string; enum?: unknown[] }; +type ObjectParameters = { + properties?: Record; + required?: string[]; +}; + +function toolByName(request: KiloChatRequest, name: string): KiloChatTool | undefined { + return request.tools?.find((tool) => tool.function.name === name); +} + +function stringArgument( + tool: KiloChatTool, + value: string, + fieldPattern: RegExp +): Record { + const schema = tool.function.parameters as ObjectParameters | undefined; + const properties = schema?.properties ?? {}; + const required = schema?.required ?? []; + const candidates = Object.entries(properties).filter(([key, property]) => { + const described = `${key} ${property.description ?? ""}`; + return property.type === "string" && fieldPattern.test(described); + }); + const matchingEnum = candidates.find(([, property]) => property.enum?.includes(value)); + const field = matchingEnum?.[0] ?? (candidates.length === 1 ? candidates[0][0] : undefined); + if (field === undefined || required.some((name) => name !== field)) { + throw new Error( + `Unrecognized Kilo ${tool.function.name} tool schema: ${JSON.stringify(tool.function.parameters)}` + ); + } + return { [field]: value }; +} + +function textLeaves(value: unknown): string[] { + if (typeof value === "string") return [value]; + if (Array.isArray(value)) return value.flatMap(textLeaves); + if (value !== null && typeof value === "object") { + const object = value as Record; + const text = typeof object.text === "string" ? [object.text] : []; + return [ + ...text, + ...Object.entries(object) + .filter(([key]) => key !== "text") + .flatMap(([, nested]) => textLeaves(nested)), + ]; + } + return []; +} + +function toolResult(request: KiloChatRequest, id: string): string | undefined { + const message = request.messages.find( + (candidate) => candidate.role === "tool" && candidate.tool_call_id === id + ); + return message === undefined ? undefined : textLeaves(message.content).join("\n"); +} + +function outputStrings(output: string): string[] { + return output + .split(/\r?\n/) + .filter(Boolean) + .flatMap((line) => { + try { + return textLeaves(JSON.parse(line)); + } catch { + return [line]; + } + }); +} + +describeNative("E2E: saved native generation in Kilo", () => { + it("merges coexisting project instructions and deduplicates identical references", async () => { + const { tempDir, projectDir, fakeHome, cleanup } = await createTestEnv( + "kilo-config-instruction-merge" + ); + const rootMarker = "KILO_ROOT_CONFIG_RULE_914"; + const nestedMarker = "KILO_NESTED_CONFIG_RULE_914"; + const duplicateMarker = "KILO_DUPLICATE_CONFIG_RULE_914"; + const scenarios = [ + { + name: "both-configs", + root: [".kilo/rules/01-standards/1-root.md"], + nested: [".kilo/rules/01-standards/2-nested.md"], + expected: { root: 1, nested: 1, duplicate: 0 }, + }, + { + name: "root-empty", + root: [], + nested: [".kilo/rules/01-standards/2-nested.md"], + expected: { root: 0, nested: 1, duplicate: 0 }, + }, + { + name: "nested-empty", + root: [".kilo/rules/01-standards/1-root.md"], + nested: [], + expected: { root: 1, nested: 0, duplicate: 0 }, + }, + { + name: "duplicate-reference", + root: [".kilo/rules/01-standards/3-duplicate.md"], + nested: [".kilo/rules/01-standards/3-duplicate.md"], + expected: { root: 0, nested: 0, duplicate: 1 }, + }, + ] as const; + let currentScenario = ""; + const observations: Array<{ + scenario: string; + root: number; + nested: number; + duplicate: number; + }> = []; + let model: Awaited> | undefined; + try { + const xdg = join(tempDir, "xdg"); + const env = { + ...process.env, + HOME: fakeHome, + XDG_CONFIG_HOME: join(xdg, "config"), + XDG_DATA_HOME: join(xdg, "data"), + XDG_STATE_HOME: join(xdg, "state"), + XDG_CACHE_HOME: join(xdg, "cache"), + KILO_PURE: "1", + KILO_DISABLE_EXTERNAL_SKILLS: "true", + }; + for (const name of ["config", "data", "state", "cache"]) { + await mkdir(join(xdg, name), { recursive: true }); + } + const localModelProvider = await localModel({ + respond(request) { + const systemMessages = request.messages + .filter((message) => message.role === "system") + .map((message) => JSON.stringify(message.content)) + .join("\n"); + const count = (marker: string) => systemMessages.split(marker).length - 1; + const observed = { + scenario: currentScenario, + root: count(rootMarker), + nested: count(nestedMarker), + duplicate: count(duplicateMarker), + }; + observations.push(observed); + return { + content: `KILO_CONFIG_${observed.scenario}:root=${observed.root};nested=${observed.nested};duplicate=${observed.duplicate}`, + }; + }, + }); + model = localModelProvider; + + const kilo = async (directory: string, ...args: string[]) => { + const result = await execFileAsync("kilo", args, { + cwd: directory, + env, + timeout: 30000, + maxBuffer: 8 * 1024 * 1024, + }); + return result.stdout; + }; + expect((await kilo(projectDir, "--version")).trim()).toBe("7.7.5"); + + for (const scenario of scenarios) { + currentScenario = scenario.name; + const directory = join(projectDir, scenario.name); + const rules = join(directory, ".kilo", "rules", "01-standards"); + const scenarioXdg = join(xdg, scenario.name); + await mkdir(rules, { recursive: true }); + for (const name of ["config", "data", "state", "cache"]) { + await mkdir(join(scenarioXdg, name), { recursive: true }); + } + await writeFile(join(rules, "1-root.md"), `# Root rule\n${rootMarker}\n`); + await writeFile(join(rules, "2-nested.md"), `# Nested rule\n${nestedMarker}\n`); + await writeFile(join(rules, "3-duplicate.md"), `# Shared rule\n${duplicateMarker}\n`); + await writeFile( + join(directory, "kilo.jsonc"), + JSON.stringify({ instructions: scenario.root }) + ); + await writeFile( + join(directory, ".kilo", "kilo.jsonc"), + JSON.stringify({ + instructions: scenario.nested, + model: "openai-compatible/fake", + provider: { + "openai-compatible": { + options: { baseURL: localModelProvider.url }, + models: { + fake: { + name: "Local deterministic config merge test", + tool_call: true, + limit: { context: 8192, output: 256 }, + }, + }, + }, + }, + }) + ); + + const scenarioEnv = { + ...env, + XDG_CONFIG_HOME: join(scenarioXdg, "config"), + XDG_DATA_HOME: join(scenarioXdg, "data"), + XDG_STATE_HOME: join(scenarioXdg, "state"), + XDG_CACHE_HOME: join(scenarioXdg, "cache"), + }; + const result = await execFileAsync( + "kilo", + [ + "run", + "--pure", + "--dir", + directory, + "--format", + "json", + "--auto", + "--model", + "openai-compatible/fake", + "Return the observed instruction counts.", + ], + { + cwd: directory, + env: scenarioEnv, + timeout: 30000, + maxBuffer: 8 * 1024 * 1024, + } + ); + const expected = scenario.expected; + const answer = `KILO_CONFIG_${scenario.name}:root=${expected.root};nested=${expected.nested};duplicate=${expected.duplicate}`; + expect(outputStrings(result.stdout)).toContain(answer); + expect(observations.filter((item) => item.scenario === scenario.name)).toEqual([ + { scenario: scenario.name, ...expected }, + ]); + } + } finally { + const server = model?.server; + if (server) { + server.closeAllConnections(); + await new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())) + ); + } + await cleanup(); + } + }, 180000); + + it("loads the native skill, follows its action, and answers from the read payload", async () => { + const { tempDir, projectDir, fakeHome, cleanup } = await createTestEnv( + "kilo-native-skill-consumption" + ); + const observations: { + skillParameters?: unknown; + skillResult?: string; + toolMessagesAfterAction?: Array<{ id?: string; name?: string; text: string }>; + actionResult?: string; + payloadResult?: string; + finalAnswer?: string; + skillArguments?: Record; + negativeSkillResult?: string; + negativeFailureStage?: string; + negativeReadCalls: number; + } = { negativeReadCalls: 0 }; + let positiveModel: Awaited> | undefined; + let negativeModel: Awaited> | undefined; + try { + const xdg = join(tempDir, "xdg"); + const env = { + ...process.env, + HOME: fakeHome, + XDG_CONFIG_HOME: join(xdg, "config"), + XDG_DATA_HOME: join(xdg, "data"), + XDG_STATE_HOME: join(xdg, "state"), + XDG_CACHE_HOME: join(xdg, "cache"), + KILO_PURE: "1", + KILO_DISABLE_EXTERNAL_SKILLS: "true", + KILO_DISABLE_SKILL_SHELL: "true", + }; + for (const name of ["config", "data", "state", "cache"]) { + await mkdir(join(xdg, name), { recursive: true }); + } + const kilo = async (...args: string[]) => { + const result = await execFileAsync("kilo", args, { + cwd: projectDir, + env, + timeout: 30000, + maxBuffer: 8 * 1024 * 1024, + }); + return result.stdout; + }; + const configureModel = async (baseURL: string) => + writeFile( + join(projectDir, "kilo.jsonc"), + JSON.stringify({ + model: "openai-compatible/fake", + provider: { + "openai-compatible": { + options: { baseURL }, + models: { + fake: { + name: "Local deterministic phase 6 test", + tool_call: true, + limit: { context: 8192, output: 256 }, + }, + }, + }, + }, + permission: { + "*": "deny", + external_directory: "deny", + read: "allow", + skill: { "*": "deny", "verify-payload": "allow" }, + }, + }) + ); + expect((await kilo("--version")).trim()).toBe("7.7.5"); + + const skillDir = join(projectDir, ".kilo", "skills", "verify-payload"); + const actionPath = ".kilo/skills/verify-payload/actions/01-verify.md"; + const payloadPath = ".kilo/skills/verify-payload/assets/payload.txt"; + for (const [relative, content] of Object.entries(phase6GeneratedSkill)) { + const path = join(skillDir, relative); + await mkdir(dirname(path), { recursive: true }); + await writeFile(path, content); + expect(createHash("sha256").update(content).digest("hex")).toBe( + phase6ArtifactHashes[relative as keyof typeof phase6ArtifactHashes] + ); + } + const payloadMarker = `PHASE6_KILO_PAYLOAD_${randomUUID()}`; + await writeFile(join(skillDir, "assets", "payload.txt"), `${payloadMarker}\n`); + const catalog = JSON.parse(await kilo("debug", "skill", "--pure")) as Array<{ + name: string; + location?: string; + }>; + expect(catalog.filter((entry) => entry.name === "verify-payload")).toHaveLength(1); + expect( + catalog.find((entry) => entry.name === "verify-payload")?.location?.replaceAll("\\", "/") + ).toContain(".kilo/skills/verify-payload/SKILL.md"); + + let positiveStage = "skill"; + positiveModel = await localModel({ + respond(request): LocalModelReply { + if (positiveStage === "skill") { + const skillTool = toolByName(request, "skill"); + if (skillTool === undefined) return { content: "PHASE6_SKILL_TOOL_UNAVAILABLE" }; + observations.skillParameters = skillTool.function.parameters; + observations.skillArguments = stringArgument( + skillTool, + "verify-payload", + /skill|name/i + ); + positiveStage = "router"; + return { + toolCall: { + id: "phase6_skill", + name: skillTool.function.name, + arguments: observations.skillArguments, + }, + }; + } + if (positiveStage === "router") { + observations.skillResult = toolResult(request, "phase6_skill"); + if ( + observations.skillResult === undefined || + !observations.skillResult.includes("# Verify Payload") || + !observations.skillResult.includes("01-verify.md") + ) { + return { content: "PHASE6_ROUTER_LOAD_FAILED" }; + } + const readTool = toolByName(request, "read"); + if (readTool === undefined) return { content: "PHASE6_READ_TOOL_UNAVAILABLE" }; + positiveStage = "action"; + return { + toolCall: { + id: "phase6_action", + name: readTool.function.name, + arguments: stringArgument(readTool, actionPath, /path|file/i), + }, + }; + } + if (positiveStage === "action") { + observations.toolMessagesAfterAction = request.messages + .filter((message) => message.role === "tool") + .map((message) => ({ + id: message.tool_call_id, + name: message.name, + text: textLeaves(message.content).join("\n").slice(0, 1200), + })); + observations.actionResult = toolResult(request, "phase6_action"); + if ( + observations.actionResult === undefined || + !observations.actionResult.includes("payload.txt") || + !observations.actionResult.includes("complete content exactly as read") + ) { + return { content: "PHASE6_ACTION_LOAD_FAILED" }; + } + const readTool = toolByName(request, "read"); + if (readTool === undefined) return { content: "PHASE6_READ_TOOL_UNAVAILABLE" }; + positiveStage = "payload"; + return { + toolCall: { + id: "phase6_payload", + name: readTool.function.name, + arguments: stringArgument(readTool, payloadPath, /path|file/i), + }, + }; + } + observations.payloadResult = toolResult(request, "phase6_payload"); + const marker = observations.payloadResult?.match(/PHASE6_KILO_PAYLOAD_[0-9a-f-]+/i)?.[0]; + if (marker === undefined) return { content: "PHASE6_PAYLOAD_READ_FAILED" }; + observations.finalAnswer = `${marker}\n`; + positiveStage = "done"; + return { content: observations.finalAnswer }; + }, + }); + await configureModel(positiveModel.url); + const filesBeforeRun = (await readdir(projectDir, { recursive: true, withFileTypes: true })) + .filter((entry) => entry.isFile()) + .map((entry) => join(relative(projectDir, entry.parentPath), entry.name)) + .sort(); + expect(filesBeforeRun).toEqual( + [ + ".kilo/.gitignore", + ".kilo/skills/verify-payload/SKILL.md", + ".kilo/skills/verify-payload/actions/01-verify.md", + ".kilo/skills/verify-payload/assets/payload.txt", + "kilo.jsonc", + ].sort() + ); + const positive = await kilo( + "run", + "--pure", + "--dir", + projectDir, + "--format", + "json", + "--model", + "openai-compatible/fake", + "Use the verify-payload skill and follow its verify action. Read its bundled payload and return only the exact content." + ); + expect(observations.skillParameters).toBeDefined(); + expect(observations.skillResult).toContain("# Verify Payload"); + expect( + observations.actionResult, + JSON.stringify(observations.toolMessagesAfterAction) + ).toContain("complete content exactly as read"); + expect(observations.payloadResult).toContain(payloadMarker); + expect(observations.finalAnswer).toBe(`${payloadMarker}\n`); + expect(outputStrings(positive)).toContain(`${payloadMarker}\n`); + expect( + positiveModel.replies.map((reply) => ("toolCall" in reply ? reply.toolCall.name : "final")) + ).toEqual(["skill", "read", "read", "final"]); + + await rm(skillDir, { recursive: true }); + const negativeCatalog = JSON.parse(await kilo("debug", "skill", "--pure")) as Array<{ + name: string; + }>; + expect(negativeCatalog.some((entry) => entry.name === "verify-payload")).toBe(false); + let negativeStage = "skill"; + negativeModel = await localModel({ + respond(request): LocalModelReply { + if (negativeStage === "skill") { + const skillTool = toolByName(request, "skill"); + if (skillTool === undefined || observations.skillArguments === undefined) { + observations.negativeFailureStage = "skill-unavailable"; + return { content: "PHASE6_NEGATIVE_SKILL_UNAVAILABLE" }; + } + negativeStage = "result"; + return { + toolCall: { + id: "phase6_negative_skill", + name: skillTool.function.name, + arguments: observations.skillArguments, + }, + }; + } + observations.negativeSkillResult = toolResult(request, "phase6_negative_skill"); + observations.negativeFailureStage = "skill-load-absent"; + observations.negativeReadCalls = + negativeModel?.replies.filter( + (reply) => "toolCall" in reply && reply.toolCall.name === "read" + ).length ?? 0; + return { content: "PHASE6_NEGATIVE_SKILL_LOAD_FAILED" }; + }, + }); + await configureModel(negativeModel.url); + const negative = await kilo( + "run", + "--pure", + "--dir", + projectDir, + "--format", + "json", + "--model", + "openai-compatible/fake", + "Use the verify-payload skill and follow its verify action. Read its bundled payload and return only the exact content." + ); + expect(observations.negativeFailureStage).toBe("skill-load-absent"); + expect(observations.negativeSkillResult).toBeDefined(); + expect(observations.negativeSkillResult).toMatch( + /not found|unknown skill|unavailable|does not exist|could not load/i + ); + expect(observations.negativeSkillResult).not.toContain("# Verify Payload"); + expect(observations.negativeReadCalls).toBe(0); + expect(negative).not.toContain(payloadMarker); + expect( + negativeModel.replies.map((reply) => ("toolCall" in reply ? reply.toolCall.name : "final")) + ).toEqual(["skill", "final"]); + expect(new URL(positiveModel.url).hostname).toBe("127.0.0.1"); + expect(new URL(negativeModel.url).hostname).toBe("127.0.0.1"); + } finally { + for (const model of [positiveModel, negativeModel]) { + if (model !== undefined) { + model.server.closeAllConnections(); + await new Promise((resolve, reject) => + model.server.close((error) => (error ? reject(error) : resolve())) + ); + } + } + await cleanup(); + } + }, 120000); + + it("loads checked-in skill, rule, agent and workflow fixtures without changing memory", async () => { + const { tempDir, projectDir, fakeHome, cleanup } = + await createTestEnv("kilo-native-generation"); + try { + const xdg = join(tempDir, "xdg"); + const env = { + ...process.env, + HOME: fakeHome, + XDG_CONFIG_HOME: join(xdg, "config"), + XDG_DATA_HOME: join(xdg, "data"), + XDG_STATE_HOME: join(xdg, "state"), + XDG_CACHE_HOME: join(xdg, "cache"), + }; + for (const name of ["config", "data", "state", "cache"]) { + await mkdir(join(xdg, name), { recursive: true }); + } + const kilo = async (...args: string[]) => { + const result = await execFileAsync("kilo", args, { + cwd: projectDir, + env, + timeout: 30000, + maxBuffer: 8 * 1024 * 1024, + }); + return result.stdout; + }; + expect((await kilo("--version")).trim()).toBe("7.7.5"); + + const fixtureManifest = JSON.parse( + await readFile(join(fixtures, "manifest.json"), "utf8") + ) as Record; + for (const [relative, expectedHash] of Object.entries(fixtureManifest)) { + const source = await readFile(join(fixtures, relative)); + expect(createHash("sha256").update(source).digest("hex")).toBe(expectedHash); + } + + const savedAgent = join(fixtures, ".kilo", "agents"); + const savedCommand = join(fixtures, ".kilo", "commands"); + const savedRule = join(fixtures, ".kilo", "rules", "01-standards", "1-naming.md"); + const rule = join(projectDir, ".kilo", "rules", "01-standards", "1-naming.md"); + await cp(join(fixtures, "AGENTS.md"), join(projectDir, "AGENTS.md")); + await mkdir(join(projectDir, "aidd_docs", "memory"), { recursive: true }); + await cp( + join(fixtures, "aidd_docs", "memory", "architecture.md"), + join(projectDir, "aidd_docs", "memory", "architecture.md") + ); + await cp(skill, join(projectDir, ".kilo", "skills", "verify-payload"), { recursive: true }); + await cp(savedAgent, join(projectDir, ".kilo", "agents"), { recursive: true }); + await cp(savedCommand, join(projectDir, ".kilo", "commands"), { recursive: true }); + await mkdir(join(projectDir, ".kilo", "rules", "01-standards"), { recursive: true }); + await cp(savedRule, rule); + await cp(join(fixtures, ".kilo", "kilo.jsonc"), join(projectDir, ".kilo", "kilo.jsonc")); + + for (const relative of Object.keys(fixtureManifest)) { + if (relative === "negative-config.jsonc") continue; + expect(await readFile(join(projectDir, relative))).toEqual( + await readFile(join(fixtures, relative)) + ); + } + expect(await readFile(rule)).toEqual(await readFile(savedRule)); + + const config = JSON.parse(await kilo("debug", "config", "--pure")) as { + instructions?: string[]; + agent?: Record; + command?: Record; + }; + expect(config.instructions).toContain(".kilo/rules/01-standards/1-naming.md"); + expect(config.agent?.["verify-agent"]?.prompt).toContain("agent-payload.txt"); + expect(config.command?.["verify-workflow"]?.agent).toBe("verify-agent"); + expect(config.command?.["verify-workflow"]?.template).toContain("verify-agent"); + const catalog = JSON.parse(await kilo("debug", "skill", "--pure")) as Array<{ + name: string; + location?: string; + }>; + expect(catalog.filter((entry) => entry.name === "verify-payload")).toHaveLength(1); + expect( + catalog.find((entry) => entry.name === "verify-payload")?.location?.replaceAll("\\", "/") + ).toContain(".kilo/skills/verify-payload/SKILL.md"); + + // String paths keep direct reads; isolated Kilo proves request/tool flow, not external reasoning. + const model = await localModel("aidd_docs/memory/architecture.md"); + try { + await writeFile( + join(projectDir, "kilo.jsonc"), + JSON.stringify({ + model: "openai-compatible/fake", + provider: { + "openai-compatible": { + options: { baseURL: model.url }, + models: { + fake: { + name: "Local fake", + tool_call: true, + limit: { context: 8192, output: 256 }, + }, + }, + }, + }, + }) + ); + const positive = await kilo( + "run", + "--pure", + "--dir", + projectDir, + "--format", + "json", + "--auto", + "--model", + "openai-compatible/fake", + "Read the project memory." + ); + expect(positive).toContain("KILO_LOCAL_READ_CONFIRMED"); + expect(JSON.stringify(model.requests)).toContain("COBALT_914"); + expect(JSON.stringify(model.requests)).toContain( + "User content must survive sync unchanged" + ); + expect(model.requests.flatMap((request) => request.messages)).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + role: "tool", + content: expect.stringContaining("MEMORY_APPLIED:prune-6842"), + }), + ]) + ); + + const beforeNegative = model.requests.length; + await cp(join(fixtures, "negative-config.jsonc"), join(projectDir, ".kilo", "kilo.jsonc")); + await rm(join(projectDir, ".kilo", "skills", "verify-payload"), { recursive: true }); + await rm(join(projectDir, ".kilo", "agents"), { recursive: true }); + await rm(join(projectDir, ".kilo", "commands"), { recursive: true }); + await kilo( + "run", + "--pure", + "--dir", + projectDir, + "--format", + "json", + "--auto", + "--model", + "openai-compatible/fake", + "Check the project context again." + ); + expect(model.requests.length).toBeGreaterThan(beforeNegative); + expect(JSON.stringify(model.requests.slice(beforeNegative))).not.toContain("COBALT_914"); + } finally { + model.server.closeAllConnections(); + await new Promise((resolve, reject) => + model.server.close((error) => (error ? reject(error) : resolve())) + ); + } + const negativeConfig = JSON.parse(await kilo("debug", "config", "--pure")) as { + instructions?: string[]; + agent?: Record; + command?: Record; + }; + expect(negativeConfig.instructions ?? []).not.toContain( + ".kilo/rules/01-standards/1-naming.md" + ); + expect(negativeConfig.agent ?? {}).not.toHaveProperty("verify-agent"); + expect(negativeConfig.command ?? {}).not.toHaveProperty("verify-workflow"); + const negativeCatalog = JSON.parse(await kilo("debug", "skill", "--pure")) as Array<{ + name: string; + }>; + expect(negativeCatalog.some((entry) => entry.name === "verify-payload")).toBe(false); + expect(await readFile(rule)).toEqual(await readFile(savedRule)); + expect(await readFile(join(projectDir, "AGENTS.md"))).toEqual( + await readFile(join(fixtures, "AGENTS.md")) + ); + expect(await readFile(join(projectDir, "aidd_docs", "memory", "architecture.md"))).toEqual( + await readFile(join(fixtures, "aidd_docs", "memory", "architecture.md")) + ); + } finally { + await cleanup(); + } + }, 180000); +}); diff --git a/cli/tests/e2e/kilo-runtime.e2e.test.ts b/cli/tests/e2e/kilo-runtime.e2e.test.ts index 22b4ecea6..a856b1cd7 100644 --- a/cli/tests/e2e/kilo-runtime.e2e.test.ts +++ b/cli/tests/e2e/kilo-runtime.e2e.test.ts @@ -1,11 +1,11 @@ import { spawn } from "node:child_process"; import { existsSync } from "node:fs"; import { mkdir, readdir, readFile, writeFile } from "node:fs/promises"; -import { createServer } from "node:http"; import { delimiter, join } from "node:path"; import { setTimeout as delay } from "node:timers/promises"; import { describe, expect, it } from "vitest"; import { REPOSITORY_ROOT } from "../helpers/repository-root.js"; +import { localModel } from "./helpers/kilo-local-model.js"; import { createTestEnv, execFileAsync, runCli, sandboxedEnv } from "./helpers.js"; const describeKiloRuntime = process.env.KILO_RUNTIME_SMOKE === "1" ? describe : describe.skip; @@ -64,57 +64,6 @@ async function markerContent(path: string): Promise { } } -async function localModel() { - const requests: Array<{ messages: Array<{ role: string; content: unknown }> }> = []; - let readSent = false; - const server = createServer((request, response) => { - let body = ""; - request.on("data", (chunk) => { - body += String(chunk); - }); - request.on("end", () => { - if (request.url !== "/v1/chat/completions") { - response.writeHead(404).end(); - return; - } - const input = JSON.parse(body) as { - messages: Array<{ role: string; content: unknown }>; - tools?: Array<{ function: { name: string } }>; - }; - requests.push(input); - response.writeHead(200, { "content-type": "text/event-stream" }); - const chunk = (delta: unknown, finish_reason: string | null = null) => { - response.write( - `data: ${JSON.stringify({ id: "chatcmpl-local", object: "chat.completion.chunk", created: 1, model: "fake", choices: [{ index: 0, delta, finish_reason }] })}\n\n` - ); - }; - chunk({ role: "assistant" }); - if (!readSent && input.tools?.some((tool) => tool.function.name === "read")) { - readSent = true; - chunk({ - tool_calls: [ - { - index: 0, - id: "call_local_read", - type: "function", - function: { name: "read", arguments: '{"filePath":"probe.txt"}' }, - }, - ], - }); - chunk({}, "tool_calls"); - } else { - chunk({ content: "KILO_LOCAL_READ_CONFIRMED" }); - chunk({}, "stop"); - } - response.end("data: [DONE]\n\n"); - }); - }); - await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); - const address = server.address(); - if (address === null || typeof address === "string") throw new Error("Missing model port"); - return { server, requests, url: `http://127.0.0.1:${address.port}/v1` }; -} - function alive(pid: number): boolean { try { process.kill(pid, 0); diff --git a/cli/tests/e2e/sandbox-reaches-no-tool-binary.e2e.test.ts b/cli/tests/e2e/sandbox-reaches-no-tool-binary.e2e.test.ts index 35bab5a7a..932212ddb 100644 --- a/cli/tests/e2e/sandbox-reaches-no-tool-binary.e2e.test.ts +++ b/cli/tests/e2e/sandbox-reaches-no-tool-binary.e2e.test.ts @@ -70,10 +70,9 @@ describe("E2E: the sandbox a test spawns into", () => { try { const env = sandboxedEnv(fakeHome); - // A hook shells out to git, and every spawned command is node itself. - // A sandbox that reached neither would make this guard pass by breaking everything. + // The hook shells out to git; each spawned command is absolute Node, so the sandbox + // cannot pass vacuously by resolving neither executable from PATH. expect(await whichUnderSandbox("git", projectDir, env)).not.toBe(""); - // The node directory can also hold Codex, so callers deliberately use its absolute path. const { stdout } = await execFileAsync(process.execPath, ["--version"], { cwd: projectDir, env, diff --git a/plugins/aidd-context/README.md b/plugins/aidd-context/README.md index 8b723c276..82f77ced2 100644 --- a/plugins/aidd-context/README.md +++ b/plugins/aidd-context/README.md @@ -32,3 +32,15 @@ New to AIDD, or unsure what to run next? Invoke `aidd-context:00-onboard`. The s 3. Loops back to reading the project after each step so the guidance always reflects the current state. Onboard adapts to whatever plugins are installed: it suggests by function and discovers the skills that fill each step, so a skill added later shows up on its own. + +## Multi-tool capabilities + +The plugin supports Claude Code, Codex CLI, OpenCode, Cursor, GitHub Copilot, and Kilo. Onboarding detects installed AIDD plugins and their skills; each generator confirms the available host tools before preparing outputs. + +- **Project memory:** initialize and sync shared project context while preserving tool-specific links and imports. +- **Rules:** generate project guidance in each confirmed tool's supported format. +- **Skills:** create router-based skills and resolve native or portable placement where supported. +- **Agents and workflows:** generate tool-specific agents and one-shot commands where supported. +- **Hooks:** generate declarative hooks where supported and provide sourced plugin guidance otherwise; OpenCode and Kilo receive guidance only, and the generator writes no Kilo plugin or hook. See [hook paths](skills/08-hook-generate/references/tool-paths.md) and [hook authoring](skills/08-hook-generate/references/hook-authoring.md). + +Paths, formats, supported fields, and limits belong to each generator's tool references. diff --git a/plugins/aidd-context/hooks/update_memory.js b/plugins/aidd-context/hooks/update_memory.js index 2cfb4ba01..190f26408 100644 --- a/plugins/aidd-context/hooks/update_memory.js +++ b/plugins/aidd-context/hooks/update_memory.js @@ -42,6 +42,7 @@ const TOOL_FILES = { codex: "AGENTS.md", cursor: "AGENTS.md", opencode: "AGENTS.md", + kilo: "AGENTS.md", copilot: ".github/copilot-instructions.md", }; @@ -49,17 +50,117 @@ function memoryPath(path, ...parts) { return path.join(DOCS_DIR, MEMORY_SUBDIR, ...parts); } -// Opening directly rather than checking existence first touches the file once, so no -// time-of-check/time-of-use race. A real error still throws. -function readTextOrNull(fs, filePath) { +function sameIdentity(left, right) { + return left.dev === right.dev && left.ino === right.ino; +} + +// Open only an existing regular, single-link file. Keep its descriptor for any later write; +// path identity checks detect ordinary substitutions but cannot defeat a hostile process +// that swaps a parent away and back between checks. +function openProjectFile(fs, path, filePath) { + const root = fs.realpathSync(process.cwd()); + const rootIdentity = fs.statSync(root, { bigint: true }); + const parts = filePath.split("/"); + const inspect = () => { + const chain = [fs.statSync(root, { bigint: true })]; + let current = root; + for (let index = 0; index < parts.length; index++) { + current = path.join(current, parts[index]); + let stat; + try { + stat = fs.lstatSync(current, { bigint: true }); + } catch (err) { + if (err.code === "ENOENT") return null; + throw err; + } + const final = index === parts.length - 1; + if (stat.isSymbolicLink() || (final ? !stat.isFile() : !stat.isDirectory())) { + const error = new Error(`${filePath} has an unsafe destination, not synced`); + error.code = "AIDD_UNSAFE_PATH"; + throw error; + } + chain.push(stat); + } + return chain; + }; + const before = inspect(); + if (before === null) return null; + let fd; try { - return fs.readFileSync(filePath, "utf8"); + const flags = fs.constants.O_RDWR | + (process.platform === "win32" ? 0 : (fs.constants.O_NOFOLLOW || 0) | (fs.constants.O_NONBLOCK || 0)); + fd = fs.openSync(path.join(root, ...parts), flags); + const opened = fs.fstatSync(fd, { bigint: true }); + if (!opened.isFile() || opened.nlink > 1n || !sameIdentity(opened, before.at(-1))) { + const error = new Error(`${filePath} changed or has multiple hard links, not synced`); + error.code = "AIDD_UNSAFE_PATH"; + throw error; + } + const after = inspect(); + if (!after || before.length !== after.length || before.some((stat, index) => !sameIdentity(stat, after[index]))) { + const error = new Error(`${filePath} path changed while opening, not synced`); + error.code = "AIDD_UNSAFE_PATH"; + throw error; + } + const openedAgain = fs.fstatSync(fd, { bigint: true }); + if (!sameIdentity(opened, openedAgain)) { + const error = new Error(`${filePath} changed while opening, not synced`); + error.code = "AIDD_UNSAFE_PATH"; + throw error; + } + if (fs.realpathSync(root) !== root || !sameIdentity(rootIdentity, after[0])) { + const error = new Error(`${filePath} project root changed while opening, not synced`); + error.code = "AIDD_UNSAFE_PATH"; + throw error; + } + return { fd, root, parts, chain: after, identity: opened, rootIdentity }; } catch (err) { + if (fd !== undefined) fs.closeSync(fd); if (err.code === "ENOENT") return null; throw err; } } +function readOpenedText(fs, opened) { + return fs.readFileSync(opened.fd, "utf8"); +} + +// README is opt-in by marker. This probe is read-only; any marked destination is reopened +// through a verified descriptor before it can be written, while a blockless symlink is ignored. +function readTextOrNull(fs, filePath) { + try { return fs.readFileSync(filePath, "utf8"); } + catch (err) { + if (err.code === "ENOENT") return null; + throw err; + } +} + +function writeOpenedText(fs, path, filePath, opened, content) { + if ( + fs.realpathSync(opened.root) !== opened.root || + !sameIdentity(fs.statSync(opened.root, { bigint: true }), opened.rootIdentity) + ) { + throw new Error(`${filePath} project root changed before write, not synced`); + } + let current = opened.root; + for (let index = 0; index < opened.parts.length; index++) { + current = path.join(current, opened.parts[index]); + const stat = fs.lstatSync(current, { bigint: true }); + const final = index === opened.parts.length - 1; + if (stat.isSymbolicLink() || (final ? !stat.isFile() : !stat.isDirectory()) || !sameIdentity(stat, opened.chain[index + 1])) { + throw new Error(`${filePath} path changed before write, not synced`); + } + } + const descriptorStat = fs.fstatSync(opened.fd, { bigint: true }); + if (!sameIdentity(descriptorStat, opened.identity)) throw new Error(`${filePath} descriptor changed before write, not synced`); + + const bytes = Buffer.from(content, "utf8"); + let offset = 0; + while (offset < bytes.length) offset += fs.writeSync(opened.fd, bytes, offset, bytes.length - offset, offset); + // Truncate only after the complete replacement bytes are written; metadata and inode stay. + fs.ftruncateSync(opened.fd, bytes.length); +} + // Single-touch, like readTextOrNull: no existence check before reading. function readDirOrEmpty(fs, dir) { try { @@ -117,14 +218,12 @@ function buildBlockContent(rootFiles, onDemandFiles, syntax, prefix = "") { // Markers that each own their line, outside any code fence. Substring search would cut on // the quoted marker every upgrade note carries, mangling prose and missing the real block. -function findBlockLines(lines, open, close) { +function markerLines(lines, markers) { let fence = null; - let openLine = -1; - - for (let i = 0; i < lines.length; i++) { - const trimmed = lines[i].trim(); + const found = []; + for (let index = 0; index < lines.length; index++) { + const trimmed = lines[index].trim(); const opener = /^(`{3,}|~{3,})/u.exec(trimmed); - if (fence !== null) { // A fence closes only on the same character, repeated at least as often, // so a ``` inside a ```` example does not end it early. @@ -135,10 +234,17 @@ function findBlockLines(lines, open, close) { fence = opener[1]; continue; } - if (trimmed === open) openLine = i; - else if (trimmed === close && openLine !== -1) return { openLine, closeLine: i }; + if (markers.includes(trimmed)) found.push({ index, marker: trimmed }); } + return found; +} +function findBlockLines(lines, open, close) { + let openLine = -1; + for (const token of markerLines(lines, [open, close])) { + if (token.marker === open) openLine = token.index; + else if (openLine !== -1) return { openLine, closeLine: token.index }; + } return null; } @@ -147,11 +253,49 @@ function updateMarkers(content, open, close, innerContent) { const found = findBlockLines(lines, open, close); if (found === null) return null; - return ( - lines.slice(0, found.openLine + 1).join("\n") + - innerContent + - lines.slice(found.closeLine).join("\n") - ); + // Splice at line offsets, keeping both marker lines and every byte outside them. + // The opening line chooses the generated block's newline convention. + const start = lines.slice(0, found.openLine).reduce((size, line) => size + line.length + 1, 0); + const end = lines.slice(0, found.closeLine).reduce((size, line) => size + line.length + 1, 0); + const opening = lines[found.openLine]; + const eol = opening.endsWith("\r") ? "\r\n" : "\n"; + const openingEnd = start + opening.length - (opening.endsWith("\r") ? 1 : 0); + return content.slice(0, openingEnd) + innerContent.replace(/\n/gu, eol) + content.slice(end); +} + +// Explicit sync is an operation on the entire selected set. Accept no ambiguous block: +// exactly one matching modern or legacy pair, with examples outside the contract. +function validateMarkers(filePath, content, pairs, label) { + if (content === null) return; + const tokens = markerLines(content.split("\n"), pairs.flat()).map((token) => token.marker); + if (tokens.length === 0) return; + if (tokens.length === 2 && pairs.some(([open, close]) => tokens[0] === open && tokens[1] === close)) return; + throw new Error(`${filePath} has an unpaired ${label} marker or ambiguous block, not synced`); +} + +// Inspect each project-relative component without following symlinks. Missing files and +// directories are permitted so the skill can preflight before its separate Upsert step. +function validateDestination(fs, path, filePath) { + const parts = filePath.split("/"); + let current = fs.realpathSync(process.cwd()); + for (let index = 0; index < parts.length; index++) { + current = path.join(current, parts[index]); + let stat; + try { + stat = fs.lstatSync(current); + } catch (err) { + if (err.code === "ENOENT") { + fs.accessSync(path.dirname(current), fs.constants.W_OK | fs.constants.X_OK); + return; + } + throw err; + } + const final = index === parts.length - 1; + if (stat.isSymbolicLink() || (final ? !stat.isFile() : !stat.isDirectory())) { + throw new Error(`${filePath} has an unsafe destination, not synced`); + } + if (final) fs.accessSync(current, fs.constants.W_OK); + } } function migrateLegacyMarkers(content) { @@ -208,8 +352,7 @@ function resolveTargets(tools) { const unknown = tools.filter((t) => !(t in TOOL_FILES)); if (unknown.length > 0) { const known = Object.keys(TOOL_FILES).join(", "); - console.error(`update_memory: unknown tool ${unknown.join(", ")} (known: ${known})`); - process.exit(1); + throw new Error(`unknown tool ${unknown.join(", ")} (known: ${known})`); } const wanted = new Set(tools.map((t) => TOOL_FILES[t])); @@ -239,57 +382,130 @@ function gitAdd(childProcess, files) { const root = process.env.CLAUDE_PROJECT_DIR; if (root && fs.existsSync(root)) process.chdir(root); - if (!fs.existsSync(DOCS_DIR)) process.exit(0); - - const tools = process.argv.slice(2).map((arg) => arg.toLowerCase()); - const targets = resolveTargets(tools); - - const rootFiles = scanRootFiles(fs, path); - const onDemandFiles = ON_DEMAND_DIRS.flatMap((sub) => scanSubdir(fs, path, sub)); - const changed = []; - let unpaired = false; - - for (const target of targets) { - const original = readTextOrNull(fs, target.path); - if (original === null) continue; + const args = process.argv.slice(2).map((arg) => arg.toLowerCase()); + const checkOnly = args.includes("--check"); + const tools = args.filter((arg) => arg !== "--check"); + let targets; + try { + targets = resolveTargets(tools); + } catch (err) { + console.error(`update_memory: ${err.message}`); + process.exitCode = 1; + return; + } + const explicit = checkOnly || tools.length > 0; + const readmePath = memoryPath(path, MEMORY_README); + const originals = new Map(); + const openedFiles = new Map(); + let readmeOriginal; - const innerContent = buildBlockContent( - rootFiles, - onDemandFiles, - target.syntax, - relativePrefix(target.path), - ); - // Compared against what is on disk, not against the migrated text: an unchanged memory - // list would otherwise skip the write and leave the old markers in place forever. - const updated = updateBlock(migrateLegacyMarkers(original), innerContent); + try { + if (explicit) { + // Complete preflight before scanning the bank or writing the first target. This + // prevents known destination errors from leaving earlier selected files changed; + // it is not a transaction against concurrent edits or process interruption. + try { + for (const target of targets) { + validateDestination(fs, path, target.path); + const opened = openProjectFile(fs, path, target.path); + if (opened) openedFiles.set(target.path, opened); + const original = opened === null ? null : readOpenedText(fs, opened); + validateMarkers(target.path, original, [ + [BLOCK_OPEN, BLOCK_CLOSE], [LEGACY_BLOCK_OPEN, LEGACY_BLOCK_CLOSE], + ], "project memory"); + originals.set(target.path, original); + } + readmeOriginal = readTextOrNull(fs, readmePath); + if (readmeOriginal !== null && markerLines(readmeOriginal.split("\n"), [TOC_OPEN, TOC_CLOSE]).length > 0) { + validateDestination(fs, path, readmePath); + const readme = openProjectFile(fs, path, readmePath); + openedFiles.set(readmePath, readme); + readmeOriginal = readOpenedText(fs, readme); + validateMarkers(readmePath, readmeOriginal, [[TOC_OPEN, TOC_CLOSE]], "memory README"); + } + } catch (err) { + console.error(`update_memory: ${err.message}`); + process.exitCode = 1; + return; + } + if (checkOnly) return; + } + if (!fs.existsSync(DOCS_DIR)) return; + + const rootFiles = scanRootFiles(fs, path); + const onDemandFiles = ON_DEMAND_DIRS.flatMap((sub) => scanSubdir(fs, path, sub)); + const changed = []; + const pending = []; + + for (const target of targets) { + let opened = openedFiles.get(target.path); + let original = explicit ? originals.get(target.path) : undefined; + if (!explicit) { + try { + opened = openProjectFile(fs, path, target.path); + if (opened) openedFiles.set(target.path, opened); + original = opened === null ? null : readOpenedText(fs, opened); + } catch (err) { + console.error(`update_memory: ${err.message}`); + continue; + } + } + if (original === null) continue; + + const innerContent = buildBlockContent( + rootFiles, + onDemandFiles, + target.syntax, + relativePrefix(target.path), + ); + // Compared against what is on disk, not against the migrated text: an unchanged memory + // list would otherwise skip the write and leave the old markers in place forever. + const updated = updateBlock(migrateLegacyMarkers(original), innerContent); + + if (updated === null) { + if (!explicit) reportUnpairedMarkers(target.path, original); + continue; + } + if (updated === original) continue; + + if (explicit) pending.push({ path: target.path, content: updated, opened }); + else writeOpenedText(fs, path, target.path, opened, updated); + changed.push(target.path); + } - if (updated === null) { - if (reportUnpairedMarkers(target.path, original)) unpaired = true; - continue; + // Only if the README opts in with its own markers. + if (!explicit) { + readmeOriginal = readTextOrNull(fs, readmePath); + if (readmeOriginal !== null && markerLines(readmeOriginal.split("\n"), [TOC_OPEN, TOC_CLOSE]).length > 0) { + try { + const readme = openProjectFile(fs, path, readmePath); + openedFiles.set(readmePath, readme); + readmeOriginal = readOpenedText(fs, readme); + } catch (err) { + // Automatic mode is best-effort; an unsafe opted-in README is not written. + if (err.code !== "AIDD_UNSAFE_PATH") throw err; + readmeOriginal = null; + } + } + } + if (readmeOriginal !== null) { + const toc = buildToc(rootFiles, onDemandFiles, path); + const updated = updateMarkers(readmeOriginal, TOC_OPEN, TOC_CLOSE, toc); + if (updated !== null && updated !== readmeOriginal) { + const opened = openedFiles.get(readmePath); + if (explicit) pending.push({ path: readmePath, content: updated, opened }); + else writeOpenedText(fs, path, readmePath, opened, updated); + changed.push(readmePath); + } } - if (updated === original) continue; - fs.writeFileSync(target.path, updated, "utf8"); - changed.push(target.path); - } + for (const edit of pending) writeOpenedText(fs, path, edit.path, edit.opened, edit.content); - // Only if the README opts in with its own markers. - const readmePath = memoryPath(path, MEMORY_README); - const readmeOriginal = readTextOrNull(fs, readmePath); - if (readmeOriginal !== null) { - const toc = buildToc(rootFiles, onDemandFiles, path); - const updated = updateMarkers(readmeOriginal, TOC_OPEN, TOC_CLOSE, toc); - if (updated !== null && updated !== readmeOriginal) { - fs.writeFileSync(readmePath, updated, "utf8"); - changed.push(readmePath); - } + // Only as the auto hook, which owns no other change: called by the skill, staging its own + // two files would leave a partial index that reads like the whole change. + if (changed.length > 0 && tools.length === 0) gitAdd(childProcess, changed); + } finally { + for (const opened of openedFiles.values()) fs.closeSync(opened.fd); } - // Only as the auto hook, which owns no other change: called by the skill, staging its own - // two files would leave a partial index that reads like the whole change. - if (changed.length > 0 && tools.length === 0) gitAdd(childProcess, changed); - - // Only when tools were named, so the skill's sync action can stop: the auto hook must - // never fail a session start over a file the user has yet to repair. - if (unpaired && tools.length > 0) process.exit(1); })(); diff --git a/plugins/aidd-context/skills/00-onboard/references/state/detection.md b/plugins/aidd-context/skills/00-onboard/references/state/detection.md index f66011153..5fe9472c7 100644 --- a/plugins/aidd-context/skills/00-onboard/references/state/detection.md +++ b/plugins/aidd-context/skills/00-onboard/references/state/detection.md @@ -10,9 +10,14 @@ A tool is used when its own dir exists, or when a file only that tool reads exis | codex | `.codex/` | `AGENTS.md` | | cursor | `.cursor/` or `.cursorrules` | `AGENTS.md` | | opencode | `.opencode/` | `AGENTS.md` | +| kilo | `.kilo/` or `kilo.json` or `kilo.jsonc` or `.kilo/kilo.json` or `.kilo/kilo.jsonc` or `.kilocode/` | `AGENTS.md` | | copilot | `.github/copilot-instructions.md` or `.github/{instructions,agents,skills,prompts}/` | `.github/copilot-instructions.md` | - Detected tools only. An unused optional tool is omitted, never crossed. - No tool detected at all: the row reads `none yet`, uncrossed. The memory row's `❌` already carries the gap. - A used tool whose file lacks the block is not wired, and needs wiring. - Missing memory is a foundation status, not a tool row (see `zones.md`). + +Treat `.kilocode/` as a legacy Kilo detection signal, never an output path. OpenCode JSON config files do not identify Kilo. + +Kilo sources: [project configuration](https://kilo.ai/docs/getting-started/settings), [legacy directory fallback](https://github.com/Kilo-Org/kilocode/blob/main/packages/opencode/src/kilocode/skills/kilo-config.md), and [root `AGENTS.md`](https://kilo.ai/docs/customize/agents-md). diff --git a/plugins/aidd-context/skills/02-project-memory/actions/04-sync.md b/plugins/aidd-context/skills/02-project-memory/actions/04-sync.md index e96e752ae..afb1165b1 100644 --- a/plugins/aidd-context/skills/02-project-memory/actions/04-sync.md +++ b/plugins/aidd-context/skills/02-project-memory/actions/04-sync.md @@ -15,20 +15,25 @@ Each picked tool's context file, carrying the filled block. 1. **Require.** Stop unless `aidd_docs/memory/` holds a `.md`, sending the user to write the memory first. 2. **Detect.** Find the AI tools present per [tools.md](../references/tools.md). 3. **Pick.** Show every tool, the detected ones ticked, and wait for one or several. -4. **Upsert.** Ensure each picked tool's context file carries the block, per [tools.md](../references/tools.md). +4. **Preflight.** From the user's project root, resolve and deduplicate every picked destination per [tools.md](../references/tools.md), including the bank README when it opts into `files` markers. Inspect the complete set against its [explicit sync safety contract](../references/tools.md#explicit-sync-safety) before any creation or mutation. Stop without changing files if a destination, marker pair, ancestor, or write condition is invalid, ambiguous, or cannot be verified. Never infer a hook installation path from the project root or the active skill's location. +5. **Upsert.** Ensure each picked tool's context file carries the block, per [tools.md](../references/tools.md). - Absent file: create it from [AGENTS.md](../assets/templates/AGENTS.md). - Its AIDD structure differs: offer to reconcile it, applying only what the user approves. -5. **Fill.** Run `hooks/update_memory.js` from the project root, naming the picked tools, and stop on a non-zero exit. - - No script, the skill shipped alone: write each block from the bank. -6. **Verify.** Read each picked tool's block back and compare it to the bank. +6. **Fill.** Reinspect the complete selected set and opted-in README immediately before filling. Stop before filling if anything fails preflight. In each deduplicated context block, list sorted root `.md` files except `README.md`; list `.md` files under `internal/` and `external/` as plain read-on-demand paths, never imports. Use Claude `@` imports and relative Markdown links for the root files in other tools. Refresh the opted-in README `files` block with links relative to that README. Preserve all bytes outside owned blocks, including line endings. Leave unselected files untouched, skip byte-identical writes, and do not stage or add files to Git. +7. **Verify.** Read each picked tool's block and the opted-in README `files` block back and compare them to the bank. - A file in one and not the other: the fill did not land, report it and stop. +This manual workflow does not establish the hook's protection against path substitution or multiple hard links. If a hardlink, a changing path, or any other condition requires a guarantee the manual checks cannot demonstrate, stop the sync and explain that the manual workflow cannot provide hook-level path safety. The automatic hook keeps its existing best-effort behavior; this explicit sync must refuse detected problems before writing. + ## Test | Case | Pass | | --- | --- | | Empty bank | no context file created, the run stops | -| Script | it exits `0` | +| Unsafe or unverifiable path | no file changes; explain the hook-level path-safety limit when required | | Picked tool | its block lists every root `.md` except `README.md`, nothing else | | Bank grew | the block gains that file and keeps the rest | | Unpicked tool | its context file is unchanged | +| Invalid last destination or opted-in README | preflight refuses before any context creation or modification | +| Shared destination | picked tools resolving to `AGENTS.md` produce one block and one write | +| Identical rerun | every context file remains byte-identical | diff --git a/plugins/aidd-context/skills/02-project-memory/references/tools.md b/plugins/aidd-context/skills/02-project-memory/references/tools.md index 4b90d76d1..ae216a2be 100644 --- a/plugins/aidd-context/skills/02-project-memory/references/tools.md +++ b/plugins/aidd-context/skills/02-project-memory/references/tools.md @@ -8,6 +8,7 @@ The AI tools a project can use. | codex | `.codex/` | `AGENTS.md` | | cursor | `.cursor/` or `.cursorrules` | `AGENTS.md` | | opencode | `.opencode/` | `AGENTS.md` | +| kilo | `.kilo/` or `kilo.json` or `kilo.jsonc` or `.kilo/kilo.json` or `.kilo/kilo.jsonc` or `.kilocode/` | `AGENTS.md` | | copilot | `.github/copilot-instructions.md` or `.github/{instructions,agents,skills,prompts}/` | `.github/copilot-instructions.md` | - A shared `AGENTS.md` is a wiring target, never a detection signal. @@ -15,3 +16,15 @@ The AI tools a project can use. - A context file carries the block under a `## Memory Management` section, shaped like `assets/templates/AGENTS.md`. - An existing context file keeps everything else: add only what is missing. - Touch no context file a picked tool does not resolve to. + +Kilo: `.kilocode/` is a legacy detection signal only; selected Kilo memory goes to the shared root `AGENTS.md`. Neither `opencode.json` nor `opencode.jsonc` identifies Kilo; OpenCode detection remains the row above. + +Kilo sources: [project configuration](https://kilo.ai/docs/getting-started/settings), [legacy directory fallback](https://github.com/Kilo-Org/kilocode/blob/main/packages/opencode/src/kilocode/skills/kilo-config.md), and [root `AGENTS.md`](https://kilo.ai/docs/customize/agents-md). + +## Explicit sync safety + +Before Upsert and Fill, inspect all selected destinations and the memory README if it carries standalone `files` markers. An absent or blockless regular file is valid for Upsert. Refuse symlink destinations or ancestors, non-regular or non-writable destinations (and non-writable creation parents), and ambiguous blocks before any write: duplicate, nested, reversed, unmatched, or mixed legacy/current marker pairs. Count only standalone marker lines outside fenced code; quoted examples are user content. Preserve bytes outside the owned block, including line endings, and leave unselected files unchanged. + +The automatic hook remains best-effort. Explicit preflight prevents writes on detected configuration errors; it does not promise a multi-file transaction against a crash or a filesystem change after validation. + +The hook's `--check` is a destination preflight: it checks the selected context destinations and an opted-in README for safe file types, access, and unambiguous markers. It accepts absent or blockless context files, then exits before scanning the memory bank. It does not verify that references in the context files match the bank. diff --git a/plugins/aidd-context/skills/04-skill-generate/actions/01-scope.md b/plugins/aidd-context/skills/04-skill-generate/actions/01-scope.md index 2ed28f681..115b1cb60 100644 --- a/plugins/aidd-context/skills/04-skill-generate/actions/01-scope.md +++ b/plugins/aidd-context/skills/04-skill-generate/actions/01-scope.md @@ -14,8 +14,9 @@ The confirmed frame, written nowhere, per [scope-frame.md](../references/scope-f 1. **Detect.** Detect the installed tools per [tool-detect.md](../references/tool-detect.md). 2. **Fill.** For each field in [scope-frame.md](../references/scope-frame.md), propose a value or ask one question. -3. **Check.** Check the name per [naming.md](../references/naming.md) and surface any overlap. -4. **Confirm.** Hand the confirmed frame to plan. +3. **Place.** Resolve placement and existing same-name copies per [tool-write.md](../references/tool-write.md), without writing. +4. **Check.** Check the name per [naming.md](../references/naming.md) and surface any overlap. +5. **Confirm.** Hand the confirmed frame to plan. ## Test @@ -26,3 +27,5 @@ The confirmed frame, written nowhere, per [scope-frame.md](../references/scope-f | No name overlaps | the run states that it found none | | A frame field needs the user | one question is asked, and only that one | | The frame is handed to plan | its target was confirmed by the user first | +| Kilo portable placement is not agreed | no portable target is handed to plan | +| Same-name copies already exist | resolution is requested before any write | diff --git a/plugins/aidd-context/skills/04-skill-generate/actions/03-write.md b/plugins/aidd-context/skills/04-skill-generate/actions/03-write.md index 595826e48..a605d3410 100644 --- a/plugins/aidd-context/skills/04-skill-generate/actions/03-write.md +++ b/plugins/aidd-context/skills/04-skill-generate/actions/03-write.md @@ -13,14 +13,15 @@ The skill tree at the target, and the list of files written. ## Process -1. **Tree.** Create the shape in [skill-tree.md](../references/skill-tree.md). -2. **Router.** Fill [skill-template.md](../assets/skill-template.md) against [skill-authoring.md](../references/skill-authoring.md), strip the scaffold. +1. **Preflight.** Read [tool-write.md](../references/tool-write.md) and prevalidate the entire planned publication, including modify destinations, before creating anything. +2. **Tree.** Create the shape in [skill-tree.md](../references/skill-tree.md). +3. **Router.** Fill [skill-template.md](../assets/skill-template.md) against [skill-authoring.md](../references/skill-authoring.md), strip the scaffold. - Modify: revise the existing SKILL.md in place, keeping the user's edits. -3. **Actions.** Fill [action-template.md](../assets/action-template.md) per row against [skill-authoring.md](../references/skill-authoring.md), its test copied from the plan. +4. **Actions.** Fill [action-template.md](../assets/action-template.md) per row against [skill-authoring.md](../references/skill-authoring.md), its test copied from the plan. - Modify: revise a changed action in place. Leave an untouched one alone. -4. **Hint.** Set `argument-hint` per [skill-authoring.md](../references/skill-authoring.md). -5. **Place.** Write once per confirmed target, using [tool-write.md](../references/tool-write.md). -6. **Check.** Confirm every written path is relative, inside the workspace, outside the plugin install directory, and under the chosen target. Else stop and report it. +5. **Render.** Apply the selected host frontmatter from `tool-write.md` after filling the canonical template. +6. **Place.** Publish each distinct confirmed target per `tool-write.md`, skipping byte-identical files. +7. **Check.** Confirm every written path is relative, inside the workspace, outside the plugin install directory, and under the chosen target. Else stop and report it. ## Test @@ -32,3 +33,6 @@ The skill tree at the target, and the list of files written. | The tree is written | its shape matches [skill-tree.md](../references/skill-tree.md) | | Any written file is read back | no `<` placeholder and no template instruction line survives | | A path resolves outside the chosen target | the run stops and reports that path | +| A later target or reference fails preflight | every tree remains unchanged, with no new directory | +| The same request is repeated | bytes and mtime remain unchanged | +| Modify leaves an action or asset outside the plan | its user bytes remain unchanged | diff --git a/plugins/aidd-context/skills/04-skill-generate/actions/04-validate.md b/plugins/aidd-context/skills/04-skill-generate/actions/04-validate.md index fca3dacbb..7ecf761ff 100644 --- a/plugins/aidd-context/skills/04-skill-generate/actions/04-validate.md +++ b/plugins/aidd-context/skills/04-skill-generate/actions/04-validate.md @@ -13,8 +13,9 @@ A report, one row per file: what was checked and any fix applied. ## Process 1. **Review.** Review each file against [review-protocol.md](../references/review-protocol.md). -2. **Fix.** Apply the confirmed fixes on disk, then re-review the changed files. -3. **Report.** Deliver the findings, even when clean. +2. **Target.** Validate placement, host frontmatter, preserved resources and rerun stability per [tool-write.md](../references/tool-write.md). +3. **Fix.** Apply the confirmed fixes on disk, then re-review the changed files. +4. **Report.** Deliver the findings, even when clean. ## Test @@ -23,3 +24,5 @@ A report, one row per file: what was checked and any fix applied. | The report is delivered | every written file has a row, with its findings or none | | The review finds a breach | it names the broken rule and a `file:line` | | A fix is confirmed | the file differs on disk and is reviewed again | +| Kilo output is validated | one agreed placement, name matches folder, only supported fields | +| A selected host is not Kilo | its existing field contract still holds | diff --git a/plugins/aidd-context/skills/04-skill-generate/references/scope-frame.md b/plugins/aidd-context/skills/04-skill-generate/references/scope-frame.md index 31635008c..744356422 100644 --- a/plugins/aidd-context/skills/04-skill-generate/references/scope-frame.md +++ b/plugins/aidd-context/skills/04-skill-generate/references/scope-frame.md @@ -7,6 +7,7 @@ The fields a scoped skill must define. - name: kebab-case. - flow: visible user journey. - mode: optional, `auto` or `interactive`. +- placement: for Kilo, confirmed native or explicitly agreed portable; omit for other tools. - target: detected tool, plugin source, or dedicated tool. Modes: @@ -22,3 +23,4 @@ Output shape: - flow: - mode: omit when absent. - target: +- placement: omit when not applicable. diff --git a/plugins/aidd-context/skills/04-skill-generate/references/tool-detect.md b/plugins/aidd-context/skills/04-skill-generate/references/tool-detect.md index 286dbc7a3..8168474bf 100644 --- a/plugins/aidd-context/skills/04-skill-generate/references/tool-detect.md +++ b/plugins/aidd-context/skills/04-skill-generate/references/tool-detect.md @@ -9,5 +9,10 @@ Which AI tools a project has installed. | OpenCode | `.opencode/` | | GitHub Copilot | `.github/copilot-instructions.md` | | Codex CLI | `.codex/` | +| Kilo | `.kilo/`, `kilo.json`, `kilo.jsonc`, `.kilo/kilo.json`, `.kilo/kilo.jsonc`, `.kilocode/` | A bare `AGENTS.md` means Cursor, OpenCode, or Codex. Several signals can coexist. + +`AGENTS.md` and `opencode.json[c]` alone do not detect Kilo. `.kilocode/` is historical detection only; new Kilo artifacts use `.kilo/`, never `.kilocode/`. + +Kilo sources: [project configuration](https://kilo.ai/docs/getting-started/settings), [legacy directory fallback](https://github.com/Kilo-Org/kilocode/blob/main/packages/opencode/src/kilocode/skills/kilo-config.md), and [native skill paths](https://kilo.ai/docs/customize/skills). diff --git a/plugins/aidd-context/skills/04-skill-generate/references/tool-write.md b/plugins/aidd-context/skills/04-skill-generate/references/tool-write.md index 2863f4ccd..7bab4c069 100644 --- a/plugins/aidd-context/skills/04-skill-generate/references/tool-write.md +++ b/plugins/aidd-context/skills/04-skill-generate/references/tool-write.md @@ -11,8 +11,27 @@ Where a skill tree is written, per tool. | OpenCode | `.opencode/skills//` | | GitHub Copilot | `.github/skills//` | | Codex CLI | `.agents/skills//` | +| Kilo native | `.kilo/skills//` | +| Kilo portable | `.agents/skills//` | | Plugin source | `plugins//skills//` | +## Placement and preservation + +### Shared rules + +- Resolve every selected host's destination before writing, then deduplicate identical physical destinations so each is rendered and written once. +- When hosts share a destination, render only fields supported by all of them. If a user-requested field is unsupported by any selected host, or selected hosts require incompatible destinations, ask for an explicit target or field resolution; never let target order decide or silently discard a requested field. +- Existing same-name skills in any detected skill root require user resolution before writing. Do not delete, synchronize, or overwrite user copies implicitly. +- Before creating directories, inspect every target, planned file, existing ancestor, and local reference. Reject unsafe names, paths escaping the workspace or selected target, symlinks, non-regular files, non-writable destinations, and unresolved name collisions. A missing destination requires a writable existing ancestor. +- For modify, read the current tree first and preserve user edits and resources, including actions and assets absent from the change plan. Ask before replacing a conflicting file; refusal leaves all targets unchanged. Creating over an existing skill also requires explicit resolution. +- Resolve every local Markdown link within the final skill tree before publication. Links may point to planned files or retained regular files, never outside the skill. Preflight all selected targets before the first write; any known invalid target refuses the whole run. +- For identical requests with identical choices, reuse current content. Compare bytes before writing and retain mtime when unchanged. This interpreted workflow does not promise a multi-file crash or concurrent-write transaction. + +### Kilo placement + +- Kilo defaults to the native proposal at `.kilo/skills//`. Require explicit user agreement before using the portable `.agents/skills//` target, including when Codex is also selected. Confirm one placement and never write both Kilo trees for the same skill. +- If Kilo native and another selected host would both publish this skill to `.agents/skills/`, resolve placement before writing. Offer one explicitly agreed shared portable copy or a revised target selection; never create both trees to satisfy unresolved choices. + ## Frontmatter Emit `description` always, `name` only where listed, drop the rest. @@ -23,3 +42,12 @@ Emit `description` always, `name` only where listed, drop the rest. | Cursor, GitHub Copilot | `name`, `description`, opt `allowed-tools` | | OpenCode | `description`, opt `permission` map | | Codex CLI | `name`, `description` (strips the rest) | +| Kilo native or portable | `name`, `description`, opt `license`, `compatibility`, `metadata` | + +**Canonical-to-host rendering:** Author the canonical skill according to AIDD conventions, including R4. Generated host artifacts must follow their target host's field contract. Omit unsupported canonical-only fields; explicitly resolve unsupported user-requested fields before writing. + +For Kilo, `name` must equal the parent directory name, use lowercase letters, digits and single hyphens, and contain at most 64 characters. Require a nonempty description of at most 1024 characters. Emit optional fields only when requested; omit `argument-hint` and Claude-specific fields from Kilo outputs. + +Native and portable locations are loader-compatible; compatibility does not authorize copying. + +Kilo source: [Skills](https://kilo.ai/docs/customize/skills) documents native `.kilo/skills/` and portable `.agents/skills/` discovery. diff --git a/plugins/aidd-context/skills/05-rule-generate/actions/01-capture-rule.md b/plugins/aidd-context/skills/05-rule-generate/actions/01-capture-rule.md index ce6f18d17..21fb313e3 100644 --- a/plugins/aidd-context/skills/05-rule-generate/actions/01-capture-rule.md +++ b/plugins/aidd-context/skills/05-rule-generate/actions/01-capture-rule.md @@ -14,6 +14,7 @@ Approved candidates with planned paths, or confirmed sources, operation and dest 1. Confirm the operation and [destination](../references/rule-publication.md). - Hosts: reuse the existing rule's hosts, else ask. Missing hosts: detect, propose installation, await selection before drafting. + - For Kilo, inspect all four project configurations before writing. If several are candidates and no unique configuration owns the exact rule path, present the candidates and require an explicit choice; do not publish without it. 2. Frame the topic. - Manual: ask for it. - Auto: scan, propose topics, await selection. @@ -24,3 +25,5 @@ Approved candidates with planned paths, or confirmed sources, operation and dest 6. Present each candidate in full (body, description, scope, hosts), the planned paths and every finding (new rules too), then end the turn. - Needs review: ask for evidence, reassess before offering source changes. - Resolved: replan, present the revised candidate, end the turn again. + +For an ambiguous Kilo configuration, pass only the user's confirmed `--kilo-config` path to the writer. diff --git a/plugins/aidd-context/skills/05-rule-generate/actions/02-write-rule.md b/plugins/aidd-context/skills/05-rule-generate/actions/02-write-rule.md index a8e705925..9c58cb40c 100644 --- a/plugins/aidd-context/skills/05-rule-generate/actions/02-write-rule.md +++ b/plugins/aidd-context/skills/05-rule-generate/actions/02-write-rule.md @@ -16,9 +16,9 @@ The written files, verified, or the refusal. 1. Match: for create/update, require that approval and compare body, fields, paths, resolution and evidence with the approved candidate. Without approval or on any difference, return to capture-rule, present the revision, end the turn, write nothing. 2. Write the approved result. - - Host: run the writer by the absolute path resolved in capture-rule, with the approved request. + - Host: run the writer by the absolute path resolved in capture-rule, with the approved request. For Kilo, pass the confirmed `--kilo-config` path when one was selected. - Plugin: write or delete the approved source only. -3. Verify: for a host, run the same absolute writer with `--check --publish`. Require no drift, deletions included. +3. Verify: for a host, run the same absolute writer with `--check --publish` and the same confirmed targets/configuration. Require no drift, deletions included. 4. Report the written files. On refusal or drift, stop and quote it verbatim. ## Rules diff --git a/plugins/aidd-context/skills/05-rule-generate/references/rule-publication.md b/plugins/aidd-context/skills/05-rule-generate/references/rule-publication.md index af89c4aaf..d814594f0 100644 --- a/plugins/aidd-context/skills/05-rule-generate/references/rule-publication.md +++ b/plugins/aidd-context/skills/05-rule-generate/references/rule-publication.md @@ -9,6 +9,7 @@ | Copilot / `copilot` | `.github/instructions/-.instructions.md` | comma-joined `applyTo`; all-files `**` | | Codex / `codex` | root `AGENTS.md` | shared signed `aidd_rules`, textual scope | | OpenCode / `opencode` | root `AGENTS.md` | same contribution | +| Kilo / `kilo` | `.kilo/rules//.md` plus an exact `instructions` entry in the selected `kilo.json[c]` | Markdown body; describe scope in the body when needed | Copilot `` drops the slug's numeric prefix: category `01-standards`, slug `1-local-names` → `01-local-names.instructions.md`. @@ -27,5 +28,14 @@ node "$installed_rule_script" --project "$project_root" --tools "$confirmed_ids" - `--delete category/slug` replaces `--input`: one source, all recorded publications; reports `files`. - `--plan` on any of the three: runs every refusal, writes nothing, reports `{"dryRun":true,"files":[...]}` with the exact paths a real run touches. - `--check --publish`: writes nothing, reports `{"drift":[...]}`, exits non-zero while a publication differs from its canonical source. +- Kilo: inspect all four project configurations before planning. When selection is ambiguous, pass only the user's confirmed `--kilo-config` path alongside the `kilo` target. - Plugin: `plugins//rules//.md`; YAML `paths` for scope, or no frontmatter. No writer, project metadata or host fan-out. - Physical deduplication does not ensure unique host context; local 32 KiB passes do not ensure the total instruction budget. + +## Kilo configuration + +Detect Kilo from `.kilo/` (including its JSON or JSONC config), root `kilo.json` or `kilo.jsonc`, or legacy `.kilocode/`. The legacy directory is a detection signal only; write new rules under `.kilo/`. Root `AGENTS.md` and OpenCode configuration alone do not identify Kilo. + +Inspect `.kilo/kilo.json`, `.kilo/kilo.jsonc`, `kilo.json` and `kilo.jsonc`. `.kilo/` takes precedence over root; do not assume an ordering between JSON and JSONC at the same level. With no config, the writer creates `.kilo/kilo.jsonc`. With one config, it uses that file. With several, it reuses the unique owner of the exact rule path; otherwise present the candidates and require the user to select one with `--kilo-config`. Invalid or unsafe candidates refuse publication before writing. + +The writer preserves JSONC comments, entry order, duplicates and unrelated values. Removing a rule referenced by multiple Kilo configs is refused. It rechecks config choices and destinations before publishing, stages the outputs, and restores completed replacements after covered errors. Each replacement is atomic; abrupt termination or power loss can still interrupt a multi-file publication. diff --git a/plugins/aidd-context/skills/05-rule-generate/scripts/kilo-config.cjs b/plugins/aidd-context/skills/05-rule-generate/scripts/kilo-config.cjs new file mode 100644 index 000000000..c9530b072 --- /dev/null +++ b/plugins/aidd-context/skills/05-rule-generate/scripts/kilo-config.cjs @@ -0,0 +1,251 @@ +"use strict"; + +// Pure JSONC edit and read-only project selection. Publication belongs to the rule writer. +const fs = require("node:fs"); +const path = require("node:path"); +const { createHash } = require("node:crypto"); +const { TextDecoder } = require("node:util"); + +const CONFIG_PATHS = [".kilo/kilo.json", ".kilo/kilo.jsonc", "kilo.json", "kilo.jsonc"]; +const DEFAULT_CONFIG = ".kilo/kilo.jsonc"; +const fail = (message) => { throw new Error(message); }; + +function checkInstructionPath(value) { + if (typeof value !== "string" || !/^\.kilo\/rules\/[0-9]{2}-[a-z0-9-]+\/[0-9]-[a-z0-9]+(?:[-@.][a-z0-9]+)*\.md$/.test(value)) { + fail("Expected an exact relative .kilo/rules//.md path."); + } +} + +function parseJsonc(text) { + if (typeof text !== "string" || text.includes("\0")) fail("Invalid JSONC text."); + let pos = 0; + function skip() { + while (pos < text.length) { + if (/[ \t\r\n]/.test(text[pos])) { pos++; continue; } + if (text.startsWith("//", pos)) { + pos += 2; + while (pos < text.length && text[pos] !== "\n") pos++; + continue; + } + if (text.startsWith("/*", pos)) { + const end = text.indexOf("*/", pos + 2); + if (end < 0) fail("Unclosed JSONC comment."); + pos = end + 2; + continue; + } + break; + } + } + function string() { + const start = pos; + pos++; + while (pos < text.length) { + if (text[pos] === "\\") { pos += 2; continue; } + if (text[pos++] === '"') { + const raw = text.slice(start, pos); + try { return { type: "string", value: JSON.parse(raw), start, end: pos }; } + catch { fail("Invalid JSONC string."); } + } + } + fail("Unclosed JSONC string."); + } + function value() { + skip(); + const start = pos; + if (text[pos] === '"') return string(); + if (text[pos] === "[") { + pos++; + const items = []; + const commas = []; + let trailingComma = false; + let lastCommaEnd = null; + skip(); + while (text[pos] !== "]") { + if (pos >= text.length) fail("Unclosed JSONC array."); + items.push(value()); + skip(); + if (text[pos] === ",") { + commas.push(pos); + pos++; + trailingComma = true; + lastCommaEnd = pos; + skip(); + } else if (text[pos] === "]") trailingComma = false; + else fail("Expected comma in JSONC array."); + } + const close = pos++; + return { type: "array", start, end: pos, close, items, commas, trailingComma, lastCommaEnd }; + } + if (text[pos] === "{") { + pos++; + const properties = []; + let trailingComma = false; + let lastCommaEnd = null; + skip(); + while (text[pos] !== "}") { + if (pos >= text.length) fail("Unclosed JSONC object."); + if (text[pos] !== '"') fail("Expected JSONC property name."); + const key = string(); + skip(); + if (text[pos++] !== ":") fail("Expected JSONC colon."); + const entry = value(); + properties.push({ key: key.value, value: entry }); + skip(); + if (text[pos] === ",") { + pos++; + trailingComma = true; + lastCommaEnd = pos; + skip(); + } else if (text[pos] === "}") trailingComma = false; + else fail("Expected comma in JSONC object."); + } + const close = pos++; + return { type: "object", start, end: pos, close, properties, trailingComma, lastCommaEnd }; + } + const match = /^(?:true|false|null|-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?)/.exec(text.slice(pos)); + if (!match) fail("Invalid JSONC value."); + pos += match[0].length; + if (pos < text.length && !/[ \t\r\n,}\]/]/.test(text[pos]) && !text.startsWith("//", pos) && !text.startsWith("/*", pos)) { + fail("Invalid JSONC value boundary."); + } + return { type: "scalar", start, end: pos }; + } + const root = value(); + skip(); + if (pos !== text.length || root.type !== "object") fail("Expected one JSONC object."); + const fields = root.properties.filter(({ key }) => key === "instructions"); + if (fields.length > 1) fail("Duplicate instructions keys are ambiguous."); + const instructions = fields[0]?.value; + if (instructions && (instructions.type !== "array" || instructions.items.some((item) => item.type !== "string"))) { + fail("instructions must be an array of strings."); + } + return { root, instructions }; +} + +function prepareInstructionEdit(content, instructionPath) { + checkInstructionPath(instructionPath); + if (typeof content !== "string" || Buffer.from(content).toString("utf8") !== content) fail("Invalid UTF-8 config text."); + const { root, instructions } = parseJsonc(content); + if (instructions?.items.some((item) => item.value === instructionPath)) return { content, changed: false }; + const entry = JSON.stringify(instructionPath); + let offset; + let insertion; + if (instructions) { + if (!instructions.items.length) { + offset = instructions.close; + insertion = entry; + } else if (instructions.trailingComma) { + offset = instructions.lastCommaEnd; + insertion = entry + ","; + } else { + offset = instructions.items.at(-1).end; + insertion = "," + entry; + } + } else { + const property = `"instructions": [${entry}]`; + offset = root.trailingComma ? root.lastCommaEnd : root.close; + insertion = root.properties.length && !root.trailingComma ? "," + property : property; + } + const updated = content.slice(0, offset) + insertion + content.slice(offset); + parseJsonc(updated); + return { content: updated, changed: true }; +} + +function prepareInstructionRemoval(content, instructionPath) { + checkInstructionPath(instructionPath); + const { instructions } = parseJsonc(content); + if (!instructions) return { content, changed: false }; + const indexes = instructions.items.flatMap((item, index) => item.value === instructionPath ? [index] : []); + if (!indexes.length) return { content, changed: false }; + if (indexes.length > 1) fail("Duplicate exact instruction entries need manual resolution before deletion."); + // Remove only a token and its adjacent comma. Comments in that span have no + // unambiguous owner, so require a manual edit instead of discarding them. + let updated = content; + for (const index of indexes.reverse()) { + const item = instructions.items[index]; + const following = instructions.commas[index]; + const preceding = instructions.commas[index - 1]; + const start = following !== undefined ? item.start : preceding !== undefined ? preceding : item.start; + const end = following !== undefined ? following + 1 : item.end; + const removed = content.slice(start, end); + if (/\/\*|\/\//.test(removed)) fail("Cannot safely remove a commented instruction entry."); + updated = updated.slice(0, start) + updated.slice(end); + } + parseJsonc(updated); + return { content: updated, changed: true }; +} + +function inspectProject(projectRoot, instructionPath, chosenPath) { + checkInstructionPath(instructionPath); + const root = path.resolve(projectRoot); + if (!fs.statSync(root).isDirectory() || fs.realpathSync(root) !== root) fail("Project must be a real directory."); + const rootIdentity = fs.statSync(root); + const decoder = new TextDecoder("utf-8", { fatal: true }); + const candidates = []; + for (const relative of CONFIG_PATHS) { + const parts = relative.split("/"); + const current = path.join(root, ...parts); + const before = inspectPath(root, parts, relative); + if (before === null) continue; + let fd; + let content; + try { + const flags = fs.constants.O_RDONLY | (process.platform === "win32" ? 0 : (fs.constants.O_NOFOLLOW || 0) | (fs.constants.O_NONBLOCK || 0)); + fd = fs.openSync(current, flags); + const opened = fs.fstatSync(fd); + if (!opened.isFile() || !sameIdentity(opened, before.at(-1))) fail(`Config changed while opening: ${relative}`); + const after = inspectPath(root, parts, relative); + if (!after || fs.realpathSync(root) !== root || !sameIdentity(rootIdentity, fs.statSync(root)) || !samePath(before, after)) fail(`Config path changed while opening: ${relative}`); + content = decoder.decode(fs.readFileSync(fd)); + } catch (error) { + if (error.message?.startsWith("Config ")) throw error; + fail(`Cannot safely read config: ${relative}`); + } finally { + if (fd !== undefined) fs.closeSync(fd); + } + const { instructions } = parseJsonc(content); + candidates.push({ + path: relative, + ownsInstruction: !!instructions?.items.some((item) => item.value === instructionPath), + snapshot: { dev: before.at(-1).dev, ino: before.at(-1).ino, mode: before.at(-1).mode & 0o7777, hash: createHash("sha256").update(content).digest("hex") }, + }); + } + if (chosenPath !== undefined && chosenPath !== null && !candidates.some((item) => item.path === chosenPath)) { + fail(`Chosen Kilo config is absent: ${chosenPath}`); + } + if (!candidates.length) return { candidates, selectedPath: DEFAULT_CONFIG, needsChoice: false }; + if (candidates.length === 1) return { candidates, selectedPath: candidates[0].path, needsChoice: false }; + const owners = candidates.filter((item) => item.ownsInstruction); + if (owners.length === 1) return { candidates, selectedPath: owners[0].path, needsChoice: false }; + return { candidates, selectedPath: chosenPath || null, needsChoice: !chosenPath, multipleOwners: owners.length > 1 }; +} + +function sameIdentity(left, right) { + return left.dev === right.dev && left.ino === right.ino; +} + +function samePath(left, right) { + return left.length === right.length && left.every((stat, index) => sameIdentity(stat, right[index])); +} + +function inspectPath(root, parts, relative) { + const stats = [fs.statSync(root)]; + let current = root; + for (let index = 0; index < parts.length; index++) { + current = path.join(current, parts[index]); + let stat; + try { stat = fs.lstatSync(current); } + catch (error) { + if (error.code === "ENOENT") return null; + throw error; + } + const final = index === parts.length - 1; + if (stat.isSymbolicLink() || (final ? !stat.isFile() : !stat.isDirectory())) { + fail(`Symlink or invalid config path: ${relative}`); + } + stats.push(stat); + } + return stats; +} + +module.exports = { inspectProject, prepareInstructionEdit, prepareInstructionRemoval }; diff --git a/plugins/aidd-context/skills/05-rule-generate/scripts/write-rule.cjs b/plugins/aidd-context/skills/05-rule-generate/scripts/write-rule.cjs index ba341479d..43789db5a 100644 --- a/plugins/aidd-context/skills/05-rule-generate/scripts/write-rule.cjs +++ b/plugins/aidd-context/skills/05-rule-generate/scripts/write-rule.cjs @@ -5,8 +5,9 @@ const fs = require("node:fs"); const path = require("node:path"); const { createHash, randomUUID } = require("node:crypto"); +const { inspectProject, prepareInstructionEdit, prepareInstructionRemoval } = require("./kilo-config.cjs"); -const TOOLS = ["claude", "cursor", "copilot", "codex", "opencode"]; +const TOOLS = ["claude", "cursor", "copilot", "codex", "opencode", "kilo"]; const CATEGORIES = ["architecture", "standards", "programming-languages", "frameworks-and-libraries", "tooling", "testing", "design-patterns", "quality", "domain", "other"]; const SHARED = ["codex", "opencode"]; const SOURCE = "aidd_docs/rules"; @@ -25,7 +26,7 @@ function options(args) { const result = {}; for (let i = 0; i < args.length; i++) { const key = args[i]; - if (!["--project", "--tools", "--input", "--delete", "--publish", "--plan", "--check"].includes(key) || key in result) fail(`Unknown or repeated option: ${key}`); + if (!["--project", "--tools", "--input", "--delete", "--publish", "--plan", "--check", "--kilo-config"].includes(key) || key in result) fail(`Unknown or repeated option: ${key}`); result[key] = ["--publish", "--plan", "--check"].includes(key) ? true : args[++i]; if (!result[key] || (typeof result[key] === "string" && result[key].startsWith("--"))) fail(`Missing value: ${key}`); } @@ -37,7 +38,7 @@ function options(args) { function tools(value) { const selected = typeof value === "string" ? value.split(",") : value; - if (!Array.isArray(selected) || !selected.length || new Set(selected).size !== selected.length || selected.some((tool) => !TOOLS.includes(tool))) fail("Targets must be distinct confirmed tools: claude,cursor,copilot,codex,opencode."); + if (!Array.isArray(selected) || !selected.length || new Set(selected).size !== selected.length || selected.some((tool) => !TOOLS.includes(tool))) fail(`Targets must be distinct confirmed tools: ${TOOLS.join(",")}.`); return [...selected].sort(); } @@ -108,6 +109,7 @@ function nativePath(rule, tool) { const id = identity(rule.category, rule.slug); if (tool === "claude") return `.claude/rules/${id}.md`; if (tool === "cursor") return `.cursor/rules/${id}.mdc`; + if (tool === "kilo") return `.kilo/rules/${id}.md`; return `.github/instructions/${rule.category.slice(0, 2)}-${rule.slug.slice(2)}.instructions.md`; } @@ -117,7 +119,9 @@ function renderNative(rule, tool, signed = true) { if (tool === "claude") fields = rule.paths.length ? `paths:\n${rule.paths.map((glob) => ` - ${quote(glob)}\n`).join("")}` : ""; if (tool === "cursor") fields = `description: ${quote(rule.description)}\n${rule.paths.length ? `globs: ${quote(rule.paths.join(","))}\n` : ""}alwaysApply: ${!rule.paths.length}\n`; if (tool === "copilot") fields = `applyTo: ${quote(rule.paths.length ? rule.paths.join(",") : "**")}\n`; - const text = (fields ? `---\n${fields}---\n` : "") + rule.body; + if (tool === "kilo") fields = ""; + const kiloScope = tool === "kilo" && rule.paths.length ? `Applies to: ${rule.paths.map(codeSpan).join(", ")}.\n\n` : ""; + const text = (fields ? `---\n${fields}---\n` : "") + kiloScope + rule.body; safeText(text); return signed ? `${text}\n\n` : text; } @@ -240,6 +244,7 @@ function main() { const affected = new Set(selected); if (changed && previous.has(changed)) for (const tool of previous.get(changed).targets) affected.add(tool); const writes = new Map(); + const configChoices = []; if (changed) writes.set(changed, prospective.has(changed) ? canonical(prospective.get(changed)) : null); for (const tool of affected) { if (SHARED.includes(tool)) continue; @@ -253,6 +258,36 @@ function main() { writes.set(relative, next?.targets.includes(tool) ? renderNative(next, tool) : null); } } + if (affected.has("kilo")) { + const configWrites = new Map(); + for (const name of new Set([...previous.keys(), ...prospective.keys()])) { + const old = previous.get(name); + const next = prospective.get(name); + if (!old?.targets.includes("kilo") && !next?.targets.includes("kilo")) continue; + const instruction = nativePath(next || old, "kilo"); + const choice = inspectProject(root, instruction, opts["--kilo-config"]); + if (choice.needsChoice) fail(`Choose a Kilo config for ${instruction}: ${choice.candidates.map((item) => item.path).join(", ")}`); + if (!next?.targets.includes("kilo") && choice.multipleOwners) fail(`Multiple Kilo configs own ${instruction}; resolve references before deletion.`); + configChoices.push({ instruction, choice }); + const config = choice.selectedPath; + const original = configWrites.has(config) ? configWrites.get(config) : read(config); + if (!configWrites.has(config)) { + const snapshot = choice.candidates.find((item) => item.path === config)?.snapshot; + if (snapshot) { + let stat; + try { stat = fs.lstatSync(target(config)); } + catch (error) { if (error.code !== "ENOENT") throw error; } + if (original === null || !stat || stat.dev !== snapshot.dev || stat.ino !== snapshot.ino || (stat.mode & 0o7777) !== snapshot.mode || hash(original) !== snapshot.hash) fail(`Kilo config changed after inspection: ${config}`); + } else if (original !== null) fail(`Kilo config appeared after inspection: ${config}`); + } + if (next?.targets.includes("kilo")) { + configWrites.set(config, prepareInstructionEdit(original ?? "{}\n", instruction).content); + } else if (original !== null) { + configWrites.set(config, prepareInstructionRemoval(original, instruction).content); + } + } + for (const [relative, content] of configWrites) writes.set(relative, content); + } else if (opts["--kilo-config"]) fail("--kilo-config requires a Kilo target."); if ([...affected].some((tool) => SHARED.includes(tool))) { const existing = read("AGENTS.md"); const updated = renderShared(existing || "", nextRules); @@ -262,22 +297,90 @@ function main() { } if (existing !== null || updated) writes.set("AGENTS.md", updated); } - // Preflight every destination and ownership above; no validation can first happen after mutation. + // Stage every result before publication. Recheck each snapshot at mutation + // time and restore completed replacements if a covered filesystem call fails. + const pending = []; + const createdDirs = []; + const staged = []; + const applied = []; + function stage(item, text, temporary) { + target(item.relative); + const mode = item.stat ? item.stat.mode & 0o7777 : undefined; + const fd = fs.openSync(temporary, "wx", mode === undefined ? 0o666 : mode); + staged.push(temporary); + try { + target(item.relative); + fs.writeFileSync(fd, text); + if (mode !== undefined && process.platform !== "win32") fs.fchmodSync(fd, mode); + } finally { fs.closeSync(fd); } + } + function checkConfigChoices() { + for (const { instruction, choice } of configChoices) { + const current = inspectProject(root, instruction, opts["--kilo-config"]); + if (current.selectedPath !== choice.selectedPath || JSON.stringify(current.candidates) !== JSON.stringify(choice.candidates)) { + fail(`Kilo config changed during publication: ${instruction}`); + } + } + } + // Preflight every destination before either read-only mode or publication. for (const relative of writes.keys()) target(relative); + checkConfigChoices(); if (opts["--plan"]) return void process.stdout.write(JSON.stringify({ dryRun: true, files: [...writes.keys()] }) + "\n"); if (opts["--check"]) { const drift = [...writes].filter(([relative, text]) => text !== read(relative)).map(([relative]) => relative); process.exitCode = drift.length ? 1 : 0; return void process.stdout.write(JSON.stringify({ drift }) + "\n"); } - for (const [relative, text] of writes) { - const file = target(relative); - if (text === read(relative)) continue; - if (text === null) { fs.rmSync(file, { force: true }); continue; } - fs.mkdirSync(path.dirname(file), { recursive: true }); - const temporary = `${file}.${randomUUID()}.tmp`; - try { fs.writeFileSync(temporary, text, { flag: "wx" }); fs.renameSync(temporary, file); } - finally { fs.rmSync(temporary, { force: true }); } + try { + for (const [relative, text] of writes) { + const file = target(relative); + let stat = null; + try { stat = fs.lstatSync(file); } catch (error) { if (error.code !== "ENOENT") throw error; } + if (stat && !stat.isFile()) fail(`Unsafe output type: ${relative}`); + const original = read(relative); + if (text === original) continue; + pending.push({ relative, file, text, original, stat }); + } + checkConfigChoices(); + for (const item of pending) { + target(item.relative); + let dir = path.dirname(item.file); + const missing = []; + while (dir !== root && !fs.existsSync(dir)) { missing.push(dir); dir = path.dirname(dir); } + for (const name of missing.reverse()) { fs.mkdirSync(name); createdDirs.push(name); } + if (item.text !== null) { + item.temporary = `${item.file}.${randomUUID()}.tmp`; + stage(item, item.text, item.temporary); + } + } + checkConfigChoices(); + for (const item of pending) { + const now = target(item.relative); + let stat = null; + try { stat = fs.lstatSync(now); } catch (error) { if (error.code !== "ENOENT") throw error; } + if (Boolean(stat) !== Boolean(item.stat) || (stat && (stat.dev !== item.stat.dev || stat.ino !== item.stat.ino || (stat.mode & 0o7777) !== (item.stat.mode & 0o7777))) || read(item.relative) !== item.original) fail(`Output changed during publication: ${item.relative}`); + if (item.text === null) fs.rmSync(item.file); + else fs.renameSync(item.temporary, item.file); + applied.push(item); + } + } catch (error) { + const rollbackErrors = []; + for (const item of applied.reverse()) { + try { + if (item.original === null) fs.rmSync(item.file, { force: true }); + else { + const restore = `${item.file}.${randomUUID()}.tmp`; + stage(item, item.original, restore); + target(item.relative); + fs.renameSync(restore, item.file); + } + } catch (rollback) { rollbackErrors.push(rollback.message); } + } + if (rollbackErrors.length) fail(`${error.message}; rollback failed: ${rollbackErrors.join("; ")}`); + throw error; + } finally { + for (const temporary of staged) fs.rmSync(temporary, { force: true }); + for (const dir of createdDirs.reverse()) { try { fs.rmdirSync(dir); } catch (error) { if (error.code !== "ENOTEMPTY" && error.code !== "ENOENT") throw error; } } } process.stdout.write(JSON.stringify({ files: [...writes.keys()] }) + "\n"); } diff --git a/plugins/aidd-context/skills/06-agent-generate/actions/01-capture-agent.md b/plugins/aidd-context/skills/06-agent-generate/actions/01-capture-agent.md index 45010c424..7a06578f2 100644 --- a/plugins/aidd-context/skills/06-agent-generate/actions/01-capture-agent.md +++ b/plugins/aidd-context/skills/06-agent-generate/actions/01-capture-agent.md @@ -20,6 +20,7 @@ In-context: the role and its system prompt, the chosen name, an optional user-sp 6. **Mode.** Ask where the agent goes: - **Host project**: detect the installed tools ([tool-paths.md](../references/tool-paths.md)), propose them, and confirm which to target. - **Plugin source**: confirm or create `plugins//agents/`. + - **Kilo**: when a Kilo signal is present, propose its canonical `.kilo/agents/.md` target. Capture only requested `model`, `temperature`, and `permission`; record that Kilo uses filename-derived identity and `mode: subagent`. ## Test diff --git a/plugins/aidd-context/skills/06-agent-generate/actions/02-write-agent.md b/plugins/aidd-context/skills/06-agent-generate/actions/02-write-agent.md index 92c656e02..d4c86e819 100644 --- a/plugins/aidd-context/skills/06-agent-generate/actions/02-write-agent.md +++ b/plugins/aidd-context/skills/06-agent-generate/actions/02-write-agent.md @@ -13,12 +13,13 @@ One agent file per confirmed tool, and the list of files written. ## Process 1. **Build.** Copy [agent-template.md](../assets/agent-template.md) into one canonical agent. Strip the scaffold (comments + `<...>`). -2. **Frontmatter.** Apply the per-tool frontmatter from [tool-paths.md](../references/tool-paths.md). Drop a field where the tool does not support it. Plugin source: keep the canonical frontmatter. +2. **Frontmatter.** Apply the per-tool frontmatter from [tool-paths.md](../references/tool-paths.md). Drop a field where the tool does not support it. For Kilo, use filename identity, `description`, `mode: subagent`, and only requested options. Plugin source: keep the canonical frontmatter. 3. **Convert.** Apply any per-tool structural conversion listed in [tool-paths.md](../references/tool-paths.md). Plugin source skips conversion. -4. **Render.** Per the write mode ([tool-paths.md](../references/tool-paths.md)): +4. **Preflight.** Render every selected output in memory, then resolve and validate every selected target and reference before creating anything, using [tool-paths.md](../references/tool-paths.md). Check the last target too. Refuse unsafe, symlinked, non-writable, colliding or target-specific requested fields unsupported by that tool; a refusal found here leaves all selected targets unchanged. +5. **Publish.** Before each write, compare that target's current state with its preflight state; stop on a detected change and report any outputs already published. Per the write mode ([tool-paths.md](../references/tool-paths.md)): - **Host**: for each confirmed tool, write to its path and extension. - **Plugin source**: write one canonical agent. No per-tool fan-out. -5. **Validate.** Run the write-target validation ([tool-paths.md](../references/tool-paths.md)). +6. **Validate.** Run the write-target validation ([tool-paths.md](../references/tool-paths.md)). Skip unchanged bytes; a repeated confirmed request is idempotent and preserves mtime. This interpreted workflow does not promise a multi-target rollback. ## Test diff --git a/plugins/aidd-context/skills/06-agent-generate/actions/03-validate.md b/plugins/aidd-context/skills/06-agent-generate/actions/03-validate.md index 9169f12c2..5057aa98a 100644 --- a/plugins/aidd-context/skills/06-agent-generate/actions/03-validate.md +++ b/plugins/aidd-context/skills/06-agent-generate/actions/03-validate.md @@ -13,7 +13,7 @@ A short pass or fail line per agent file. ## Process 1. **Exists.** Confirm each file is on disk at its expected path. -2. **Shape.** Confirm each file matches its target format ([tool-paths.md](../references/tool-paths.md)), a confirmed tool's, or the canonical form in plugin source, and carries the name, description, and body. +2. **Shape.** Confirm each file matches its target format ([tool-paths.md](../references/tool-paths.md)), a confirmed tool's, or the canonical form in plugin source, and carries the target's required identity, description, and body. For Kilo, identity comes from the filename and the frontmatter holds `mode: subagent`, not `name`. 3. **Concise.** Confirm the body stays focused on one role. ## Test diff --git a/plugins/aidd-context/skills/06-agent-generate/references/tool-paths.md b/plugins/aidd-context/skills/06-agent-generate/references/tool-paths.md index 90a1badde..f0c5cda26 100644 --- a/plugins/aidd-context/skills/06-agent-generate/references/tool-paths.md +++ b/plugins/aidd-context/skills/06-agent-generate/references/tool-paths.md @@ -11,8 +11,9 @@ The per-tool agent path and the gate every run executes before writing. Agent sl | OpenCode | `.opencode/agents/.md` | markdown + frontmatter | | GitHub Copilot | `.github/agents/.agent.md` | markdown + frontmatter | | Codex CLI | `.codex/agents/.toml` | TOML (converted) | +| Kilo | `.kilo/agents/.md` | markdown + frontmatter | -Agents are supported on all five tools. +Agents are supported on all six tools. ## Frontmatter per tool @@ -25,6 +26,9 @@ The canonical agent carries `name` and `description`. Emit those a row accepts, | OpenCode | `name`, `description`, optional `model`, `temperature`, `permission` | | GitHub Copilot | `name`, `description`, optional `model`, `tools` | | Codex CLI | `name`, `description` (drops `model`) | +| Kilo | `description`, `mode: subagent`, requested `model`, `temperature`, `permission` | + +For Kilo, the name is the filename. Do not emit the Claude `name` field. Its supported permission field is singular and maps requested tools or patterns to `allow`, `ask`, or `deny`. Emit `mode: subagent` and an optional field only when requested; never invent a default. ## Codex TOML conversion @@ -44,6 +48,11 @@ Codex agents are TOML, not markdown. Convert: | `.codex/` | Codex CLI | | `.github/copilot-instructions.md` | GitHub Copilot | | `AGENTS.md` | Cursor, OpenCode, or Codex (list all) | +| `.kilo/`, `kilo.json`, `kilo.jsonc`, `.kilo/kilo.json`, `.kilo/kilo.jsonc`, or `.kilocode/` | Kilo | + +The six Kilo signals are shared with the Kilo skill contract. `AGENTS.md` and `opencode.json[c]` alone are not Kilo signals. `.kilocode/` detects legacy Kilo but new output remains under `.kilo/`. + +Kilo sources: [project configuration](https://kilo.ai/docs/getting-started/settings), [legacy directory fallback](https://github.com/Kilo-Org/kilocode/blob/main/packages/opencode/src/kilocode/skills/kilo-config.md), and [custom subagents](https://kilo.ai/docs/customize/custom-subagents) for `.kilo/agents/` and frontmatter. ## Write targets @@ -56,3 +65,5 @@ The mode is chosen in the capture action. Never pick one silently. - **Asset-access precheck.** Before writing, confirm this reference is readable. If not, stop: the plugin is not installed in this host. - **Write-target validation.** After writing, confirm every path is relative, under the workspace, and at the chosen scope. Otherwise stop and report the bad path. +- **Publication preflight.** Render all selected outputs in memory. Before creating a directory or file, resolve and validate every selected target and its references, including the last target. Refuse a path outside the workspace, a symlink or non-regular collision, a non-writable target, an existing user agent that was not explicitly selected for modify, or a target-specific requested field unsupported by that tool. Ordinary canonical fields omitted during per-tool conversion are still dropped as specified above. Any refusal known at this gate leaves every selected target unchanged. Skip byte-identical writes, so a confirmed rerun preserves bytes and mtime. +- **Concurrent changes.** Compare each target's state with its preflight state immediately before writing it. Stop on a detected change and report any outputs already published. A change after that comparison can still race with the write. This interpreted workflow does not provide atomic publication or complete rollback across targets. diff --git a/plugins/aidd-context/skills/07-command-generate/actions/01-capture-command.md b/plugins/aidd-context/skills/07-command-generate/actions/01-capture-command.md index 4f16ef6af..e80d45f09 100644 --- a/plugins/aidd-context/skills/07-command-generate/actions/01-capture-command.md +++ b/plugins/aidd-context/skills/07-command-generate/actions/01-capture-command.md @@ -20,6 +20,7 @@ In-context: the command name and one-line goal, its location, its arguments, the 6. **Write mode.** Ask where the command goes: - **Host project**: detect the installed tools ([tool-paths.md](../references/tool-paths.md)), propose the supported ones, and confirm which to target. Never pick one silently. - **Plugin source**: confirm or create `plugins//commands/`. + - **Kilo**: when a Kilo signal is present, propose `.kilo/commands/.md`, without a nested location. Capture only requested `agent`, `model`, `variant`, and `subtask`. ## Test diff --git a/plugins/aidd-context/skills/07-command-generate/actions/02-write-command.md b/plugins/aidd-context/skills/07-command-generate/actions/02-write-command.md index 3ad15ad0c..eaa59176c 100644 --- a/plugins/aidd-context/skills/07-command-generate/actions/02-write-command.md +++ b/plugins/aidd-context/skills/07-command-generate/actions/02-write-command.md @@ -13,12 +13,13 @@ One command file per supported confirmed tool, the list of files written, and an ## Process 1. **Build.** Copy [command-template.md](../assets/command-template.md) into one canonical command: single objective, under ten steps, no Role section. Strip the scaffold (comments + `<...>`). -2. **Frontmatter.** Set the fields per tool from [tool-paths.md](../references/tool-paths.md). Drop a field a tool does not support. -3. **Render.** Per the write mode ([tool-paths.md](../references/tool-paths.md)): +2. **Frontmatter.** Render only the fields and location supported by the selected host, as specified in [tool-paths.md](../references/tool-paths.md). Drop unsupported fields. +3. **Arguments.** If the command takes input, reference it with `$ARGUMENTS`. For a CLI call, use the injection syntax only where the target tool supports it, otherwise describe the command in the body ([command-authoring.md](../references/command-authoring.md)). +4. **Preflight.** Render every supported selected output in memory, then resolve and validate every selected target and reference before creating anything, using [tool-paths.md](../references/tool-paths.md). Check the last target too. Refuse unsafe, symlinked, non-writable, colliding or target-specific requested fields unsupported by that tool; a refusal found here leaves all selected targets unchanged. +5. **Publish.** Before each write, compare that target's current state with its preflight state; stop on a detected change and report any outputs already published. Per the write mode ([tool-paths.md](../references/tool-paths.md)): - **Host**: for each supported confirmed tool, write to its path at the chosen location. Skip an unsupported tool, carrying its reason forward. - **Plugin source**: write one canonical `.md` command. No per-tool fan-out. -4. **Arguments.** If the command takes input, reference it with `$ARGUMENTS`. For a CLI call, use the injection syntax only where the target tool supports it, otherwise describe the command in the body ([command-authoring.md](../references/command-authoring.md)). -5. **Validate.** Run the write-target validation ([tool-paths.md](../references/tool-paths.md)). +6. **Validate.** Run the write-target validation ([tool-paths.md](../references/tool-paths.md)). Skip unchanged bytes; a repeated confirmed request is idempotent and preserves mtime. This interpreted workflow does not promise a multi-target rollback. ## Test diff --git a/plugins/aidd-context/skills/07-command-generate/actions/03-validate.md b/plugins/aidd-context/skills/07-command-generate/actions/03-validate.md index f284c5a49..76e689d6b 100644 --- a/plugins/aidd-context/skills/07-command-generate/actions/03-validate.md +++ b/plugins/aidd-context/skills/07-command-generate/actions/03-validate.md @@ -13,7 +13,7 @@ A short pass or fail line per command file. ## Process 1. **Exists.** Confirm each file is on disk at its expected path, at its chosen location. -2. **Shape.** Confirm the frontmatter and body match the tool. If the command takes input, the body uses `$ARGUMENTS`. +2. **Shape.** Validate each artifact against the selected host's path, frontmatter, and body requirements in [tool-paths.md](../references/tool-paths.md). If the command takes input, the body uses `$ARGUMENTS` where the target supports it. 3. **Concise.** Confirm a single objective and fewer than ten steps. ## Test diff --git a/plugins/aidd-context/skills/07-command-generate/references/tool-paths.md b/plugins/aidd-context/skills/07-command-generate/references/tool-paths.md index 663455b07..248f77a9d 100644 --- a/plugins/aidd-context/skills/07-command-generate/references/tool-paths.md +++ b/plugins/aidd-context/skills/07-command-generate/references/tool-paths.md @@ -11,10 +11,12 @@ The per-tool command path and write targets. Command slice only, nothing about s | OpenCode | `.opencode/commands//.md` | yes | | GitHub Copilot | `.github/prompts/.prompt.md` | yes (flat) | | Codex CLI | - | no | +| Kilo | `.kilo/commands/.md` | yes | `` is whatever the user chose: a flat folder, a namespace, or an opt-in `_/` from the taxonomy. Copilot is flat: no subfolder, so fold any location prefix into the filename. - **Codex CLI**: no custom slash commands, only built-ins. Skip it. Suggest a skill if a reusable workflow is needed. +- **Kilo**: use the filename as the workflow name, with no nested location. ## Frontmatter per tool @@ -26,6 +28,9 @@ Drop a field a tool does not support. Never invent a substitute. | Cursor | none (plain markdown), input via `$ARGUMENTS` | | OpenCode | `description`, input via `$ARGUMENTS` or `$1`, `$2` | | GitHub Copilot | `description`, `applyTo` (no `model`) | +| Kilo | `description`, `agent`, `model`, `variant`, `subtask` when requested | + +Kilo accepts only these documented fields. Do not inject Claude `argument-hint`, `allowed-tools`, `disable-model-invocation`, CLI injection, or a nested location. ## Detect (which tools are installed) @@ -35,6 +40,11 @@ Drop a field a tool does not support. Never invent a substitute. | `.cursor/` | Cursor | | `.opencode/` | OpenCode | | `.github/copilot-instructions.md` | GitHub Copilot | +| `.kilo/`, `kilo.json`, `kilo.jsonc`, `.kilo/kilo.json`, `.kilo/kilo.jsonc`, or `.kilocode/` | Kilo | + +The six Kilo signals are shared with the Kilo agent contract. `AGENTS.md` and `opencode.json[c]` alone are not Kilo signals. `.kilocode/` detects legacy Kilo but new workflows are emitted only under `.kilo/commands/`. + +Kilo sources: [project configuration](https://kilo.ai/docs/getting-started/settings), [legacy directory fallback](https://github.com/Kilo-Org/kilocode/blob/main/packages/opencode/src/kilocode/skills/kilo-config.md), and [workflows](https://kilo.ai/docs/customize/workflows) for `.kilo/commands/` and frontmatter. ## Write targets @@ -47,3 +57,5 @@ The mode is chosen in the capture action. Never pick one silently. - **Asset-access precheck**: before writing, confirm this reference is readable. If not, stop: the plugin is not installed in this host. - **Write-target validation**: after writing, confirm every path is relative, under the workspace, and at the chosen location. Otherwise stop and report the bad path. +- **Publication preflight**: render all supported selected outputs in memory. Before creating a directory or file, resolve and validate every selected target and reference, including the last target. Refuse a path outside the workspace, a symlink or non-regular collision, a non-writable target, an existing user workflow that was not explicitly selected for modify, or a target-specific requested field unsupported by that tool. Ordinary canonical fields omitted during per-tool rendering are still dropped as specified above. Any refusal known at this gate leaves every selected target unchanged. Skip byte-identical writes, so a confirmed rerun preserves bytes and mtime. +- **Concurrent changes**: compare each target's state with its preflight state immediately before writing it. Stop on a detected change and report any outputs already published. A change after that comparison can still race with the write. This interpreted workflow does not provide atomic publication or complete rollback across targets. diff --git a/plugins/aidd-context/skills/08-hook-generate/SKILL.md b/plugins/aidd-context/skills/08-hook-generate/SKILL.md index 5cd7a8838..110ac4d7e 100644 --- a/plugins/aidd-context/skills/08-hook-generate/SKILL.md +++ b/plugins/aidd-context/skills/08-hook-generate/SKILL.md @@ -6,17 +6,17 @@ argument-hint: event | action # Hook Generate -Builds one hook: an entry merged into the chosen scope for each supported tool, plus the backing script. +Builds one hook for tools with a documented declarative contract. For plugin-only tools, returns sourced guidance without generating a hook. ## Actions | # | Action | Role | Input | | --- | -------------- | ------------------------------------------------- | ----------------- | | 01 | `capture-hook` | Clarify the moment, action, matcher, scope, tools | user request | -| 02 | `write-hook` | Merge the entry per tool, write the script | the captured spec | -| 03 | `validate` | Check the file, the merge, and the moment fit | files written | +| 02 | `write-hook` | Return plugin guidance; preflight and write declarative targets | the captured spec | +| 03 | `validate` | Check guidance or written files and relaunch safety | action 02 result | -Run the actions in order, `01 → 03`, and run each action's `## Test` before the next. +Run actions `01 → 02 → 03` and each action's `## Test` in order for guidance-only, declarative-only, and mixed requests. Action 02 writes only confirmed declarative targets; for guidance-only requests it writes nothing, and action 03 checks the guidance and unchanged project tree. Before running an action, read its file in `actions/`, not only the table or assets. ## References diff --git a/plugins/aidd-context/skills/08-hook-generate/actions/01-capture-hook.md b/plugins/aidd-context/skills/08-hook-generate/actions/01-capture-hook.md index b5a752fd7..7414e4209 100644 --- a/plugins/aidd-context/skills/08-hook-generate/actions/01-capture-hook.md +++ b/plugins/aidd-context/skills/08-hook-generate/actions/01-capture-hook.md @@ -13,25 +13,28 @@ In-context decisions, nothing written yet: - the lifecycle moment the hook targets - the action it runs, and whether it needs a backing script - the matcher, or none for every occurrence -- the scope to install into +- the scope to install into, for declarative targets only - the confirmed tools, and any skipped with a reason -- the write mode: a host project, or a plugin source +- the write mode for declarative targets: a host project, or a plugin source +- a before-state of the project tree for guidance-only requests ## Process -1. **Gate.** Run the asset-access precheck ([tool-paths.md](../references/tool-paths.md)). -2. **Tools.** Detect the installed tools and confirm which to target ([tool-paths.md](../references/tool-paths.md)). Skip a tool that does not support hooks, with its reason. +1. **Gate.** Run the asset-access precheck ([tool-paths.md](../references/tool-paths.md)). For a guidance-only request, record the project tree's paths and file bytes before continuing, so action 03 can check that no file was written. +2. **Tools.** Detect the installed tools and confirm which to target ([tool-paths.md](../references/tool-paths.md)). OpenCode and Kilo are plugin-guidance targets, not declarative write targets. Skip tools without a documented applicable capability with a reason. 3. **Moment.** Pick the narrowest lifecycle moment that fits ([hook-authoring.md](../references/hook-authoring.md)). - - Confirm each target tool exposes that moment ([tool-paths.md](../references/tool-paths.md)). Skip a tool that lacks it, with its reason. + - For every target, confirm the event and required timing, matcher, payload, and blocking properties from its documentation ([hook-authoring.md](../references/hook-authoring.md), [tool-paths.md](../references/tool-paths.md)). Do not treat a nearby event as equivalent. If equivalence is unproved, skip publication for that target and explain why. Offer a behaviorally different moment only with its differences and explicit confirmation. Plugin-only targets receive sourced guidance only for a documented typed hook or event; otherwise say unsupported, with no invented fallback. 4. **Action.** Decide what runs at the moment, and whether it needs a backing script ([hook-authoring.md](../references/hook-authoring.md)). 5. **Matcher.** Set a matcher only when the moment must be filtered. Prefer a precise filter ([hook-authoring.md](../references/hook-authoring.md)). -6. **Scope.** Ask the user where to install: a single agent or skill component, the shared project, the project local-only, or the user's global config. Offer only the scopes the target tools support ([tool-paths.md](../references/tool-paths.md)). +6. **Scope.** For declarative targets, ask the user where to install: a single agent or skill component, the shared project, the project local-only, or the user's global config. Offer only the scopes those tools support ([tool-paths.md](../references/tool-paths.md)). Plugin guidance names documented paths without choosing or writing one. - For a component scope, name the exact skill or agent file, and confirm the moment fits a component-scoped hook ([tool-paths.md](../references/tool-paths.md)). - - State the resolved file and confirm. Never pick silently. -7. **Write mode.** Host project, or a plugin source. For a plugin source, name the plugin. + - For declarative targets, state the resolved file and confirm. Never pick silently. +7. **Write mode.** For declarative targets, choose host project or plugin source. For a plugin source, name the plugin. Guidance-only targets have no write mode. +8. **Plugin guidance.** For each selected plugin-only tool, return terminal guidance stating its documented JS/TS plugin paths, the documented event or unsupported limit, the official source, and that no files are generated for that tool. Carry the guidance to actions 02 and 03, including in a mixed request. ## Test -- Every decision is stated and confirmed in writing. -- The resolved scope and file are named before any write. -- Each tool was confirmed to support the moment, or skipped with its reason. +- Every applicable decision is stated and confirmed in writing. +- Each declarative target's resolved scope and file are named before any write; guidance-only targets require neither. +- Each tool has a documented equivalent event and required properties, or its publication is skipped with a reason. A merely nearby event is not substituted. +- OpenCode-only and Kilo-only guidance name their sources and leave the complete project tree unchanged. diff --git a/plugins/aidd-context/skills/08-hook-generate/actions/02-write-hook.md b/plugins/aidd-context/skills/08-hook-generate/actions/02-write-hook.md index fbddc89c1..faf69e674 100644 --- a/plugins/aidd-context/skills/08-hook-generate/actions/02-write-hook.md +++ b/plugins/aidd-context/skills/08-hook-generate/actions/02-write-hook.md @@ -1,25 +1,30 @@ # 02 - Write hook -Render the entry for each confirmed tool, merge it into the chosen scope, and write the backing script. +Return plugin guidance and write only confirmed declarative targets after preflighting the entire fan-out. ## Input -From 01: the moment, action, script need, matcher, scope, confirmed tools, and write mode. +From 01: the moment, action, script need, matcher, confirmed tools, and any declarative scope and write mode. Carry the guidance-only before-state through to action 03. ## Output -The updated target file per tool, any script written, and the list of paths touched. +The updated declarative target file per tool, any script written, the list of paths touched, and separate plugin guidance. ## Process -1. **Script.** If the action needs a backing script ([hook-authoring.md](../references/hook-authoring.md)): copy [hook-script-template.sh](../assets/hook-script-template.sh), fill the logic, place it in the scope's script directory ([tool-paths.md](../references/tool-paths.md)), and make it executable. One script can back every tool. -2. **Entry.** Per confirmed tool, fill [hook-template.json](../assets/hook-template.json) from [tool-paths.md](../references/tool-paths.md), using that tool's shape and stripping the scaffold. +1. **Guidance.** Return sourced OpenCode or Kilo plugin guidance from action 01, including an unsupported event limit where applicable. Report it separately from written targets in a mixed fan-out. Do not generate a script, plugin, or config for either plugin-only tool. +2. **Preflight.** Before any script or config write, resolve every selected declarative destination. Exclude targets without a documented equivalent event or required properties; they receive no hook or backing script. Read and parse each remaining config in its own format, validate the event list/entry shape and every existing handler needed for the merge, and reject an invalid entry, collision, symlink escape, unsafe script path or unwritable target. Stage the intended entry and handler bytes in memory. If any target is invalid, abort before any write; report the failing path and leave all targets unchanged. A guidance-only request writes nothing and continues to action 03. +3. **Script.** If the action needs a backing script ([hook-authoring.md](../references/hook-authoring.md)): copy [hook-script-template.sh](../assets/hook-script-template.sh), fill the logic, place it in the scope's script directory ([tool-paths.md](../references/tool-paths.md)), and make it executable. One script can back every declarative tool. Reuse an existing byte-identical AIDD script without rewriting it; do not overwrite user content. +4. **Entry.** Per confirmed declarative tool, fill [hook-template.json](../assets/hook-template.json) from [tool-paths.md](../references/tool-paths.md), using that tool's shape and stripping the scaffold. - Point the handler at the script by absolute path or an approved `${VAR}`. -3. **Merge.** For each tool, read the target file and append the entry to the moment's list under the right key for the scope ([tool-paths.md](../references/tool-paths.md)). Preserve every sibling, never overwrite. -4. **Validate.** Run the merge check and write-target validation ([tool-paths.md](../references/tool-paths.md)). +5. **Merge.** For each tool, append only when that exact AIDD entry is absent from the chosen event list. Compare the full entry, including matcher and handler command, not just the event name. Preserve existing order, every user hook and user duplicate, and pre-existing AIDD duplicates. Never normalize or remove prior entries. Skip byte-identical writes on a second pass. +6. **Validate.** Run the merge check and write-target validation ([tool-paths.md](../references/tool-paths.md)). ## Test - Each target file is valid after the merge. - The new entry is present in each, and every prior sibling survives. - A script-backed handler's script exists at its path and is executable. +- A second identical pass leaves config/script bytes and mtimes unchanged; user hooks and duplicates survive. +- An invalid last target aborts before any write to an earlier target. +- A target with an absent or merely nearby non-equivalent event has no hook, script, or config published for it; confirmed declarative targets in a mixed request remain eligible. diff --git a/plugins/aidd-context/skills/08-hook-generate/actions/03-validate.md b/plugins/aidd-context/skills/08-hook-generate/actions/03-validate.md index ac660ec00..e90a8cf28 100644 --- a/plugins/aidd-context/skills/08-hook-generate/actions/03-validate.md +++ b/plugins/aidd-context/skills/08-hook-generate/actions/03-validate.md @@ -1,10 +1,10 @@ # 03 - Validate -Check the written hook against the contract, per tool. +Check plugin guidance and every written declarative hook against its own contract. ## Input -The list of paths touched (from 02). +The list of paths touched and plugin guidance (from 02), including an empty path list and the before-state from action 01 for guidance-only requests. ## Output @@ -12,12 +12,17 @@ A short pass or fail line per tool, plus the script. ## Process -1. **Parse.** Confirm each target file still parses in its format and the entry sits at the right key for its scope. -2. **Moment.** Confirm the event name is the right one for that tool's moment, and the matcher is well-formed ([tool-paths.md](../references/tool-paths.md)). -3. **Blocking.** If the hook blocks, confirm the moment can block on that tool ([tool-paths.md](../references/tool-paths.md)). -4. **Script.** For a script-backed handler, confirm the script exists, is executable, and reads stdin and signals per the contract. +1. **Guidance.** For each OpenCode or Kilo target, confirm the guidance cites its official source and a documented event or explicit unsupported limit. For a guidance-only request, compare the complete project tree before and after: no write. For mixed requests, confirm no plugin-only hook, config, script, or plugin was added. +2. **Parse.** Confirm each written declarative target file still parses in its format and the entry sits at the right key for its scope. +3. **Moment.** For each declarative publication, confirm the event's documented semantic equivalence, timing, payload and matcher against the requested moment ([hook-authoring.md](../references/hook-authoring.md), [tool-paths.md](../references/tool-paths.md)). A merely nearby event is not an equivalent. Confirm skipped targets have no published artifact. +4. **Blocking.** If the hook blocks, confirm the documented event and tool can block at that moment ([tool-paths.md](../references/tool-paths.md)). +5. **Script.** For a script-backed handler, confirm the script exists, is executable, and reads stdin and signals per the contract. +6. **Relance.** For declarative targets, execute a second pass with identical inputs: config and script bytes and mtimes must be identical; user entries, including duplicates, remain untouched. Test an invalid final target in mixed fan-out and confirm the full tree has no new files or changed bytes. For guidance-only requests, the tree comparison in step 1 is the no-write check. ## Test -- Each target file parses with the entry under the correct key. -- Each event name matches the tool's moment, and any blocking hook sits on a moment that can block. +- Each written declarative target file parses with the entry under the correct key. +- Each published event matches the target tool's moment, and any blocking hook sits on a moment that can block. +- A second declarative pass is byte-identical and leaves user hooks intact; invalid input fails before any write. +- OpenCode-only and Kilo-only requests run this action, cite supported events or unsupported limits, and leave the project tree unchanged. +- A mixed request preserves declarative validation and publishes nothing for unsupported or plugin-only targets. diff --git a/plugins/aidd-context/skills/08-hook-generate/references/hook-authoring.md b/plugins/aidd-context/skills/08-hook-generate/references/hook-authoring.md index 8a8d4ecab..8f47d9f94 100644 --- a/plugins/aidd-context/skills/08-hook-generate/references/hook-authoring.md +++ b/plugins/aidd-context/skills/08-hook-generate/references/hook-authoring.md @@ -1,6 +1,6 @@ # Hook authoring -The contract every generated hook must satisfy. A hook is a handler wired to a lifecycle moment, a config entry that runs a script when that moment occurs. +The event-mapping contract applies to every target. The script-and-entry rules below apply to generated declarative hooks; plugin-only tools use guidance from [tool-paths.md](tool-paths.md). ## Rules @@ -14,7 +14,7 @@ The contract every generated hook must satisfy. A hook is a handler wired to a l ## Lifecycle moments -The agnostic moments a hook can target. Each tool names these differently and supports a different subset. `references/tool-paths.md` maps the commonly supported moments to each tool's event name; a moment absent from that table has no dedicated cross-tool event, so fold it into the nearest supported moment. +The agnostic moments a hook can target. Each tool names these differently and supports a different subset. [tool-paths.md](tool-paths.md) maps commonly supported moments for declarative tools and records plugin-only guidance. For every target, use an event automatically only when its semantic equivalence to the requested moment and relevant timing, matcher, payload, and blocking behavior are documented. A nearby event is not equivalent by proximity. If equivalence is unproved, do not publish for that target; explain the missing guarantee. You may propose a different moment only by describing its behavioral differences and obtaining explicit confirmation for that different request. Never invent an event, matcher, or blocking capability. | Moment | Fires when | | ------------------- | --------------------------------------------------- | @@ -30,7 +30,7 @@ The agnostic moments a hook can target. Each tool names these differently and su | turn stop | the agent finishes its turn | | session end | a session ends | -Confirm the chosen moment exists for the target tool before wiring. If it does not, skip that tool with the reason. +Confirm the chosen moment and its required properties for each target before wiring. If they are absent or unverified, skip that target with the reason; an unsupported target must not receive a hook or backing script. ## Handler contract diff --git a/plugins/aidd-context/skills/08-hook-generate/references/tool-paths.md b/plugins/aidd-context/skills/08-hook-generate/references/tool-paths.md index 48b1d19ef..d5e5529fb 100644 --- a/plugins/aidd-context/skills/08-hook-generate/references/tool-paths.md +++ b/plugins/aidd-context/skills/08-hook-generate/references/tool-paths.md @@ -1,22 +1,33 @@ # Tool paths (hooks) -Per-tool hook support, event names, file formats, and scopes. Hook slice only: nothing about skills, rules, agents, commands, plugins, or marketplaces. +Per-tool hook support, event names, file formats, and scopes. OpenCode and Kilo plugin APIs are guidance surfaces here; this skill does not generate plugins. + +## Host detection + +Detect Kilo when the project has `.kilo/` (including `.kilo/kilo.json` or `.kilo/kilo.jsonc`), root `kilo.json` or `kilo.jsonc`, or legacy `.kilocode/`. The legacy directory is a detection signal only; new Kilo plugins belong under `.kilo/`. Root `AGENTS.md`, `opencode.json`, or `opencode.jsonc` alone do not identify Kilo. This follows the onboarding detection contract without requiring another installed skill; this generator provides Kilo plugin guidance without writing files. + +Kilo detection sources: [project configuration](https://kilo.ai/docs/getting-started/settings) and [legacy directory fallback](https://github.com/Kilo-Org/kilocode/blob/main/packages/opencode/src/kilocode/skills/kilo-config.md). ## Per-tool support -| Tool | Supported | Note | +| Tool | Output | Note | | -------------- | --------- | --------------------------------------------------------------- | -| Claude Code | yes | JSON config + script. Richest event set. | -| Codex CLI | yes | JSON/TOML config + script. Hooks are stable, on by default. | -| Cursor | yes | JSON config + script. | -| GitHub Copilot | yes | JSON config + script. Also reads Claude's `.claude/` config. | -| OpenCode | no | Hooks are JS/TS plugin modules, not config. Skip with the reason below. | +| Claude Code | declarative | JSON config + script. Richest event set. | +| Codex CLI | declarative | JSON/TOML config + script. Hooks are stable, on by default. | +| Cursor | declarative | JSON config + script. | +| GitHub Copilot | declarative | JSON config + script. Also reads Claude's `.claude/` config. | +| OpenCode | guidance | JS/TS plugin module; no declarative config or generated script. | +| Kilo | guidance | JS/TS plugin module; no declarative config or generated script. | -**OpenCode skip reason.** OpenCode hooks are code, not a config entry plus a script. Point the user to write a plugin under `.opencode/plugins/` (project) or `~/.config/opencode/plugins/` (user), per `https://opencode.ai/docs/plugins`. This skill does not generate it. +**Plugin-only guidance.** For OpenCode or Kilo, cite the official plugin documentation and a documented event or typed hook only when its timing and required matcher, payload, and blocking behavior fit the request. Otherwise state the unsupported or unverified limit and do not claim an equivalent. A different moment may be proposed with its differences and explicit confirmation. This generator publishes no hook, backing script, config, or plugin for either tool. In a mixed request, keep this guidance separate from declarative publications. + +**OpenCode guidance.** OpenCode hooks are JS/TS plugin code, not a config entry plus a script. The documented directories are `.opencode/plugins/` (project) and `~/.config/opencode/plugins/` (user). The [official plugin reference](https://opencode.ai/docs/plugins) lists `session.created` and `tool.execute.before`/`tool.execute.after`; name one only when its documented semantics fit the requested moment. These paths are OpenCode-supported, not AIDD output destinations. + +**Kilo guidance.** The official project path explicitly documented for JavaScript or TypeScript modules is `.kilo/plugin/`; `.kilocode/plugin/` is the legacy path. Recommend `.kilo/plugin/` for new project plugins. Do not present `.kilo/plugins/` as a confirmed project path. These paths are Kilo-supported, not AIDD output destinations; AIDD does not generate a Kilo plugin or hook here. The API exposes typed hooks and an `event` bus. For `session start`, `session.created` is documented; the existing AIDD bridge in `cli/src/contexts/tools/domain/profiles/kilo/kilo-hooks-bridge.ts` may be cited only for the events it actually maps. For any other requested moment, check the [official plugin documentation](https://kilo.ai/docs/automate/extending/plugins) and the bridge before naming an equivalent. If the equivalent is unproved, state `unsupported: no documented Kilo event for this requested moment; no fallback mapping`, and ask for a different moment or a separately scoped plugin implementation. Do not create `.kilo/hooks.json`, Claude `hooks.json`, a shell script, or a generic plugin for Kilo. ## Lifecycle moment to event name -Each tool names the same moment differently and supports a different subset. Core moments, with the canonical event name per tool. A `-` means the tool does not expose that moment. +Each tool names moments differently and supports a different subset. The table gives documented core event names for declarative tools; it does not itself prove that every matcher, payload, or blocking property fits a request. A `-` means this table has no event for that moment; check the target's official reference before concluding that no such event exists. Never substitute another table row merely because its event occurs nearby. | Moment | Claude Code | Codex CLI | Cursor | GitHub Copilot | | ------------------ | ------------------ | --------------- | -------------------- | ------------------ | @@ -24,12 +35,15 @@ Each tool names the same moment differently and supports a different subset. Cor | prompt submitted | `UserPromptSubmit` | `UserPromptSubmit` | `beforeSubmitPrompt` | `UserPromptSubmit` | | before a tool runs | `PreToolUse` | `PreToolUse` | `preToolUse` | `PreToolUse` | | after a tool runs | `PostToolUse` | `PostToolUse` | `postToolUse` | `PostToolUse` | +| after a tool fails | `PostToolUseFailure` | - | `postToolUseFailure` | `PostToolUseFailure` | | before compaction | `PreCompact` | `PreCompact` | `preCompact` | `PreCompact` | | subagent stop | `SubagentStop` | `SubagentStop` | `subagentStop` | `SubagentStop` | | turn stop | `Stop` | `Stop` | `stop` | `Stop` | | session end | `SessionEnd` | `SessionEnd` | `sessionEnd` | `SessionEnd` | -Each tool exposes more moments than these. For the full list, read the tool's docs: Claude `https://code.claude.com/docs/en/hooks`, Codex `https://developers.openai.com/codex/hooks`, Cursor `https://cursor.com/docs/hooks`, Copilot `https://docs.github.com/en/copilot/reference/hooks-configuration`. Confirm a moment exists before wiring it. Copilot also accepts the camelCase names (`sessionStart`, `preToolUse`). +Each tool exposes more moments than these. For the full list and event properties, read the tool's docs: Claude `https://code.claude.com/docs/en/hooks`, Codex `https://developers.openai.com/codex/hooks`, Cursor `https://cursor.com/docs/hooks`, Copilot `https://docs.github.com/en/copilot/reference/hooks-configuration`. Confirm the requested meaning and properties before wiring. Copilot also accepts the camelCase names (`sessionStart`, `preToolUse`). The table does not define OpenCode or Kilo equivalents; their guidance follows their documented plugin APIs. + +For example, [Claude](https://code.claude.com/docs/en/hooks), [Cursor](https://cursor.com/docs/hooks), and [Copilot](https://docs.github.com/en/copilot/reference/hooks-configuration) document failure-specific tool events. [Codex `PostToolUse`](https://developers.openai.com/codex/hooks) also runs after a non-zero Bash exit, but it also runs after successful tool output. It is not an automatic equivalent of "after a tool fails"; skip Codex for that exact request unless an official failure-specific contract is verified. The tools also differ on validation errors, permission denials, payloads, and blocking, so check the requested properties rather than relying on the event names alone. ## File and format per tool @@ -76,6 +90,7 @@ Written as `${VAR}` inside a command: `CLAUDE_PROJECT_DIR` (project root), `CLAU - **Host project.** Merge the entry into the resolved scope's file for each confirmed supported tool. The script goes in a `hooks/` dir beside the config by default, or another dir the user names, referenced by absolute path or an approved `${VAR}`. - **Plugin source.** Merge into `plugins//hooks/hooks.json` (the bare hooks object), with the script under `plugins//hooks/scripts/`. +- **OpenCode or Kilo.** No write target. Return the per-tool guidance above for the requested moment, even in a mixed fan-out. ## Safety checks diff --git a/scripts/__tests__/context-agent-command-artifacts.test.js b/scripts/__tests__/context-agent-command-artifacts.test.js new file mode 100644 index 000000000..b0773cea5 --- /dev/null +++ b/scripts/__tests__/context-agent-command-artifacts.test.js @@ -0,0 +1,79 @@ +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const test = require('node:test'); + +const ROOT = path.resolve(__dirname, '../..'); +const read = (file) => fs.readFileSync(path.join(ROOT, file), 'utf8'); +const normalizeEol = (text) => text.replace(/\r\n/gu, '\n'); +const agent = (file) => read(`plugins/aidd-context/skills/06-agent-generate/${file}`); +const command = (file) => read(`plugins/aidd-context/skills/07-command-generate/${file}`); +const FIXTURES = path.join(__dirname, 'fixtures/context-generation/agents-commands'); + +test('static contract: Kilo agents use canonical subagent Markdown without Claude name', () => { + const paths = agent('references/tool-paths.md'); + assert.match(paths, /\| Kilo\s*\| `\.kilo\/agents\/\.md`/u); + assert.match(paths, /description.*subagent/u); + assert.match(paths, /`model`, `temperature`, `permission`/u); + assert.match(paths, /name is the filename/u); + assert.match(paths, /six Kilo signals/u); +}); + +test('static contract: Kilo workflows use the canonical command path and supported fields', () => { + const paths = command('references/tool-paths.md'); + assert.match(paths, /\| Kilo\s*\| `\.kilo\/commands\/\.md`/u); + assert.match(paths, /`description`, `agent`, `model`, `variant`, `subtask`/u); + assert.match(paths, /no nested location/u); + assert.match(paths, /six Kilo signals/u); + assert.match(command('actions/02-write-command.md'), /Render only the fields and location supported by the selected host/u); + assert.match(command('actions/03-validate.md'), /selected host's path, frontmatter, and body requirements/u); +}); + +test('static contract: agent and workflow writers preflight targets and skip unsupported Codex commands', () => { + for (const [action, paths] of [ + [agent('actions/02-write-agent.md'), agent('references/tool-paths.md')], + [command('actions/02-write-command.md'), command('references/tool-paths.md')], + ]) { + const preflight = action.indexOf('**Preflight.**'); + const publish = action.indexOf('**Publish.**'); + assert.ok(preflight > action.indexOf('**Build.**')); + assert.ok(publish > preflight); + assert.match(action.slice(preflight, publish), /every selected target and reference before creating anything/u); + assert.match(action.slice(preflight, publish), /last target too/u); + assert.match(action.slice(preflight, publish), /refusal found here leaves all selected targets unchanged/u); + assert.match(action.slice(publish), /Before each write, compare that target's current state with its preflight state/u); + assert.match(paths, /existing user (?:agent|workflow).*target-specific requested field unsupported by that tool/u); + assert.match(paths, /Any refusal known at this gate leaves every selected target unchanged/u); + assert.match(paths, /Ordinary canonical fields omitted.*still dropped/u); + assert.match(paths, /compare each target's state with its preflight state immediately before writing it/iu); + assert.match(paths, /A change after that comparison can still race with the write/u); + assert.match(paths, /does not provide atomic publication or complete rollback across targets/u); + assert.match(action, /Skip unchanged bytes; a repeated confirmed request is idempotent and preserves mtime/u); + } + assert.match(command('references/tool-paths.md'), /Codex CLI.*Skip/u); +}); + +test('eval cases cover a last-target refusal for both interpreted generators', () => { + const cases = JSON.parse(read('scripts/skill-eval/cases.json')); + for (const [skill, name, absent, preserved] of [ + ['06-agent-generate', 'last Kilo agent collision refuses before Claude write', '.claude/agents/verify-agent.md', '.kilo/agents/verify-agent.md'], + ['07-command-generate', 'last Kilo workflow collision refuses before Claude write', '.claude/commands/verify-workflow.md', '.kilo/commands/verify-workflow.md'], + ]) { + const scenario = cases.find((item) => item.skill === skill && item.name === name); + assert.ok(scenario, name); + assert.match(scenario.prompt, /Claude first and Kilo last/u); + assert.ok(scenario.expect.filesAbsent.includes(absent)); + assert.ok(scenario.expect.filesUnchanged.includes(preserved)); + } +}); + +test('fixture contract: Kilo agent and workflow have target-specific frontmatter and bodies', () => { + const agentText = normalizeEol(fs.readFileSync(path.join(FIXTURES, 'native/.kilo/agents/verify-agent.md'), 'utf8')); + const commandText = normalizeEol(fs.readFileSync(path.join(FIXTURES, 'native/.kilo/commands/verify-workflow.md'), 'utf8')); + assert.match(agentText, /^---\ndescription: Verify the phase four agent payload\.\nmode: subagent\ntemperature: 0\npermission:\n read: allow\n---/u); + assert.doesNotMatch(agentText, /^---[\s\S]*?\nname:/u); + assert.match(agentText, /\.kilo\/agents\/assets\/agent-payload\.txt/u); + assert.match(commandText, /^---\ndescription: Verify the phase four workflow payload\.\nagent: verify-agent\nvariant: minimal\nsubtask: true\n---/u); + assert.doesNotMatch(commandText, /argument-hint|allowed-tools|disable-model-invocation/u); + assert.match(commandText, /assets\/workflow-payload\.txt/u); +}); diff --git a/scripts/__tests__/context-generation-evidence.test.js b/scripts/__tests__/context-generation-evidence.test.js new file mode 100644 index 000000000..310a98cc1 --- /dev/null +++ b/scripts/__tests__/context-generation-evidence.test.js @@ -0,0 +1,51 @@ +const assert = require('node:assert/strict'); +const { createHash } = require('node:crypto'); +const { existsSync, readFileSync } = require('node:fs'); +const path = require('node:path'); +const test = require('node:test'); + +const root = path.join(__dirname, 'fixtures/context-generation'); +const manifest = JSON.parse(readFileSync(path.join(root, 'artifact-manifest.json'), 'utf8')); +const read = (relative) => readFileSync(path.join(root, relative)); + +test('local Kilo generation fixtures match their checked-in artifact manifest', () => { + assert.ok(Object.keys(manifest).length > 0); + for (const [relative, expected] of Object.entries(manifest)) { + assert.equal(createHash('sha256').update(read(relative)).digest('hex'), expected, relative); + } +}); + +test('local Kilo skill fixture contains one native tree and preserves its user resource', () => { + const skill = read('skills/native/.kilo/skills/verify-payload/SKILL.md').toString('utf8'); + assert.match(skill, /^name: verify-payload$/mu); + assert.match(skill, /description:/u); + assert.doesNotMatch(skill, /argument-hint:|allowed-tools:/u); + assert.equal(existsSync(path.join(root, 'skills/native/.agents/skills/verify-payload/SKILL.md')), false); + assert.equal( + read('skills/native/.kilo/skills/verify-payload/assets/user.txt').toString('utf8'), + 'USER_RESOURCE_DO_NOT_REPLACE\n', + ); + assert.match( + read('skills/native/.kilo/skills/verify-payload/actions/01-verify.md').toString('utf8'), + /assets\/payload\.txt/u, + ); +}); + +test('local Kilo rule fixtures distinguish linked and unlinked configurations', () => { + const rule = read('kilo-rule/rule.md').toString('utf8'); + const positive = read('kilo-rule/positive-config.jsonc').toString('utf8'); + const negative = read('kilo-rule/negative-config.jsonc').toString('utf8'); + const exactPath = '.kilo/rules/01-standards/1-naming.md'; + assert.match(rule, /KILO_RULE_FIXTURE_MARKER/u); + assert.ok(positive.includes(exactPath)); + assert.ok(!negative.includes(exactPath)); +}); + +test('local agent and workflow fixtures retain Kilo frontmatter and payload paths', () => { + const agent = read('agents-commands/native/.kilo/agents/verify-agent.md').toString('utf8'); + const workflow = read('agents-commands/native/.kilo/commands/verify-workflow.md').toString('utf8'); + assert.match(agent, /mode: subagent/u); + assert.match(agent, /\.kilo\/agents\/assets\/agent-payload\.txt/u); + assert.match(workflow, /agent: verify-agent/u); + assert.match(workflow, /\.kilo\/commands\/assets\/workflow-payload\.txt/u); +}); diff --git a/scripts/__tests__/context-hook-generation.test.js b/scripts/__tests__/context-hook-generation.test.js new file mode 100644 index 000000000..921a8332a --- /dev/null +++ b/scripts/__tests__/context-hook-generation.test.js @@ -0,0 +1,143 @@ +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const test = require('node:test'); + +const ROOT = path.resolve(__dirname, '../..'); +const hook = (file) => fs.readFileSync(path.join(ROOT, 'plugins/aidd-context/skills/08-hook-generate', file), 'utf8'); +const fixture = (file) => JSON.parse(fs.readFileSync(path.join(__dirname, 'fixtures/context-generation/hooks/mixed', file), 'utf8')); + +test('contract: plugin-only requests run through no-write validation', () => { + const paths = hook('references/tool-paths.md'); + const router = hook('SKILL.md'); + const capture = hook('actions/01-capture-hook.md'); + const write = hook('actions/02-write-hook.md'); + const validate = hook('actions/03-validate.md'); + const evalCase = JSON.parse(fs.readFileSync(path.join(ROOT, 'scripts/skill-eval/cases.json'), 'utf8')) + .find((item) => item.name === 'Kilo-only session start gives guidance without files'); + const evalCases = evalCase.expect; + assert.match(paths, /official project path explicitly documented[\s\S]*`\.kilo\/plugin\/`/u); + assert.match(paths, /Do not present `\.kilo\/plugins\/` as a confirmed project path/u); + assert.match(paths, /These paths are Kilo-supported, not AIDD output destinations/u); + assert.match(paths, /AIDD does not generate a Kilo plugin or hook/u); + assert.match(paths, /https:\/\/kilo\.ai\/docs\/automate\/extending\/plugins/u); + assert.match(paths, /session\.created/u); + assert.match(paths, /unsupported.*no.*fallback/iu); + assert.match(paths, /\| OpenCode\s+\| guidance/u); + assert.match(paths, /\| Kilo\s+\| guidance/u); + assert.match(paths, /\.opencode\/plugins\//u); + assert.match(paths, /These paths are OpenCode-supported, not AIDD output destinations/u); + assert.match(router, /Run actions `01 → 02 → 03`[\s\S]*guidance-only/u); + assert.match(capture, /OpenCode and Kilo are plugin-guidance targets/u); + assert.match(capture, /record the project tree's paths and file bytes before continuing/u); + assert.match(capture, /guidance-only targets require neither/u); + assert.match(write, /A guidance-only request writes nothing and continues to action 03/u); + assert.match(validate, /For a guidance-only request, compare the complete project tree before and after: no write/u); + assert.match(validate, /For mixed requests, confirm no plugin-only hook, config, script, or plugin was added/u); + assert.match(evalCase.prompt, /Explicitly state that the docs do not confirm \.kilo\/plugins\//u); + assert.ok(evalCases.stdoutMatches.includes('\\.kilo/plugin/')); + assert.ok(evalCases.stdoutMatches.includes('session\\.created')); + assert.equal(evalCases.projectTreeUnchanged, true); + assert.match(evalCases.judge, /explicitly states that \.kilo\/plugins\/ is not a confirmed project path/u); + assert.equal(evalCases.stdoutNotContains, undefined); +}); + +test('contract: event mapping requires documented semantics and rejects nearby substitutions', () => { + const authoring = hook('references/hook-authoring.md'); + const paths = hook('references/tool-paths.md'); + const capture = hook('actions/01-capture-hook.md'); + const write = hook('actions/02-write-hook.md'); + const validate = hook('actions/03-validate.md'); + assert.match(authoring, /semantic equivalence[\s\S]*timing, matcher, payload, and blocking behavior are documented/u); + assert.match(authoring, /A nearby event is not equivalent by proximity/u); + assert.match(authoring, /do not publish for that target[\s\S]*explicit confirmation/u); + assert.doesNotMatch(authoring, /fold it into the nearest|consider a nearest documented supported moment/u); + assert.match(paths, /\| session start\s+\| `SessionStart`\s+\| `SessionStart`\s+\| `sessionStart`\s+\| `SessionStart`/u); + assert.match(paths, /\| after a tool fails\s+\| `PostToolUseFailure`\s+\| -\s+\| `postToolUseFailure`\s+\| `PostToolUseFailure`/u); + assert.match(paths, /Codex `PostToolUse`[\s\S]*not an automatic equivalent of "after a tool fails"/u); + assert.match(capture, /skip publication for that target and explain why/u); + assert.match(write, /Exclude targets without a documented equivalent event or required properties/u); + assert.match(validate, /A merely nearby event is not an equivalent/u); +}); + +test('eval cases cover supported, absent, nearby and mixed plugin-only targets', () => { + const cases = JSON.parse(fs.readFileSync(path.join(ROOT, 'scripts/skill-eval/cases.json'), 'utf8')); + const named = (name) => { + const found = cases.find((item) => item.name === name); + assert.ok(found, name); + return found; + }; + const openCode = named('OpenCode-only session start gives plugin guidance without files'); + assert.ok(openCode.expect.filesAbsent.includes('.opencode/plugins/aidd-hook.ts')); + assert.ok(openCode.expect.filesUnchanged.includes('opencode.json')); + assert.equal(openCode.expect.projectTreeUnchanged, true); + const supported = named('declarative session start keeps documented event names'); + assert.match(supported.prompt, /Claude, Codex, Cursor and Copilot/u); + assert.deepEqual(supported.expect.jsonValid, [ + '.claude/settings.json', '.codex/hooks.json', '.cursor/hooks.json', '.github/hooks/aidd.json', + ]); + assert.match(supported.expect.fileMatches['.claude/settings.json'], /SessionStart/u); + assert.match(supported.expect.fileMatches['.codex/hooks.json'], /SessionStart/u); + assert.match(supported.expect.fileMatches['.cursor/hooks.json'], /sessionStart/u); + assert.match(supported.expect.fileMatches['.github/hooks/aidd.json'], /SessionStart/u); + for (const pattern of Object.values(supported.expect.fileMatches)) assert.doesNotThrow(() => new RegExp(pattern)); + assert.deepEqual(supported.expect.fileNotContains['.claude/settings.json'], ['"matcher"']); + assert.deepEqual(supported.expect.fileNotContains['.codex/hooks.json'], ['"matcher"']); + const supportedWithoutCodex = named('declarative session start publishes supported non-Codex targets'); + assert.ok(supportedWithoutCodex.expect.filesExist.includes('.cursor/hooks.json')); + assert.ok(supportedWithoutCodex.expect.filesAbsent.includes('.codex/hooks.json')); + assert.deepEqual(supportedWithoutCodex.expect.jsonValid, [ + '.claude/settings.json', '.cursor/hooks.json', '.github/hooks/aidd.json', + ]); + assert.match(supportedWithoutCodex.expect.fileMatches['.claude/settings.json'], /SessionStart/u); + assert.match(supportedWithoutCodex.expect.fileMatches['.cursor/hooks.json'], /sessionStart/u); + assert.match(supportedWithoutCodex.expect.fileMatches['.github/hooks/aidd.json'], /SessionStart/u); + for (const pattern of Object.values(supportedWithoutCodex.expect.fileMatches)) assert.doesNotThrow(() => new RegExp(pattern)); + assert.deepEqual(supportedWithoutCodex.expect.fileNotContains['.claude/settings.json'], ['"matcher"']); + const absent = named('unsupported exact moment skips every declarative target'); + assert.ok(absent.expect.filesAbsent.includes('.claude/settings.json')); + const nearby = named('tool failure does not become Codex PostToolUse'); + assert.ok(nearby.expect.filesAbsent.includes('.codex/hooks.json')); + assert.match(nearby.expect.judge, /success as well as failure/u); + const mixed = named('plugin-only guidance and declarative hook stay separate'); + assert.ok(mixed.expect.filesAbsent.includes('.kilo/hooks.json')); + assert.ok(mixed.expect.filesAbsent.includes('.opencode/plugins/aidd-hook.ts')); +}); + +test('static contract: mixed hooks preflight before writes and preserve user hooks', () => { + const write = hook('actions/02-write-hook.md'); + const validate = hook('actions/03-validate.md'); + assert.ok(write.indexOf('**Preflight.**') < write.indexOf('**Script.**')); + const preflight = write.match(/2\. \*\*Preflight\.\*\*([\s\S]*?)(?=\n3\. \*\*Script\.\*\*)/u)?.[1] ?? ''; + assert.match(write, /exact AIDD entry/u); + assert.match(write, /preserve.*user.*duplicate/iu); + assert.match(preflight, /If any target is invalid, abort before any write/u); + assert.match(validate, /second declarative pass.*byte.identical/iu); +}); + +test('fixture contract: mixed hook configurations retain user duplicates and one AIDD entry per target', () => { + const configs = [ + ['.claude/settings.json', 'SessionStart', (entry) => entry.hooks[0].command], + ['.codex/hooks.json', 'SessionStart', (entry) => entry.hooks[0].command], + ['.cursor/hooks.json', 'sessionStart', (entry) => entry.command], + ['.github/hooks/aidd.json', 'SessionStart', (entry) => entry.command], + ]; + for (const [file, event, command] of configs) { + const config = fixture(file); + const entries = config.hooks[event].map(command); + assert.deepEqual(entries, ['/project/hooks/user.sh', '/project/hooks/user.sh', '/project/hooks/aidd.sh'], file); + } + assert.deepEqual(fixture('kilo.json'), {}); + assert.equal(fs.existsSync(path.join(__dirname, 'fixtures/context-generation/hooks/mixed/.kilo/hooks.json')), false); + const userBody = fs.readFileSync(path.join(__dirname, 'fixtures/context-generation/hooks/mixed/hooks/user.sh'), 'utf8'); + assert.match(userBody, /USER_HOOK_BODY_UNCHANGED/u); +}); + +test('fixture contract: invalid final mixed target has no earlier generated files', () => { + const base = path.join(__dirname, 'fixtures/context-generation/hooks/invalid'); + const copilot = JSON.parse(fs.readFileSync(path.join(base, '.github/hooks/aidd.json'), 'utf8')); + assert.equal(typeof copilot.hooks.SessionStart, 'string'); + assert.equal(fs.existsSync(path.join(base, '.claude/settings.json')), false); + assert.equal(fs.existsSync(path.join(base, 'hooks/aidd.sh')), false); + assert.equal(fs.existsSync(path.join(base, '.kilo/hooks.json')), false); +}); diff --git a/scripts/__tests__/context-kilo-detection.test.js b/scripts/__tests__/context-kilo-detection.test.js new file mode 100644 index 000000000..20713f4cb --- /dev/null +++ b/scripts/__tests__/context-kilo-detection.test.js @@ -0,0 +1,169 @@ +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const path = require("node:path"); +const test = require("node:test"); + +// These checks read the Markdown contracts that ship to the model. The small +// interpreter below is a test oracle for explicit path alternatives, not a +// production detector or proof that a model follows the contract at runtime. +const ROOT = path.resolve(__dirname, "../.."); +const FIXTURES = path.join(__dirname, "fixtures/context-generation/detection"); +const CONTRACTS = [ + "plugins/aidd-context/skills/00-onboard/references/state/detection.md", + "plugins/aidd-context/skills/02-project-memory/references/tools.md", +]; +const GENERATORS = [ + { + path: "plugins/aidd-context/skills/05-rule-generate", + reference: "rule-publication.md", + kind: "rule", + }, + { + path: "plugins/aidd-context/skills/08-hook-generate", + reference: "tool-paths.md", + kind: "hook", + }, +]; +const SIGNALS = [ + ".kilo/", + "kilo.json", + "kilo.jsonc", + ".kilo/kilo.json", + ".kilo/kilo.jsonc", + ".kilocode/", +]; + +// Retain the legitimate source reference for the historical fallback signal: +// https://github.com/Kilo-Org/kilocode/blob/main/packages/opencode/src/kilocode/skills/kilo-config.md + +function row(text, tool) { + const line = text.split("\n").find((candidate) => + new RegExp(`^\\|\\s*${tool}\\s*\\|`, "u").test(candidate) + ); + assert.ok(line, `Missing ${tool} detection row`); + const [, , detection, destination] = line.split("|"); + return { + signals: [...detection.matchAll(/`([^`]+)`/gu)].map((match) => match[1]), + destination: destination.trim().replaceAll("`", ""), + }; +} + +function hasSignal(project, signal) { + const target = path.join(project, signal); + if (!fs.existsSync(target)) return false; + return signal.endsWith("/") ? fs.statSync(target).isDirectory() : fs.statSync(target).isFile(); +} + +const scenarios = JSON.parse(fs.readFileSync(path.join(FIXTURES, "cases.json"), "utf8")); + +for (const contract of CONTRACTS) { + const text = fs.readFileSync(path.join(ROOT, contract), "utf8"); + + test(`${contract}: six distinct Kilo signals, one shared memory destination`, () => { + const kilo = row(text, "kilo"); + assert.deepEqual([...kilo.signals].sort(), [...SIGNALS].sort()); + assert.equal(kilo.destination, "AGENTS.md"); + for (const tool of ["codex", "cursor", "opencode"]) { + assert.equal(row(text, tool).destination, kilo.destination); + } + assert.deepEqual(row(text, "opencode").signals, [".opencode/"]); + }); + + for (const scenario of scenarios) { + test(`${contract}: ${scenario.name}`, () => { + const project = path.join(FIXTURES, scenario.project); + const kilo = row(text, "kilo"); + // A nested configuration also necessarily creates .kilo/. The separate + // exact signal assertion above catches omission of either nested format. + assert.equal(kilo.signals.some((signal) => hasSignal(project, signal)), scenario.kilo); + if (scenario.signals) { + for (const signal of scenario.signals) assert.ok(hasSignal(project, signal), signal); + } + if (scenario.tools) { + for (const tool of scenario.tools) { + assert.ok(row(text, tool).signals.some((signal) => hasSignal(project, signal)), tool); + } + assert.equal(new Set(scenario.tools.map((tool) => row(text, tool).destination)).size, 1); + } + }); + } + + test(`${contract}: legacy is detection only and OpenCode config is not a Kilo signal`, () => { + assert.match(text, /\.kilocode\/[^\n]*(?:legacy|historical)[^\n]*detection/iu); + if (contract.includes('02-project-memory')) { + assert.match(text, /\.kilocode\/[^\n]*detection signal only; selected Kilo memory goes to the shared root `AGENTS\.md`/iu); + assert.match(text, /Neither `opencode\.json` nor `opencode\.jsonc` identifies Kilo/u); + } else { + assert.match(text, /Treat `\.kilocode\/` as a legacy Kilo detection signal, never an output path/u); + assert.match(text, /OpenCode JSON config files do not identify Kilo/u); + } + }); +} + +for (const generator of GENERATORS) { + const action = path.join(ROOT, generator.path, `actions/01-capture-${generator.kind}.md`); + const reference = path.join(ROOT, generator.path, "references", generator.reference); + + test(`${generator.path}: capture action loads its own publication reference`, () => { + const actionText = fs.readFileSync(action, "utf8"); + const links = [...actionText.matchAll(/\[[^\]]+\]\(([^)]+)\)/gu)]; + assert.ok(links.length > 0, "capture action must link its publication reference"); + assert.ok(links.some(([, relative]) => path.resolve(path.dirname(action), relative) === reference)); + const text = fs.readFileSync(reference, "utf8"); + if (generator.kind === "hook") { + assert.match(text, /## Host detection\n\n([^\n]+)\n/u); + assert.doesNotMatch(text.match(/## Host detection\n\n([^\n]+)\n/u)[1], /\]\([^)]*\.\.\/[^)]*\)/u); + } else { + assert.match(text, /## Publications\n/u); + assert.match(text, /## Kilo configuration\n/u); + } + }); + + const text = fs.readFileSync(reference, "utf8"); + const hookGuidance = generator.kind === "hook" + ? text.match(/## Host detection\n\n([^\n]+)\n/u)?.[1] + : undefined; + const kiloSection = generator.kind === "rule" + ? text.match(/## Kilo configuration\n([\s\S]*?)(?=\n## |$)/u)?.[1] + : undefined; + const detectionContract = generator.kind === "hook" ? hookGuidance : kiloSection; + + test(`${generator.path}: local guidance covers Kilo signals and exclusions`, () => { + assert.ok(detectionContract, "missing local Kilo detection guidance"); + for (const signal of SIGNALS) assert.ok(detectionContract.includes(`\`${signal}\``), signal); + + if (generator.kind === "hook") { + assert.match(hookGuidance, /`AGENTS\.md`, `opencode\.json`, or `opencode\.jsonc` alone do not identify Kilo/u); + assert.match(hookGuidance, /legacy `\.kilocode\/`.*detection signal only/u); + assert.match(hookGuidance, /new Kilo [^.]*under `\.kilo\/`/u); + return; + } + + assert.match(kiloSection, /Root `AGENTS\.md` and OpenCode configuration alone do not identify Kilo/u); + assert.match(kiloSection, /legacy directory is a detection signal only; write new rules under `\.kilo\/`/u); + }); + + for (const scenario of scenarios) { + test(`${generator.path}: ${scenario.name}`, () => { + const project = path.join(FIXTURES, scenario.project); + const detected = SIGNALS.some((signal) => detectionContract.includes(`\`${signal}\``) && hasSignal(project, signal)); + assert.equal(detected, scenario.kilo); + }); + } + + if (generator.kind === "rule") { + test(`${generator.path}: publication target and Kilo configuration choices remain explicit`, () => { + assert.ok(text.includes("| Kilo / `kilo` | `.kilo/rules//.md` plus an exact `instructions` entry in the selected `kilo.json[c]` |")); + for (const config of [".kilo/kilo.json", ".kilo/kilo.jsonc", "kilo.json", "kilo.jsonc"]) { + assert.ok(kiloSection.includes(`\`${config}\``), config); + } + assert.match(kiloSection, /`\.kilo\/` takes precedence over root; do not assume an ordering between JSON and JSONC at the same level/u); + assert.match(kiloSection, /With no config, the writer creates `\.kilo\/kilo\.jsonc`/u); + assert.match(kiloSection, /With one config, it uses that file/u); + assert.match(kiloSection, /With several, it reuses the unique owner of the exact rule path; otherwise present the candidates and require the user to select one with `--kilo-config`/u); + assert.match(kiloSection, /Invalid or unsafe candidates refuse publication before writing/u); + assert.match(kiloSection, /preserves JSONC comments, entry order, duplicates and unrelated values/u); + assert.match(kiloSection, /restores completed replacements after covered errors/u); + }); + } +} diff --git a/scripts/__tests__/context-project-memory-sync.test.js b/scripts/__tests__/context-project-memory-sync.test.js new file mode 100644 index 000000000..20656db20 --- /dev/null +++ b/scripts/__tests__/context-project-memory-sync.test.js @@ -0,0 +1,27 @@ +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const test = require('node:test'); + +const ROOT = path.resolve(__dirname, '../..'); +const read = (file) => fs.readFileSync(path.join(ROOT, file), 'utf8'); + +test('project-memory sync uses the manual contract without a project-relative hook invocation', () => { + const sync = read('plugins/aidd-context/skills/02-project-memory/actions/04-sync.md'); + assert.doesNotMatch(sync, /(?:node\s+)?hooks\/update_memory\.js/u); + assert.match(sync, /deduplicat.*destination/iu); + assert.match(sync, /README.*before any (?:creation|write|mutation)/iu); + assert.match(sync, /reinspect|recheck/iu); + assert.match(sync, /internal\/.*external\//iu); + assert.match(sync, /README.*`files` block/iu); + assert.match(sync, /hardlink|multiple hard links/iu); + assert.match(sync, /manual workflow cannot provide hook-level path safety/u); + assert.match(sync, /Do not stage|Do not add.*Git/iu); +}); + +test('Kilo README agrees with the supported paths and AIDD output boundary', () => { + const readme = read('plugins/aidd-context/README.md'); + assert.match(readme, /writes no Kilo plugin or hook/u); + assert.match(readme, /See \[hook paths\]/u); + assert.doesNotMatch(readme, /#\d{3,}|2026-09-25|2026-10-10|https:\/\/kilo\.ai/u); +}); diff --git a/scripts/__tests__/context-skill-artifacts.test.js b/scripts/__tests__/context-skill-artifacts.test.js new file mode 100644 index 000000000..68e0abdbc --- /dev/null +++ b/scripts/__tests__/context-skill-artifacts.test.js @@ -0,0 +1,123 @@ +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const test = require('node:test'); +const yaml = require('js-yaml'); + +const ROOT = path.resolve(__dirname, '../..'); +const SKILL = 'plugins/aidd-context/skills/04-skill-generate'; +const read = (file) => fs.readFileSync(path.join(ROOT, SKILL, file), 'utf8'); +const normalizeEol = (text) => text.replace(/\r\n/gu, '\n'); + +// These checks validate documented contracts; they do not execute a skill writer. +test('static contract: skill detection lists the six Kilo signals and keeps OpenCode distinct', () => { + const detection = read('references/tool-detect.md'); + const row = detection.split('\n').find((line) => /^\| Kilo\s*\|/u.test(line)); + assert.ok(row, 'Missing Kilo detection row'); + assert.deepEqual([...row.matchAll(/`([^`]+)`/gu)].map((m) => m[1]), + ['.kilo/', 'kilo.json', 'kilo.jsonc', '.kilo/kilo.json', '.kilo/kilo.jsonc', '.kilocode/']); + assert.match(detection, /\| OpenCode\s*\| `\.opencode\/`/u); + assert.match(detection, /(?:AGENTS\.md|opencode\.json)[^\n]*not[^\n]*Kilo/u); +}); + +test('static contract: Kilo placement requires portable consent and resolves duplicates before writing', () => { + const placement = read('references/tool-write.md'); + assert.match(placement, /\.kilo\/skills\/\//u); + assert.match(placement, /explicit[^\n]*\.agents\/skills/iu); + assert.match(placement, /never[^\n]*both/iu); + assert.match(placement, /same-name skills[^\n]*resolution before writing/u); + assert.match(placement, /deduplicate identical physical destinations/u); + assert.match(placement, /Kilo native[^\n]*another selected host[^\n]*resolve placement before writing/u); + assert.match(read('actions/01-scope.md'), /tool-write\.md/u); + assert.match(read('references/scope-frame.md'), /placement/u); +}); + +test('static contract: Kilo fields stay target-specific without weakening the Claude template', () => { + const placement = read('references/tool-write.md'); + assert.match(placement, /\| Kilo[^\n]*`name`[^\n]*`description`/u); + assert.match(placement, /name[^\n]*parent directory/u); + assert.match(placement, /argument-hint[^\n]*Kilo/u); + assert.match(placement, /\*\*Canonical-to-host rendering:\*\* Author the canonical skill according to AIDD conventions, including R4/u); + assert.match(placement, /Omit unsupported canonical-only fields; explicitly resolve unsupported user-requested fields before writing/u); + assert.match(read('assets/skill-template.md'), /argument-hint:/u); + assert.match(read('references/skill-authoring.md'), /\*\*R4\.\*\*/u); + assert.doesNotMatch(read('references/skill-authoring.md'), /generated host-specific artifact|Canonical-to-host rendering/u); +}); + +test('static contract: full preflight precedes creation and covers unsafe paths and user resources', () => { + const write = read('actions/03-write.md'); + const preflight = write.indexOf('**Preflight.**'); + const tree = write.indexOf('**Tree.**'); + assert.ok(preflight >= 0 && tree > preflight, 'Preflight must exist before Tree'); + const placement = read('references/tool-write.md'); + for (const invariant of ['symlink', 'writable', 'reference', 'collision', 'unchanged', 'mtime']) { + assert.ok(placement.includes(invariant), `Missing ${invariant}`); + } + assert.match(read('actions/04-validate.md'), /tool-write\.md/u); +}); + +const FIXTURES = path.join(__dirname, 'fixtures/context-generation/skills'); +// Golden artifacts are produced by the native caller using the candidate skill, +// not by a test-only renderer. Absent evidence is a failure, never a skipped pass. +test('fixture contract: generated target trees have valid frontmatter, links and preserved resources', () => { + const cases = JSON.parse(fs.readFileSync(path.join(FIXTURES, 'cases.json'), 'utf8')); + for (const fixture of cases) { + const dir = path.join(FIXTURES, fixture.project, fixture.target, 'verify-payload'); + const text = normalizeEol(fs.readFileSync(path.join(dir, 'SKILL.md'), 'utf8')); + const match = text.match(/^---\n([\s\S]*?)\n---\n/u); + assert.ok(match, fixture.project); + const fields = yaml.load(match[1]); + assert.deepEqual(Object.keys(fields).sort(), fixture.fields.sort()); + assert.equal(fields.description, 'Verify a bundled payload. Use when the user wants to verify the phase three fixture.'); + if (fixture.fields.includes('name')) assert.equal(fields.name, path.basename(dir)); + assert.match(text, /flowchart/u); + assert.match(text, /\| Action\s*\| Does/u); + for (const link of text.matchAll(/\]\(([^)]+)\)/gu)) { + const resolved = path.resolve(dir, link[1]); + assert.ok(resolved.startsWith(`${dir}${path.sep}`)); + assert.ok(fs.existsSync(resolved), link[1]); + } + assert.ok(fs.readFileSync(path.join(dir, 'actions/01-verify.md'), 'utf8').includes('assets/payload.txt')); + assert.equal(normalizeEol(fs.readFileSync(path.join(dir, 'assets/user.txt'), 'utf8')), 'USER_RESOURCE_DO_NOT_REPLACE\n'); + for (const absent of fixture.absent) assert.ok(!fs.existsSync(path.join(FIXTURES, fixture.project, absent)), absent); + } +}); + +const detectionFixtures = path.join(__dirname, 'fixtures/context-generation/detection'); +for (const fixture of JSON.parse(fs.readFileSync(path.join(detectionFixtures, 'cases.json'), 'utf8'))) { + test(`static contract: Kilo detection oracle recognizes ${fixture.name}`, () => { + const row = read('references/tool-detect.md').split('\n').find((line) => /^\| Kilo\s*\|/u.test(line)); + assert.ok(row); + const signals = [...row.matchAll(/`([^`]+)`/gu)].map((m) => m[1]); + const project = path.join(detectionFixtures, fixture.project); + assert.equal(signals.some((signal) => { + const p = path.join(project, signal); + return fs.existsSync(p) && (signal.endsWith('/') ? fs.statSync(p).isDirectory() : fs.statSync(p).isFile()); + }), fixture.kilo); + }); +} + +test('fixture contract: native and portable trees each preserve the user resource once', () => { + for (const project of ['native', 'portable']) { + const fixture = JSON.parse(fs.readFileSync(path.join(FIXTURES, 'cases.json'), 'utf8')) + .find((item) => item.project === project); + assert.ok(fixture); + const dir = path.join(FIXTURES, project, fixture.target, 'verify-payload'); + assert.equal(fs.readFileSync(path.join(dir, 'assets/user.txt'), 'utf8'), 'USER_RESOURCE_DO_NOT_REPLACE\n'); + assert.ok(fs.existsSync(path.join(dir, 'actions/01-verify.md'))); + assert.ok(fs.existsSync(path.join(dir, 'assets/payload.txt'))); + } +}); + +test('static contract: shared portable fields require resolution when Kilo fields conflict with Codex', () => { + const contract = read('references/tool-write.md'); + assert.match(contract, /hosts share a destination[^\n]*fields supported by all/u); + assert.match(contract, /user-requested field is unsupported[^\n]*explicit target or field resolution/u); + assert.match(contract, /never[^\n]*silently discard a requested field/u); +}); + +test('static contract: mixed Kilo-native and portable targets resolve placement before writing', () => { + const contract = read('references/tool-write.md'); + assert.match(contract, /Kilo native[^\n]*another selected host[^\n]*resolve placement before writing/u); + assert.match(contract, /never create both trees/u); +}); diff --git a/scripts/__tests__/fixtures/context-generation/agents-commands/native/.kilo/agents/assets/agent-payload.txt b/scripts/__tests__/fixtures/context-generation/agents-commands/native/.kilo/agents/assets/agent-payload.txt new file mode 100644 index 000000000..446b5d015 --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/agents-commands/native/.kilo/agents/assets/agent-payload.txt @@ -0,0 +1 @@ +AGENT_APPLIED:maple-4820 diff --git a/scripts/__tests__/fixtures/context-generation/agents-commands/native/.kilo/agents/verify-agent.md b/scripts/__tests__/fixtures/context-generation/agents-commands/native/.kilo/agents/verify-agent.md new file mode 100644 index 000000000..c6a1fdf65 --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/agents-commands/native/.kilo/agents/verify-agent.md @@ -0,0 +1,16 @@ +--- +description: Verify the phase four agent payload. +mode: subagent +temperature: 0 +permission: + read: allow +--- + +# Role + +Verify the bundled phase four agent payload. + +# Behavior + +1. Read `.kilo/agents/assets/agent-payload.txt`. +2. Return only its exact content. diff --git a/scripts/__tests__/fixtures/context-generation/agents-commands/native/.kilo/commands/assets/workflow-payload.txt b/scripts/__tests__/fixtures/context-generation/agents-commands/native/.kilo/commands/assets/workflow-payload.txt new file mode 100644 index 000000000..bb790c839 --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/agents-commands/native/.kilo/commands/assets/workflow-payload.txt @@ -0,0 +1 @@ +WORKFLOW_APPLIED:birch-9173 diff --git a/scripts/__tests__/fixtures/context-generation/agents-commands/native/.kilo/commands/verify-workflow.md b/scripts/__tests__/fixtures/context-generation/agents-commands/native/.kilo/commands/verify-workflow.md new file mode 100644 index 000000000..e436966c3 --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/agents-commands/native/.kilo/commands/verify-workflow.md @@ -0,0 +1,8 @@ +--- +description: Verify the phase four workflow payload. +agent: verify-agent +variant: minimal +subtask: true +--- + +Read `.kilo/commands/assets/workflow-payload.txt` and return only its exact content. diff --git a/scripts/__tests__/fixtures/context-generation/artifact-manifest.json b/scripts/__tests__/fixtures/context-generation/artifact-manifest.json new file mode 100644 index 000000000..ee8c42482 --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/artifact-manifest.json @@ -0,0 +1,14 @@ +{ + "skills/native/.kilo/skills/verify-payload/SKILL.md": "ebab5bcc394e82d96a3d128872adb909fcedbeb0b29ef40ac45c16798afa7aff", + "skills/native/.kilo/skills/verify-payload/actions/01-verify.md": "60a386a114507203fd59706de5ba0b9ca541cd30b0a0b074e16c8289b25d6c68", + "skills/native/.kilo/skills/verify-payload/actions/02-user.md": "126a3488d7e981c0fe9ac0211bbebceb6799ec14e03bc02d2acb2e6d44b3ef95", + "skills/native/.kilo/skills/verify-payload/assets/payload.txt": "7c9e202409a70393756a0ab9c0203c49f8d2c3862ab787b7d2879b3dc80e12b8", + "skills/native/.kilo/skills/verify-payload/assets/user.txt": "2f80347010a4451aed834ff0923f837e069fd5f5f3bf79d48c28fe060a0a5f59", + "agents-commands/native/.kilo/agents/verify-agent.md": "56c2594c982078fd9ee1d5d97876f7aca64c0d477efd28e3601469097a812559", + "agents-commands/native/.kilo/agents/assets/agent-payload.txt": "9c3390da19f19eb9b6a2c18d39507968645b9f72a39994fdd3106fae88e896d2", + "agents-commands/native/.kilo/commands/verify-workflow.md": "3fe09491fc749556091c8cb3c522d10569299e6b972bbe819a701d54cbf2fb41", + "agents-commands/native/.kilo/commands/assets/workflow-payload.txt": "0a78981384c13c40dacca78620f032effb4e4ae4ed32793e8f840174ae7a9029", + "kilo-rule/rule.md": "c90ac8d4cff0885f2db86970448694d866963dc6171cd11951085a51c68203c0", + "kilo-rule/positive-config.jsonc": "a4255bc07e3b98e49e1274e0c1cc798e1de19e3f101147d672333efddd2ad16d", + "kilo-rule/negative-config.jsonc": "7c241f295ed17c6992ce2e672b75d318a89a5a02d8e00bf18e0bc457ee023811" +} diff --git a/scripts/__tests__/fixtures/context-generation/detection/agents-only/AGENTS.md b/scripts/__tests__/fixtures/context-generation/detection/agents-only/AGENTS.md new file mode 100644 index 000000000..d9fa80fe6 --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/detection/agents-only/AGENTS.md @@ -0,0 +1,3 @@ +# User instructions + +This shared context file does not identify a tool. diff --git a/scripts/__tests__/fixtures/context-generation/detection/canonical/.kilo/.gitkeep b/scripts/__tests__/fixtures/context-generation/detection/canonical/.kilo/.gitkeep new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/detection/canonical/.kilo/.gitkeep @@ -0,0 +1 @@ + diff --git a/scripts/__tests__/fixtures/context-generation/detection/cases.json b/scripts/__tests__/fixtures/context-generation/detection/cases.json new file mode 100644 index 000000000..d39c3ccc5 --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/detection/cases.json @@ -0,0 +1,12 @@ +[ + { "name": "canonical directory", "project": "canonical", "kilo": true, "signals": [".kilo/"] }, + { "name": "legacy directory", "project": "legacy", "kilo": true, "signals": [".kilocode/"] }, + { "name": "root JSON", "project": "root-json", "kilo": true, "signals": ["kilo.json"] }, + { "name": "root JSONC", "project": "root-jsonc", "kilo": true, "signals": ["kilo.jsonc"] }, + { "name": "nested JSON", "project": "nested-json", "kilo": true, "signals": [".kilo/kilo.json"] }, + { "name": "nested JSONC", "project": "nested-jsonc", "kilo": true, "signals": [".kilo/kilo.jsonc"] }, + { "name": "shared AGENTS alone", "project": "agents-only", "kilo": false }, + { "name": "OpenCode JSON alone", "project": "opencode-json", "kilo": false }, + { "name": "OpenCode JSONC alone", "project": "opencode-jsonc", "kilo": false }, + { "name": "multiple tools share memory", "project": "multi-tool", "kilo": true, "tools": ["kilo", "codex", "opencode", "cursor"] } +] diff --git a/scripts/__tests__/fixtures/context-generation/detection/legacy/.kilocode/.gitkeep b/scripts/__tests__/fixtures/context-generation/detection/legacy/.kilocode/.gitkeep new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/detection/legacy/.kilocode/.gitkeep @@ -0,0 +1 @@ + diff --git a/scripts/__tests__/fixtures/context-generation/detection/multi-tool/.codex/.gitkeep b/scripts/__tests__/fixtures/context-generation/detection/multi-tool/.codex/.gitkeep new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/detection/multi-tool/.codex/.gitkeep @@ -0,0 +1 @@ + diff --git a/scripts/__tests__/fixtures/context-generation/detection/multi-tool/.cursor/.gitkeep b/scripts/__tests__/fixtures/context-generation/detection/multi-tool/.cursor/.gitkeep new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/detection/multi-tool/.cursor/.gitkeep @@ -0,0 +1 @@ + diff --git a/scripts/__tests__/fixtures/context-generation/detection/multi-tool/.kilo/.gitkeep b/scripts/__tests__/fixtures/context-generation/detection/multi-tool/.kilo/.gitkeep new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/detection/multi-tool/.kilo/.gitkeep @@ -0,0 +1 @@ + diff --git a/scripts/__tests__/fixtures/context-generation/detection/multi-tool/.opencode/.gitkeep b/scripts/__tests__/fixtures/context-generation/detection/multi-tool/.opencode/.gitkeep new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/detection/multi-tool/.opencode/.gitkeep @@ -0,0 +1 @@ + diff --git a/scripts/__tests__/fixtures/context-generation/detection/multi-tool/AGENTS.md b/scripts/__tests__/fixtures/context-generation/detection/multi-tool/AGENTS.md new file mode 100644 index 000000000..0c9ee9552 --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/detection/multi-tool/AGENTS.md @@ -0,0 +1,3 @@ +# User instructions + +Preserve this shared context for every selected tool. diff --git a/scripts/__tests__/fixtures/context-generation/detection/nested-json/.kilo/kilo.json b/scripts/__tests__/fixtures/context-generation/detection/nested-json/.kilo/kilo.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/detection/nested-json/.kilo/kilo.json @@ -0,0 +1 @@ +{} diff --git a/scripts/__tests__/fixtures/context-generation/detection/nested-jsonc/.kilo/kilo.jsonc b/scripts/__tests__/fixtures/context-generation/detection/nested-jsonc/.kilo/kilo.jsonc new file mode 100644 index 000000000..ea8cdc78b --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/detection/nested-jsonc/.kilo/kilo.jsonc @@ -0,0 +1,3 @@ +{ + // Existing user config; detection must not rewrite it. +} diff --git a/scripts/__tests__/fixtures/context-generation/detection/opencode-json/opencode.json b/scripts/__tests__/fixtures/context-generation/detection/opencode-json/opencode.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/detection/opencode-json/opencode.json @@ -0,0 +1 @@ +{} diff --git a/scripts/__tests__/fixtures/context-generation/detection/opencode-jsonc/opencode.jsonc b/scripts/__tests__/fixtures/context-generation/detection/opencode-jsonc/opencode.jsonc new file mode 100644 index 000000000..1198278b2 --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/detection/opencode-jsonc/opencode.jsonc @@ -0,0 +1,3 @@ +{ + // An OpenCode configuration does not identify Kilo. +} diff --git a/scripts/__tests__/fixtures/context-generation/detection/root-json/kilo.json b/scripts/__tests__/fixtures/context-generation/detection/root-json/kilo.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/detection/root-json/kilo.json @@ -0,0 +1 @@ +{} diff --git a/scripts/__tests__/fixtures/context-generation/detection/root-jsonc/kilo.jsonc b/scripts/__tests__/fixtures/context-generation/detection/root-jsonc/kilo.jsonc new file mode 100644 index 000000000..ea8cdc78b --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/detection/root-jsonc/kilo.jsonc @@ -0,0 +1,3 @@ +{ + // Existing user config; detection must not rewrite it. +} diff --git a/scripts/__tests__/fixtures/context-generation/hooks/invalid/.github/hooks/aidd.json b/scripts/__tests__/fixtures/context-generation/hooks/invalid/.github/hooks/aidd.json new file mode 100644 index 000000000..fd47189f2 --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/hooks/invalid/.github/hooks/aidd.json @@ -0,0 +1 @@ +{"version":1,"hooks":{"SessionStart":"INVALID"}} diff --git a/scripts/__tests__/fixtures/context-generation/hooks/invalid/kilo.json b/scripts/__tests__/fixtures/context-generation/hooks/invalid/kilo.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/hooks/invalid/kilo.json @@ -0,0 +1 @@ +{} diff --git a/scripts/__tests__/fixtures/context-generation/hooks/mixed/.claude/settings.json b/scripts/__tests__/fixtures/context-generation/hooks/mixed/.claude/settings.json new file mode 100644 index 000000000..5b78b404c --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/hooks/mixed/.claude/settings.json @@ -0,0 +1,9 @@ +{ + "hooks": { + "SessionStart": [ + { "hooks": [{ "type": "command", "command": "/project/hooks/user.sh" }] }, + { "hooks": [{ "type": "command", "command": "/project/hooks/user.sh" }] }, + { "hooks": [{ "type": "command", "command": "/project/hooks/aidd.sh" }] } + ] + } +} diff --git a/scripts/__tests__/fixtures/context-generation/hooks/mixed/.codex/hooks.json b/scripts/__tests__/fixtures/context-generation/hooks/mixed/.codex/hooks.json new file mode 100644 index 000000000..5b78b404c --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/hooks/mixed/.codex/hooks.json @@ -0,0 +1,9 @@ +{ + "hooks": { + "SessionStart": [ + { "hooks": [{ "type": "command", "command": "/project/hooks/user.sh" }] }, + { "hooks": [{ "type": "command", "command": "/project/hooks/user.sh" }] }, + { "hooks": [{ "type": "command", "command": "/project/hooks/aidd.sh" }] } + ] + } +} diff --git a/scripts/__tests__/fixtures/context-generation/hooks/mixed/.cursor/hooks.json b/scripts/__tests__/fixtures/context-generation/hooks/mixed/.cursor/hooks.json new file mode 100644 index 000000000..6ea404956 --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/hooks/mixed/.cursor/hooks.json @@ -0,0 +1,10 @@ +{ + "version": 1, + "hooks": { + "sessionStart": [ + { "command": "/project/hooks/user.sh" }, + { "command": "/project/hooks/user.sh" }, + { "command": "/project/hooks/aidd.sh" } + ] + } +} diff --git a/scripts/__tests__/fixtures/context-generation/hooks/mixed/.github/hooks/aidd.json b/scripts/__tests__/fixtures/context-generation/hooks/mixed/.github/hooks/aidd.json new file mode 100644 index 000000000..f404c1096 --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/hooks/mixed/.github/hooks/aidd.json @@ -0,0 +1,10 @@ +{ + "version": 1, + "hooks": { + "SessionStart": [ + { "type": "command", "command": "/project/hooks/user.sh" }, + { "type": "command", "command": "/project/hooks/user.sh" }, + { "type": "command", "command": "/project/hooks/aidd.sh" } + ] + } +} diff --git a/scripts/__tests__/fixtures/context-generation/hooks/mixed/hooks/aidd.sh b/scripts/__tests__/fixtures/context-generation/hooks/mixed/hooks/aidd.sh new file mode 100644 index 000000000..09ac2ac01 --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/hooks/mixed/hooks/aidd.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +cat >/dev/null +exit 0 diff --git a/scripts/__tests__/fixtures/context-generation/hooks/mixed/hooks/user.sh b/scripts/__tests__/fixtures/context-generation/hooks/mixed/hooks/user.sh new file mode 100644 index 000000000..6652bca8d --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/hooks/mixed/hooks/user.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +cat >/dev/null +printf '%s\n' 'USER_HOOK_BODY_UNCHANGED' diff --git a/scripts/__tests__/fixtures/context-generation/hooks/mixed/kilo.json b/scripts/__tests__/fixtures/context-generation/hooks/mixed/kilo.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/hooks/mixed/kilo.json @@ -0,0 +1 @@ +{} diff --git a/scripts/__tests__/fixtures/context-generation/kilo-config/comments-and-trailing.jsonc b/scripts/__tests__/fixtures/context-generation/kilo-config/comments-and-trailing.jsonc new file mode 100644 index 000000000..c2f5eb362 --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/kilo-config/comments-and-trailing.jsonc @@ -0,0 +1,9 @@ +{ + // Keep this URL and its comment exactly. + "endpoint": "https://example.test/a//b", + "instructions": [ + "docs/first.md", // existing first entry + "docs/first.md", + ], + "nested": { "instructions": [".kilo/rules/01-standards/1-naming.md"] }, +} diff --git a/scripts/__tests__/fixtures/context-generation/kilo-config/escaped-key.jsonc b/scripts/__tests__/fixtures/context-generation/kilo-config/escaped-key.jsonc new file mode 100644 index 000000000..e93fcf78f --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/kilo-config/escaped-key.jsonc @@ -0,0 +1,4 @@ +{ + "instruct\u0069ons": ["docs/old.md"], + "unrelated": "keep { braces } and // text" +} diff --git a/scripts/__tests__/fixtures/context-generation/kilo-config/invalid-duplicate-key.jsonc b/scripts/__tests__/fixtures/context-generation/kilo-config/invalid-duplicate-key.jsonc new file mode 100644 index 000000000..fc03849bf --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/kilo-config/invalid-duplicate-key.jsonc @@ -0,0 +1,4 @@ +{ + "instructions": ["docs/first.md"], + "instruct\u0069ons": ["docs/second.md"] +} diff --git a/scripts/__tests__/fixtures/context-generation/kilo-config/invalid-element.jsonc b/scripts/__tests__/fixtures/context-generation/kilo-config/invalid-element.jsonc new file mode 100644 index 000000000..54471e65d --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/kilo-config/invalid-element.jsonc @@ -0,0 +1,3 @@ +{ + "instructions": ["docs/first.md", 42] +} diff --git a/scripts/__tests__/fixtures/context-generation/kilo-config/invalid-unclosed.jsonc b/scripts/__tests__/fixtures/context-generation/kilo-config/invalid-unclosed.jsonc new file mode 100644 index 000000000..31edd3b85 --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/kilo-config/invalid-unclosed.jsonc @@ -0,0 +1,2 @@ +{ + "instructions": ["docs/first.md" diff --git a/scripts/__tests__/fixtures/context-generation/kilo-rule/negative-config.jsonc b/scripts/__tests__/fixtures/context-generation/kilo-rule/negative-config.jsonc new file mode 100644 index 000000000..80d226e23 --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/kilo-rule/negative-config.jsonc @@ -0,0 +1,3 @@ +{ + "instructions": [], +} diff --git a/scripts/__tests__/fixtures/context-generation/kilo-rule/positive-config.jsonc b/scripts/__tests__/fixtures/context-generation/kilo-rule/positive-config.jsonc new file mode 100644 index 000000000..9a7899552 --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/kilo-rule/positive-config.jsonc @@ -0,0 +1,5 @@ +{ + "instructions": [ + ".kilo/rules/01-standards/1-naming.md", + ], +} diff --git a/scripts/__tests__/fixtures/context-generation/kilo-rule/rule.md b/scripts/__tests__/fixtures/context-generation/kilo-rule/rule.md new file mode 100644 index 000000000..8cb07fb61 --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/kilo-rule/rule.md @@ -0,0 +1,3 @@ +# Naming fixture + +KILO_RULE_FIXTURE_MARKER diff --git a/scripts/__tests__/fixtures/context-generation/skills/cases.json b/scripts/__tests__/fixtures/context-generation/skills/cases.json new file mode 100644 index 000000000..6c36b83ac --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/skills/cases.json @@ -0,0 +1,65 @@ +[ + { + "project": "native", + "target": ".kilo/skills", + "fields": [ + "name", + "description", + "license", + "compatibility", + "metadata" + ], + "absent": [ + ".agents/skills" + ] + }, + { + "project": "portable", + "target": ".agents/skills", + "fields": [ + "name", + "description" + ], + "absent": [ + ".kilo/skills" + ] + }, + { + "project": "legacy", + "target": ".kilo/skills", + "fields": [ + "name", + "description" + ], + "absent": [ + ".agents/skills", + ".kilocode/skills" + ] + }, + { + "project": "claude", + "target": ".claude/skills", + "fields": [ + "name", + "description", + "allowed-tools", + "disable-model-invocation" + ], + "absent": [ + ".kilo/skills", + ".agents/skills" + ] + }, + { + "project": "opencode", + "target": ".opencode/skills", + "fields": [ + "description", + "permission" + ], + "absent": [ + ".kilo/skills", + ".agents/skills" + ] + } +] diff --git a/scripts/__tests__/fixtures/context-generation/skills/claude/.claude/skills/verify-payload/SKILL.md b/scripts/__tests__/fixtures/context-generation/skills/claude/.claude/skills/verify-payload/SKILL.md new file mode 100644 index 000000000..82bae8818 --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/skills/claude/.claude/skills/verify-payload/SKILL.md @@ -0,0 +1,26 @@ +--- +name: verify-payload +description: Verify a bundled payload. Use when the user wants to verify the phase three fixture. +allowed-tools: Read +disable-model-invocation: true +--- + +# Verify Payload + +```mermaid +flowchart LR + request([verification request]) --> verify --> answer([verification code]) +``` + +## Actions + +Run the flow above. Read only the next action file. + +| Action | Does | +| --- | --- | +| verify | verify the bundled payload | + +## Transversal rules + +- Read [01-verify.md](actions/01-verify.md) before answering. +- Leave project files unchanged. diff --git a/scripts/__tests__/fixtures/context-generation/skills/claude/.claude/skills/verify-payload/actions/01-verify.md b/scripts/__tests__/fixtures/context-generation/skills/claude/.claude/skills/verify-payload/actions/01-verify.md new file mode 100644 index 000000000..7ef37d89a --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/skills/claude/.claude/skills/verify-payload/actions/01-verify.md @@ -0,0 +1,22 @@ +# 01 - Verify + +Verify the bundled payload. + +## Input + +A request to verify the phase three fixture. + +## Output + +The verification code. + +## Process + +1. **Read.** Read [payload.txt](../assets/payload.txt) for this request; do not guess its content. +2. **Reply.** Return exactly `PAYLOAD_APPLIED:` followed by the trimmed payload, with no extra text. + +## Test + +| Case | Pass | +| --- | --- | +| A verification request is answered | the payload read is traced and the exact verification code is returned | diff --git a/scripts/__tests__/fixtures/context-generation/skills/claude/.claude/skills/verify-payload/actions/02-user.md b/scripts/__tests__/fixtures/context-generation/skills/claude/.claude/skills/verify-payload/actions/02-user.md new file mode 100644 index 000000000..33c809839 --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/skills/claude/.claude/skills/verify-payload/actions/02-user.md @@ -0,0 +1,3 @@ +# User action + +USER_ACTION_KEEP diff --git a/scripts/__tests__/fixtures/context-generation/skills/claude/.claude/skills/verify-payload/assets/payload.txt b/scripts/__tests__/fixtures/context-generation/skills/claude/.claude/skills/verify-payload/assets/payload.txt new file mode 100644 index 000000000..0bf66b5e6 --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/skills/claude/.claude/skills/verify-payload/assets/payload.txt @@ -0,0 +1 @@ +cedar-7391 diff --git a/scripts/__tests__/fixtures/context-generation/skills/claude/.claude/skills/verify-payload/assets/user.txt b/scripts/__tests__/fixtures/context-generation/skills/claude/.claude/skills/verify-payload/assets/user.txt new file mode 100644 index 000000000..f52c38830 --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/skills/claude/.claude/skills/verify-payload/assets/user.txt @@ -0,0 +1 @@ +USER_RESOURCE_DO_NOT_REPLACE diff --git a/scripts/__tests__/fixtures/context-generation/skills/legacy/.kilo/skills/verify-payload/SKILL.md b/scripts/__tests__/fixtures/context-generation/skills/legacy/.kilo/skills/verify-payload/SKILL.md new file mode 100644 index 000000000..f15f6f952 --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/skills/legacy/.kilo/skills/verify-payload/SKILL.md @@ -0,0 +1,24 @@ +--- +name: verify-payload +description: Verify a bundled payload. Use when the user wants to verify the phase three fixture. +--- + +# Verify Payload + +```mermaid +flowchart LR + request([verification request]) --> verify --> answer([verification code]) +``` + +## Actions + +Run the flow above. Read only the next action file. + +| Action | Does | +| --- | --- | +| verify | verify the bundled payload | + +## Transversal rules + +- Read [01-verify.md](actions/01-verify.md) before answering. +- Leave project files unchanged. diff --git a/scripts/__tests__/fixtures/context-generation/skills/legacy/.kilo/skills/verify-payload/actions/01-verify.md b/scripts/__tests__/fixtures/context-generation/skills/legacy/.kilo/skills/verify-payload/actions/01-verify.md new file mode 100644 index 000000000..7ef37d89a --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/skills/legacy/.kilo/skills/verify-payload/actions/01-verify.md @@ -0,0 +1,22 @@ +# 01 - Verify + +Verify the bundled payload. + +## Input + +A request to verify the phase three fixture. + +## Output + +The verification code. + +## Process + +1. **Read.** Read [payload.txt](../assets/payload.txt) for this request; do not guess its content. +2. **Reply.** Return exactly `PAYLOAD_APPLIED:` followed by the trimmed payload, with no extra text. + +## Test + +| Case | Pass | +| --- | --- | +| A verification request is answered | the payload read is traced and the exact verification code is returned | diff --git a/scripts/__tests__/fixtures/context-generation/skills/legacy/.kilo/skills/verify-payload/actions/02-user.md b/scripts/__tests__/fixtures/context-generation/skills/legacy/.kilo/skills/verify-payload/actions/02-user.md new file mode 100644 index 000000000..33c809839 --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/skills/legacy/.kilo/skills/verify-payload/actions/02-user.md @@ -0,0 +1,3 @@ +# User action + +USER_ACTION_KEEP diff --git a/scripts/__tests__/fixtures/context-generation/skills/legacy/.kilo/skills/verify-payload/assets/payload.txt b/scripts/__tests__/fixtures/context-generation/skills/legacy/.kilo/skills/verify-payload/assets/payload.txt new file mode 100644 index 000000000..0bf66b5e6 --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/skills/legacy/.kilo/skills/verify-payload/assets/payload.txt @@ -0,0 +1 @@ +cedar-7391 diff --git a/scripts/__tests__/fixtures/context-generation/skills/legacy/.kilo/skills/verify-payload/assets/user.txt b/scripts/__tests__/fixtures/context-generation/skills/legacy/.kilo/skills/verify-payload/assets/user.txt new file mode 100644 index 000000000..f52c38830 --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/skills/legacy/.kilo/skills/verify-payload/assets/user.txt @@ -0,0 +1 @@ +USER_RESOURCE_DO_NOT_REPLACE diff --git a/scripts/__tests__/fixtures/context-generation/skills/native/.kilo/skills/verify-payload/SKILL.md b/scripts/__tests__/fixtures/context-generation/skills/native/.kilo/skills/verify-payload/SKILL.md new file mode 100644 index 000000000..facccbb03 --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/skills/native/.kilo/skills/verify-payload/SKILL.md @@ -0,0 +1,28 @@ +--- +name: verify-payload +description: Verify a bundled payload. Use when the user wants to verify the phase three fixture. +license: MIT +compatibility: Requires a host that can read local files. +metadata: + fixture: "phase-three" +--- + +# Verify Payload + +```mermaid +flowchart LR + request([verification request]) --> verify --> answer([verification code]) +``` + +## Actions + +Run the flow above. Read only the next action file. + +| Action | Does | +| --- | --- | +| verify | verify the bundled payload | + +## Transversal rules + +- Read [01-verify.md](actions/01-verify.md) before answering. +- Leave project files unchanged. diff --git a/scripts/__tests__/fixtures/context-generation/skills/native/.kilo/skills/verify-payload/actions/01-verify.md b/scripts/__tests__/fixtures/context-generation/skills/native/.kilo/skills/verify-payload/actions/01-verify.md new file mode 100644 index 000000000..7ef37d89a --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/skills/native/.kilo/skills/verify-payload/actions/01-verify.md @@ -0,0 +1,22 @@ +# 01 - Verify + +Verify the bundled payload. + +## Input + +A request to verify the phase three fixture. + +## Output + +The verification code. + +## Process + +1. **Read.** Read [payload.txt](../assets/payload.txt) for this request; do not guess its content. +2. **Reply.** Return exactly `PAYLOAD_APPLIED:` followed by the trimmed payload, with no extra text. + +## Test + +| Case | Pass | +| --- | --- | +| A verification request is answered | the payload read is traced and the exact verification code is returned | diff --git a/scripts/__tests__/fixtures/context-generation/skills/native/.kilo/skills/verify-payload/actions/02-user.md b/scripts/__tests__/fixtures/context-generation/skills/native/.kilo/skills/verify-payload/actions/02-user.md new file mode 100644 index 000000000..33c809839 --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/skills/native/.kilo/skills/verify-payload/actions/02-user.md @@ -0,0 +1,3 @@ +# User action + +USER_ACTION_KEEP diff --git a/scripts/__tests__/fixtures/context-generation/skills/native/.kilo/skills/verify-payload/assets/payload.txt b/scripts/__tests__/fixtures/context-generation/skills/native/.kilo/skills/verify-payload/assets/payload.txt new file mode 100644 index 000000000..0bf66b5e6 --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/skills/native/.kilo/skills/verify-payload/assets/payload.txt @@ -0,0 +1 @@ +cedar-7391 diff --git a/scripts/__tests__/fixtures/context-generation/skills/native/.kilo/skills/verify-payload/assets/user.txt b/scripts/__tests__/fixtures/context-generation/skills/native/.kilo/skills/verify-payload/assets/user.txt new file mode 100644 index 000000000..f52c38830 --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/skills/native/.kilo/skills/verify-payload/assets/user.txt @@ -0,0 +1 @@ +USER_RESOURCE_DO_NOT_REPLACE diff --git a/scripts/__tests__/fixtures/context-generation/skills/opencode/.opencode/skills/verify-payload/SKILL.md b/scripts/__tests__/fixtures/context-generation/skills/opencode/.opencode/skills/verify-payload/SKILL.md new file mode 100644 index 000000000..3cf8bf24c --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/skills/opencode/.opencode/skills/verify-payload/SKILL.md @@ -0,0 +1,25 @@ +--- +description: Verify a bundled payload. Use when the user wants to verify the phase three fixture. +permission: + read: allow +--- + +# Verify Payload + +```mermaid +flowchart LR + request([verification request]) --> verify --> answer([verification code]) +``` + +## Actions + +Run the flow above. Read only the next action file. + +| Action | Does | +| --- | --- | +| verify | verify the bundled payload | + +## Transversal rules + +- Read [01-verify.md](actions/01-verify.md) before answering. +- Leave project files unchanged. diff --git a/scripts/__tests__/fixtures/context-generation/skills/opencode/.opencode/skills/verify-payload/actions/01-verify.md b/scripts/__tests__/fixtures/context-generation/skills/opencode/.opencode/skills/verify-payload/actions/01-verify.md new file mode 100644 index 000000000..7ef37d89a --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/skills/opencode/.opencode/skills/verify-payload/actions/01-verify.md @@ -0,0 +1,22 @@ +# 01 - Verify + +Verify the bundled payload. + +## Input + +A request to verify the phase three fixture. + +## Output + +The verification code. + +## Process + +1. **Read.** Read [payload.txt](../assets/payload.txt) for this request; do not guess its content. +2. **Reply.** Return exactly `PAYLOAD_APPLIED:` followed by the trimmed payload, with no extra text. + +## Test + +| Case | Pass | +| --- | --- | +| A verification request is answered | the payload read is traced and the exact verification code is returned | diff --git a/scripts/__tests__/fixtures/context-generation/skills/opencode/.opencode/skills/verify-payload/actions/02-user.md b/scripts/__tests__/fixtures/context-generation/skills/opencode/.opencode/skills/verify-payload/actions/02-user.md new file mode 100644 index 000000000..33c809839 --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/skills/opencode/.opencode/skills/verify-payload/actions/02-user.md @@ -0,0 +1,3 @@ +# User action + +USER_ACTION_KEEP diff --git a/scripts/__tests__/fixtures/context-generation/skills/opencode/.opencode/skills/verify-payload/assets/payload.txt b/scripts/__tests__/fixtures/context-generation/skills/opencode/.opencode/skills/verify-payload/assets/payload.txt new file mode 100644 index 000000000..0bf66b5e6 --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/skills/opencode/.opencode/skills/verify-payload/assets/payload.txt @@ -0,0 +1 @@ +cedar-7391 diff --git a/scripts/__tests__/fixtures/context-generation/skills/opencode/.opencode/skills/verify-payload/assets/user.txt b/scripts/__tests__/fixtures/context-generation/skills/opencode/.opencode/skills/verify-payload/assets/user.txt new file mode 100644 index 000000000..f52c38830 --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/skills/opencode/.opencode/skills/verify-payload/assets/user.txt @@ -0,0 +1 @@ +USER_RESOURCE_DO_NOT_REPLACE diff --git a/scripts/__tests__/fixtures/context-generation/skills/portable/.agents/skills/verify-payload/SKILL.md b/scripts/__tests__/fixtures/context-generation/skills/portable/.agents/skills/verify-payload/SKILL.md new file mode 100644 index 000000000..f15f6f952 --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/skills/portable/.agents/skills/verify-payload/SKILL.md @@ -0,0 +1,24 @@ +--- +name: verify-payload +description: Verify a bundled payload. Use when the user wants to verify the phase three fixture. +--- + +# Verify Payload + +```mermaid +flowchart LR + request([verification request]) --> verify --> answer([verification code]) +``` + +## Actions + +Run the flow above. Read only the next action file. + +| Action | Does | +| --- | --- | +| verify | verify the bundled payload | + +## Transversal rules + +- Read [01-verify.md](actions/01-verify.md) before answering. +- Leave project files unchanged. diff --git a/scripts/__tests__/fixtures/context-generation/skills/portable/.agents/skills/verify-payload/actions/01-verify.md b/scripts/__tests__/fixtures/context-generation/skills/portable/.agents/skills/verify-payload/actions/01-verify.md new file mode 100644 index 000000000..7ef37d89a --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/skills/portable/.agents/skills/verify-payload/actions/01-verify.md @@ -0,0 +1,22 @@ +# 01 - Verify + +Verify the bundled payload. + +## Input + +A request to verify the phase three fixture. + +## Output + +The verification code. + +## Process + +1. **Read.** Read [payload.txt](../assets/payload.txt) for this request; do not guess its content. +2. **Reply.** Return exactly `PAYLOAD_APPLIED:` followed by the trimmed payload, with no extra text. + +## Test + +| Case | Pass | +| --- | --- | +| A verification request is answered | the payload read is traced and the exact verification code is returned | diff --git a/scripts/__tests__/fixtures/context-generation/skills/portable/.agents/skills/verify-payload/actions/02-user.md b/scripts/__tests__/fixtures/context-generation/skills/portable/.agents/skills/verify-payload/actions/02-user.md new file mode 100644 index 000000000..33c809839 --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/skills/portable/.agents/skills/verify-payload/actions/02-user.md @@ -0,0 +1,3 @@ +# User action + +USER_ACTION_KEEP diff --git a/scripts/__tests__/fixtures/context-generation/skills/portable/.agents/skills/verify-payload/assets/payload.txt b/scripts/__tests__/fixtures/context-generation/skills/portable/.agents/skills/verify-payload/assets/payload.txt new file mode 100644 index 000000000..0bf66b5e6 --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/skills/portable/.agents/skills/verify-payload/assets/payload.txt @@ -0,0 +1 @@ +cedar-7391 diff --git a/scripts/__tests__/fixtures/context-generation/skills/portable/.agents/skills/verify-payload/assets/user.txt b/scripts/__tests__/fixtures/context-generation/skills/portable/.agents/skills/verify-payload/assets/user.txt new file mode 100644 index 000000000..f52c38830 --- /dev/null +++ b/scripts/__tests__/fixtures/context-generation/skills/portable/.agents/skills/verify-payload/assets/user.txt @@ -0,0 +1 @@ +USER_RESOURCE_DO_NOT_REPLACE diff --git a/scripts/__tests__/fixtures/skill-eval-snapshot-race.mjs b/scripts/__tests__/fixtures/skill-eval-snapshot-race.mjs new file mode 100644 index 000000000..a7969b181 --- /dev/null +++ b/scripts/__tests__/fixtures/skill-eval-snapshot-race.mjs @@ -0,0 +1,35 @@ +import fs from "node:fs"; +import { syncBuiltinESMExports } from "node:module"; +import { treeSnapshot } from "../../skill-eval-snapshot.mjs"; + +const root = process.env.AIDD_SNAPSHOT_ROOT; +const target = process.env.AIDD_SNAPSHOT_TARGET; +const backup = `${target}.original`; +const originalOpen = fs.openSync; +const originalRead = fs.readFileSync; +let targetFd; +let replaced = false; + +fs.openSync = function (file, ...args) { + const fd = originalOpen.call(this, file, ...args); + if (file === target) targetFd = fd; + return fd; +}; +fs.readFileSync = function (file, ...args) { + if (!replaced && typeof file === "number" && file === targetFd) { + replaced = true; + fs.renameSync(target, backup); + fs.writeFileSync(target, "substituted bytes"); + } + return originalRead.call(this, file, ...args); +}; +syncBuiltinESMExports(); + +try { + treeSnapshot(root); + console.error("snapshot accepted a substituted file"); + process.exitCode = 1; +} catch (error) { + if (!/File changed while snapshotting/u.test(error.message)) throw error; + console.log("snapshot refused a substituted file"); +} diff --git a/scripts/__tests__/kilo-rule-publication.test.js b/scripts/__tests__/kilo-rule-publication.test.js new file mode 100644 index 000000000..268f8b77c --- /dev/null +++ b/scripts/__tests__/kilo-rule-publication.test.js @@ -0,0 +1,421 @@ +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const os = require("node:os"); +const path = require("node:path"); +const { spawnSync } = require("node:child_process"); +const { test } = require("node:test"); +const skill = path.resolve(__dirname, "../../plugins/aidd-context/skills/05-rule-generate"); +const rule = { category: "01-standards", slug: "1-naming", description: "Naming", paths: [], body: "# Naming\n\nUse the sentinel word cobalt.\n" }; + +const fixtureRoot = path.join(__dirname, "fixtures/context-generation/kilo-config"); +const modulePath = process.env.AIDD_KILO_CONFIG_MODULE || path.resolve(__dirname, "../../plugins/aidd-context/skills/05-rule-generate/scripts/kilo-config.cjs"); +const instruction = ".kilo/rules/01-standards/1-naming.md"; + +function project(t) { + const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "aidd-kilo-config-"))); + t.after(() => fs.rmSync(root, { recursive: true, force: true })); + function put(relative, data) { + const file = path.join(root, relative); + fs.mkdirSync(path.dirname(file), { recursive: true }); + fs.writeFileSync(file, data); + } + function snapshot() { + const found = new Map(); + function walk(dir, prefix = "") { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const name = prefix + entry.name; + const full = path.join(dir, entry.name); + if (entry.isDirectory()) { found.set(`${name}/`, "directory"); walk(full, `${name}/`); } + else found.set(name, entry.isSymbolicLink() ? fs.readlinkSync(full) : fs.readFileSync(full)); + } + } + walk(root); + return found; + } + return { root, put, snapshot }; +} + +function fixture(name) { return fs.readFileSync(path.join(fixtureRoot, name), "utf8"); } +function api() { return require(modulePath); } + +function injected(t, root, source) { + const preload = path.join(path.dirname(root), `aidd-kilo-inject-${process.pid}-${Math.random().toString(36).slice(2)}.cjs`); + fs.writeFileSync(preload, source); + t.after(() => fs.rmSync(preload, { force: true })); + return { NODE_OPTIONS: `--require=${preload}` }; +} + +function modes(root, paths) { + return new Map(paths.map((relative) => [relative, fs.statSync(path.join(root, relative)).mode & 0o7777])); +} + +function publisher(t) { + const p = project(t); + p.put("package.json", '{"type":"module"}'); + const installed = path.join(path.dirname(p.root), `aidd-kilo-installed-${process.pid}-${Math.random().toString(36).slice(2)}`); + fs.cpSync(skill, installed, { recursive: true }); + t.after(() => fs.rmSync(installed, { recursive: true, force: true })); + const script = path.join(installed, "scripts/write-rule.cjs"); + function run(value = rule, targets = "kilo", extra = [], env = {}) { + const input = path.join(installed, "input.json"); + fs.writeFileSync(input, JSON.stringify(value)); + const mode = extra.some((item) => ["--input", "--delete", "--publish"].includes(item)) ? extra : [...extra, "--input", input]; + return spawnSync(process.execPath, [script, "--project", p.root, "--tools", targets, ...mode], { encoding: "utf8", cwd: p.root, env: { PATH: "", ...env } }); + } + return { ...p, run, read: (relative) => fs.readFileSync(path.join(p.root, relative), "utf8") }; +} + +test("installed writer publishes a Kilo rule and exact instruction, then reruns byte-identically", (t) => { + const p = publisher(t); + assert.equal(p.run().status, 0); + assert.match(p.read(instruction), /Use the sentinel word cobalt/); + assert.deepEqual(JSON.parse(p.read(".kilo/kilo.jsonc")).instructions, [instruction]); + assert.match(p.read("aidd_docs/rules/01-standards/1-naming.md"), /Use the sentinel word cobalt/); + const before = p.snapshot(); + const tracked = [instruction, ".kilo/kilo.jsonc", "aidd_docs/rules/01-standards/1-naming.md"]; + const mtimes = new Map(tracked.map((relative) => [relative, fs.statSync(path.join(p.root, relative)).mtimeMs])); + assert.equal(p.run().status, 0); + assert.deepEqual(p.snapshot(), before); + for (const relative of tracked) assert.equal(fs.statSync(path.join(p.root, relative)).mtimeMs, mtimes.get(relative)); + assert.equal(p.run(null, "kilo", ["--delete", "01-standards/1-naming"]).status, 0); + assert.equal(fs.existsSync(path.join(p.root, instruction)), false); + assert.deepEqual(JSON.parse(p.read(".kilo/kilo.jsonc")).instructions, []); +}); + +test("Kilo publication preserves JSONC comments, duplicate entries, order and unrelated values", (t) => { + const p = publisher(t); + const original = fixture("comments-and-trailing.jsonc"); + p.put(".kilo/kilo.jsonc", original); + assert.equal(p.run().status, 0); + assert.equal(p.read(".kilo/kilo.jsonc").replace(JSON.stringify(instruction) + ",", ""), original); + const before = p.snapshot(); + assert.equal(p.run(null, "kilo", ["--publish"]).status, 0); + assert.deepEqual(p.snapshot(), before); +}); + +test("Kilo rendering carries canonical file scope into the loaded Markdown", (t) => { + const p = publisher(t); + assert.equal(p.run({ ...rule, paths: ["src/**/*.ts"] }).status, 0); + assert.match(p.read(instruction), /^Applies to: `src\/\*\*\/\*\.ts`\.\n\n# Naming/); +}); + +test("ambiguous selection, unsafe last config and unowned Kilo output refuse the whole fan-out", (t) => { + const p = publisher(t); + p.put(".kilo/kilo.json", "{}\n"); + p.put("kilo.jsonc", "{}\n"); + let before = p.snapshot(); + const ambiguous = p.run(rule, "claude,kilo"); + assert.notEqual(ambiguous.status, 0); + assert.match(ambiguous.stderr, /Choose a Kilo config/); + assert.deepEqual(p.snapshot(), before); + assert.equal(p.run(rule, "claude,kilo", ["--kilo-config", "kilo.jsonc"]).status, 0); + p.put("kilo.jsonc", '{"instructions":[null]}'); + before = p.snapshot(); + assert.notEqual(p.run(rule, "claude,kilo").status, 0); + assert.deepEqual(p.snapshot(), before); + p.put("kilo.jsonc", "{}\n"); + p.put(instruction, "user owned\n"); + before = p.snapshot(); + assert.notEqual(p.run(rule, "claude,kilo", ["--kilo-config", "kilo.jsonc"]).status, 0); + assert.deepEqual(p.snapshot(), before); +}); + +test("existing exact owner is reused without changing duplicate instruction entries", (t) => { + const p = publisher(t); + const owned = `{"instructions":["${instruction}","${instruction}",]}`; + p.put("kilo.jsonc", owned); + p.put(".kilo/kilo.json", "{}\n"); + assert.equal(p.run().status, 0); + assert.equal(p.read("kilo.jsonc"), owned); + assert.equal(p.read(".kilo/kilo.json"), "{}\n"); +}); + +test("writer refuses deletion with two exact owners and leaves rules, configs and directories intact", (t) => { + const p = publisher(t); + p.put(".kilo/kilo.json", `{"instructions":["${instruction}"]}\n`); + assert.equal(p.run().status, 0); + p.put("kilo.jsonc", `{"instructions":["${instruction}"]}\n`); + const before = p.snapshot(); + const result = p.run(null, "kilo", ["--kilo-config", ".kilo/kilo.json", "--delete", "01-standards/1-naming"]); + assert.notEqual(result.status, 0); + assert.match(result.stderr, /Multiple Kilo configs own/); + assert.deepEqual(p.snapshot(), before); + assert.equal(fs.existsSync(path.join(p.root, instruction)), true); +}); + +test("writer refuses removal of duplicate exact entries without mutation", (t) => { + const p = publisher(t); + assert.equal(p.run().status, 0); + p.put(".kilo/kilo.jsonc", `{"instructions":["${instruction}","${instruction}",]}`); + const before = p.snapshot(); + const result = p.run(null, "kilo", ["--delete", "01-standards/1-naming"]); + assert.notEqual(result.status, 0); + assert.match(result.stderr, /Duplicate exact instruction entries/); + assert.deepEqual(p.snapshot(), before); +}); + +test("partially written temporary file and newly created directories are cleaned", (t) => { + const p = publisher(t); + const before = p.snapshot(); + const env = injected(t, p.root, "const fs=require('node:fs');const write=fs.writeFileSync;let failed=false;fs.writeFileSync=function(file,data,...args){if(!failed&&typeof file==='number'){failed=true;write.call(this,file,data.slice(0,8),...args);throw new Error('injected partial temporary write')}return write.call(this,file,data,...args)};"); + const result = p.run(rule, "claude,kilo", [], env); + assert.notEqual(result.status, 0); + assert.match(result.stderr, /injected partial temporary write/); + assert.deepEqual(p.snapshot(), before); +}); + +test("late failure restores existing file bytes and modes", { skip: process.platform === "win32" }, (t) => { + const p = publisher(t); + p.put(".kilo/kilo.jsonc", "{}\n"); + assert.equal(p.run(rule, "codex,kilo").status, 0); + p.put(".kilo/kilo.jsonc", "{}\n"); + fs.chmodSync(path.join(p.root, ".kilo/kilo.jsonc"), 0o600); + fs.chmodSync(path.join(p.root, instruction), 0o600); + const tracked = [".kilo/kilo.jsonc", instruction, "aidd_docs/rules/01-standards/1-naming.md", "AGENTS.md"]; + const before = p.snapshot(); + const beforeModes = modes(p.root, tracked); + const env = injected(t, p.root, "const fs=require('node:fs');const rename=fs.renameSync;fs.renameSync=function(from,to){if(to.endsWith('/AGENTS.md'))throw new Error('injected late shared failure');return rename.apply(this,arguments)};"); + const result = p.run({ ...rule, body: "# Naming\n\nChanged body.\n" }, "codex,kilo", [], env); + assert.notEqual(result.status, 0); + assert.match(result.stderr, /injected late shared failure/); + assert.deepEqual(p.snapshot(), before); + assert.deepEqual(modes(p.root, tracked), beforeModes); +}); + +test("successful config replacement preserves private mode", { skip: process.platform === "win32" }, (t) => { + const p = publisher(t); + p.put(".kilo/kilo.jsonc", "{}\n"); + fs.chmodSync(path.join(p.root, ".kilo/kilo.jsonc"), 0o600); + assert.equal(p.run().status, 0); + assert.equal(fs.statSync(path.join(p.root, ".kilo/kilo.jsonc")).mode & 0o7777, 0o600); +}); + +for (const change of ["modified", "disappeared"]) { + test(`writer refuses config ${change} after inspection without publishing`, (t) => { + const p = publisher(t); + p.put(".kilo/kilo.jsonc", "{}\n"); + const config = path.join(p.root, ".kilo/kilo.jsonc"); + const env = { + ...injected(t, p.root, "const fs=require('node:fs');const read=fs.readFileSync;let changed=false;fs.readFileSync=function(file,...args){const data=read.call(this,file,...args);if(!changed&&typeof file==='number'){changed=true;if(process.env.AIDD_TEST_CONFIG_CHANGE==='modified')fs.writeFileSync(process.env.AIDD_TEST_CONFIG,'{\"other\":true}\\n');else fs.rmSync(process.env.AIDD_TEST_CONFIG)}return data};"), + AIDD_TEST_CONFIG: config, + AIDD_TEST_CONFIG_CHANGE: change, + }; + const result = p.run(rule, "claude,kilo", [], env); + assert.notEqual(result.status, 0); + assert.match(result.stderr, /changed after inspection/); + assert.equal(fs.existsSync(path.join(p.root, instruction)), false); + assert.equal(fs.existsSync(path.join(p.root, "aidd_docs")), false); + assert.equal(fs.existsSync(config), change === "modified"); + if (change === "modified") assert.equal(p.read(".kilo/kilo.jsonc"), '{"other":true}\n'); + assert.equal([...p.snapshot().keys()].some((name) => name.endsWith(".tmp")), false); + }); +} + +test("writer detects config mutation during staging and removes staged outputs", (t) => { + const p = publisher(t); + p.put(".kilo/kilo.jsonc", "{}\n"); + const config = path.join(p.root, ".kilo/kilo.jsonc"); + const env = { + ...injected(t, p.root, "const fs=require('node:fs');const open=fs.openSync;let changed=false;fs.openSync=function(file,...args){const fd=open.call(this,file,...args);if(!changed&&typeof file==='string'&&file.endsWith('.tmp')){changed=true;fs.writeFileSync(process.env.AIDD_TEST_CONFIG,'{\"external\":true}\\n')}return fd};"), + AIDD_TEST_CONFIG: config, + }; + const result = p.run(rule, "claude,kilo", [], env); + assert.notEqual(result.status, 0); + assert.match(result.stderr, /Kilo config changed during publication/); + assert.equal(p.read(".kilo/kilo.jsonc"), '{"external":true}\n'); + assert.deepEqual([...p.snapshot().keys()].sort(), [".kilo/", ".kilo/kilo.jsonc", "package.json"]); +}); + +test("late config rename failure restores canonical and native outputs", (t) => { + const p = publisher(t); + p.put(".kilo/kilo.jsonc", "{}\n"); + const before = p.snapshot(); + const preload = path.join(path.dirname(p.root), `aidd-kilo-fail-${process.pid}-${Math.random().toString(36).slice(2)}.cjs`); + const expectedConfig = path.resolve(p.root, ".kilo/kilo.jsonc"); + fs.writeFileSync(preload, `const fs=require('node:fs');const path=require('node:path');const expected=${JSON.stringify(expectedConfig)};const original=fs.renameSync;fs.renameSync=function(from,to){if(path.resolve(to)===expected)throw new Error('injected config rename failure');return original.apply(this,arguments)};`); + t.after(() => fs.rmSync(preload, { force: true })); + const result = p.run(rule, "claude,kilo", [], { NODE_OPTIONS: `--require=${preload}` }); + assert.notEqual(result.status, 0); + assert.match(result.stderr, /injected config rename failure/); + assert.deepEqual(p.snapshot(), before); +}); + +test("no config proposes .kilo/kilo.jsonc without creating it", (t) => { + const p = project(t); + const before = p.snapshot(); + const choice = api().inspectProject(p.root, instruction); + assert.equal(choice.selectedPath, ".kilo/kilo.jsonc"); + assert.equal(choice.needsChoice, false); + assert.deepEqual(p.snapshot(), before); +}); + +test("each sole project config is selected, regardless of format or location", (t) => { + for (const relative of [".kilo/kilo.json", ".kilo/kilo.jsonc", "kilo.json", "kilo.jsonc"]) { + const p = project(t); + p.put(relative, "{}\n"); + assert.equal(api().inspectProject(p.root, instruction).selectedPath, relative); + } +}); + +test("one exact owner wins across four configs; nested, glob and comment witnesses do not", (t) => { + const p = project(t); + p.put(".kilo/kilo.jsonc", fixture("comments-and-trailing.jsonc")); + p.put(".kilo/kilo.json", '{"instructions":[".kilo/rules/*.md"]}\n'); + p.put("kilo.json", `{"instructions":["${instruction}"]}\n`); + p.put("kilo.jsonc", `{// "${instruction}"\n "instructions":[]}`); + const choice = api().inspectProject(p.root, instruction); + assert.equal(choice.selectedPath, "kilo.json"); + assert.equal(choice.needsChoice, false); + assert.deepEqual(choice.candidates.map((item) => item.path), [".kilo/kilo.json", ".kilo/kilo.jsonc", "kilo.json", "kilo.jsonc"]); +}); + +test("multiple configs without an owner require choice; cancellation and explicit choice never write", (t) => { + const p = project(t); + p.put(".kilo/kilo.json", "{}\n"); + p.put("kilo.jsonc", "{}\n"); + const before = p.snapshot(); + const undecided = api().inspectProject(p.root, instruction); + assert.equal(undecided.selectedPath, null); + assert.equal(undecided.needsChoice, true); + assert.equal(api().inspectProject(p.root, instruction, null).selectedPath, null); + assert.equal(api().inspectProject(p.root, instruction, "kilo.jsonc").selectedPath, "kilo.jsonc"); + assert.deepEqual(p.snapshot(), before); +}); + +test("two exact owners remain ambiguous until explicit selection", (t) => { + const p = project(t); + for (const name of [".kilo/kilo.json", "kilo.jsonc"]) p.put(name, `{"instructions":["${instruction}"]}`); + assert.equal(api().inspectProject(p.root, instruction).needsChoice, true); + assert.equal(api().inspectProject(p.root, instruction, "kilo.jsonc").selectedPath, "kilo.jsonc"); +}); + +test("JSONC insertion preserves every original byte, duplicate and order", () => { + const original = fixture("comments-and-trailing.jsonc"); + const edited = api().prepareInstructionEdit(original, instruction); + assert.equal(edited.changed, true); + assert.equal(edited.content.replace(JSON.stringify(instruction) + ",", ""), original); + assert.match(edited.content, /"docs\/first\.md",[\s\S]*"docs\/first\.md"/); + const second = api().prepareInstructionEdit(edited.content, instruction); + assert.deepEqual(second, { content: edited.content, changed: false }); +}); + +test("escaped instructions key is reused, and absent key is inserted without serializing unrelated text", () => { + const escaped = fixture("escaped-key.jsonc"); + const reused = api().prepareInstructionEdit(escaped, instruction); + assert.equal(reused.content.replace(JSON.stringify(instruction), ""), escaped.replace('"docs/old.md"', '"docs/old.md",')); + assert.equal((reused.content.match(/instructions|instruct\\u0069ons/g) || []).length, 1); + const absent = "{\r\n // untouched\r\n \"other\": 1,\r\n}\r\n"; + const added = api().prepareInstructionEdit(absent, instruction); + assert.equal(added.changed, true); + assert.ok(added.content.includes("// untouched\r\n")); + assert.ok(added.content.includes(`"instructions": ["${instruction}"]`)); +}); + +test("existing exact entry is byte-identical even when duplicated", () => { + const original = `{"instructions":["${instruction}","${instruction}",]}`; + assert.deepEqual(api().prepareInstructionEdit(original, instruction), { content: original, changed: false }); +}); + +test("JSONC handles empty arrays, scalar comments and CRLF without changing unrelated bytes", () => { + for (const original of [ + '{"instructions":[]}', + '{"instructions":["docs/old.md"/* retained */]}', + '{\r\n "other": 1/* retained */,\r\n "instructions": [\r\n ]\r\n}\r\n', + ]) { + const result = api().prepareInstructionEdit(original, instruction); + assert.equal(result.changed, true); + assert.equal(api().prepareInstructionEdit(result.content, instruction).changed, false); + assert.ok(result.content.includes(JSON.stringify(instruction))); + if (original.includes("\r\n")) assert.ok(result.content.includes("\r\n")); + if (original.includes("/* retained */")) assert.ok(result.content.includes("/* retained */")); + } +}); + +test("copied CommonJS module runs in an ESM project without the CLI or checkout", (t) => { + const p = project(t); + p.put("package.json", '{"type":"module"}'); + const installed = path.join(p.root, "installed", "kilo-config.cjs"); + fs.mkdirSync(path.dirname(installed)); + fs.copyFileSync(modulePath, installed); + const program = `const m=require(${JSON.stringify(installed)}); process.stdout.write(JSON.stringify(m.inspectProject(${JSON.stringify(p.root)}, ${JSON.stringify(instruction)})))`; + const before = p.snapshot(); + const result = spawnSync(process.execPath, ["-e", program], { cwd: p.root, encoding: "utf8", env: { PATH: "" } }); + assert.equal(result.status, 0, result.stderr); + assert.ok(result.stdout, JSON.stringify({ status: result.status, stdout: result.stdout, stderr: result.stderr, error: result.error?.message })); + assert.equal(JSON.parse(result.stdout).selectedPath, ".kilo/kilo.jsonc"); + assert.deepEqual(p.snapshot(), before); +}); + +test("malformed, duplicate and non-string instructions refuse before any project write", (t) => { + const p = project(t); + for (const name of ["invalid-duplicate-key.jsonc", "invalid-element.jsonc", "invalid-unclosed.jsonc"]) { + p.put("kilo.jsonc", fixture(name)); + const before = p.snapshot(); + assert.throws(() => api().inspectProject(p.root, instruction)); + assert.deepEqual(p.snapshot(), before); + assert.throws(() => api().prepareInstructionEdit(fixture(name), instruction)); + } + for (const text of ['{"instructions":false}', '{"instructions":{}}', '{"instructions":[null]}']) { + assert.throws(() => api().prepareInstructionEdit(text, instruction)); + } + assert.throws(() => api().prepareInstructionEdit('{"note":"\ud800"}', instruction), /UTF-8/); + assert.throws(() => api().prepareInstructionEdit('{\u00a0"instructions":[]}', instruction), /JSONC/); +}); + +test("all existing configs are validated, even when another uniquely owns the entry", (t) => { + const p = project(t); + p.put(".kilo/kilo.json", `{"instructions":["${instruction}"]}`); + p.put("kilo.jsonc", fixture("invalid-element.jsonc")); + const before = p.snapshot(); + assert.throws(() => api().inspectProject(p.root, instruction)); + assert.deepEqual(p.snapshot(), before); +}); + +test("unsafe candidates, invalid UTF-8 and vanished choices refuse without changing bytes", (t) => { + const p = project(t); + p.put("kilo.jsonc", Buffer.from([0x7b, 0xff, 0x7d])); + assert.throws(() => api().inspectProject(p.root, instruction)); + p.put("kilo.jsonc", "{}\n"); + assert.throws(() => api().inspectProject(p.root, instruction, ".kilo/kilo.jsonc")); + assert.throws(() => api().inspectProject(p.root, "../escape.md")); + const outside = path.join(path.dirname(p.root), "outside-kilo-config.jsonc"); + p.put(".kilo/kilo.jsonc", "{}\n"); + fs.rmSync(path.join(p.root, ".kilo/kilo.jsonc")); + fs.symlinkSync(outside, path.join(p.root, ".kilo/kilo.jsonc")); + const before = p.snapshot(); + assert.throws(() => api().inspectProject(p.root, instruction)); + assert.deepEqual(p.snapshot(), before); +}); + +for (const replacement of ["file", "parent"]) { + test(`config read refuses a ${replacement} substitution after open without reading the outside witness`, (t) => { + const p = project(t); + p.put(".kilo/kilo.jsonc", '{"instructions":[]}\n'); + const outside = path.join(path.dirname(p.root), `aidd-kilo-outside-${process.pid}.jsonc`); + fs.writeFileSync(outside, '{"instructions":["outside"]}\n'); + t.after(() => fs.rmSync(outside, { force: true })); + const target = path.join(p.root, ".kilo/kilo.jsonc"); + const parent = path.dirname(target); + const held = `${target}.held`; + const outsideDir = path.join(path.dirname(p.root), `aidd-kilo-outside-dir-${process.pid}`); + fs.mkdirSync(outsideDir, { recursive: true }); + fs.writeFileSync(path.join(outsideDir, "kilo.jsonc"), '{"instructions":["outside-parent"]}\n'); + t.after(() => fs.rmSync(outsideDir, { recursive: true, force: true })); + const originalOpen = fs.openSync; + try { + fs.openSync = function (candidate, ...args) { + const fd = originalOpen.call(this, candidate, ...args); + if (candidate === target) { + fs.renameSync(replacement === "file" ? target : parent, replacement === "file" ? held : `${parent}.held`); + fs.symlinkSync(replacement === "file" ? outside : outsideDir, replacement === "file" ? target : parent, replacement === "parent" ? "dir" : undefined); + } + return fd; + }; + assert.throws(() => api().inspectProject(p.root, instruction), /safely read|changed|Symlink/u); + } finally { + fs.openSync = originalOpen; + } + assert.equal(fs.readFileSync(outside, "utf8"), '{"instructions":["outside"]}\n'); + assert.equal(fs.readFileSync(path.join(outsideDir, "kilo.jsonc"), "utf8"), '{"instructions":["outside-parent"]}\n'); + }); +} diff --git a/scripts/__tests__/model-selection.test.js b/scripts/__tests__/model-selection.test.js index 7eaa010e6..697a97b61 100644 --- a/scripts/__tests__/model-selection.test.js +++ b/scripts/__tests__/model-selection.test.js @@ -7,6 +7,22 @@ const test = require("node:test"); const ROOT = path.resolve(__dirname, "../.."); +test("static skill-eval cases specify idempotence and critical refusal checks", () => { + const cases = JSON.parse(fs.readFileSync(path.join(ROOT, "scripts/skill-eval/cases.json"), "utf8")); + const byName = new Map(cases.map((item) => [item.name, item])); + const idempotence = byName.get("Kilo native skill uses one canonical tree"); + const noConsent = byName.get("Kilo cannot choose portable without agreement"); + const mixedSkill = byName.get("unsafe late Kilo target leaves earlier Claude target absent"); + const mixedHook = byName.get("invalid final mixed target stops before prior writes"); + assert.equal(idempotence.expect.repeat, true); + for (const item of [noConsent, mixedSkill, mixedHook]) { + assert.ok(item, "Missing critical execution case"); + assert.ok(item.expect.stdoutMatches?.length, item.name); + assert.ok(item.expect.filesAbsent?.length, item.name); + assert.ok(item.expect.filesUnchanged?.length, item.name); + } +}); + test("bundled skills and agents leave model selection to the host", () => { const plugins = path.join(ROOT, "plugins"); const files = fs.readdirSync(plugins, { recursive: true }).filter((file) => @@ -61,3 +77,224 @@ for (const model of [undefined, "user-selected-model"]) { } }); } + +test("skill eval stages a worktree skill for Codex and checks its final output", (t) => { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "codex-skill-eval-")); + t.after(() => fs.rmSync(tmp, { recursive: true, force: true })); + const stub = path.join(tmp, "codex.cjs"); + fs.writeFileSync(stub, [ + 'const fs = require("node:fs");', + 'const path = require("node:path");', + 'const args = process.argv.slice(2);', + 'const cwd = process.cwd();', + 'if (!fs.existsSync(path.join(cwd, ".agents/skills/xeval-shadow/SKILL.md"))) process.exit(3);', + 'if (fs.existsSync(path.join(cwd, ".claude/skills/xeval-shadow/SKILL.md"))) process.exit(4);', + 'fs.writeFileSync("Makefile-shadow-report.md", "stub report");', + 'if (!process.env.MISSING_CODEX_OUTPUT) fs.writeFileSync(args[args.indexOf("-o") + 1], "completed");', + ].join("\n")); + const preload = path.join(tmp, "preload.cjs"); + fs.writeFileSync(preload, [ + 'const childProcess = require("node:child_process");', + 'const spawn = childProcess.spawn;', + `childProcess.spawn = (command, args, options) => command === "codex"`, + ` ? spawn(process.execPath, [${JSON.stringify(stub)}, ...args], options)`, + ' : spawn(command, args, options);', + 'require("node:module").syncBuiltinESMExports();', + ].join("\n")); + const result = spawnSync(process.execPath, [ + "--require", preload, + path.join(ROOT, "scripts/skill-eval.mjs"), + "--host=codex", + "--case=filename rule keeps a dotless name", + ], { encoding: "utf8" }); + assert.equal(result.status, 0, result.stderr + result.stdout); + const missingOutput = spawnSync(process.execPath, [ + "--require", preload, + path.join(ROOT, "scripts/skill-eval.mjs"), + "--host=codex", + "--case=filename rule keeps a dotless name", + ], { encoding: "utf8", env: { ...process.env, MISSING_CODEX_OUTPUT: "1" } }); + assert.equal(missingOutput.status, 1, missingOutput.stdout + missingOutput.stderr); +}); + +test("skill eval marks a skipped judge incomplete instead of passing it", (t) => { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "skill-eval-judge-skip-")); + t.after(() => fs.rmSync(tmp, { recursive: true, force: true })); + const stub = path.join(tmp, "codex.cjs"); + fs.writeFileSync(stub, [ + 'const fs = require("node:fs");', + 'const args = process.argv.slice(2);', + 'fs.appendFileSync(process.env.SKILL_EVAL_CALLS, JSON.stringify(args) + "\\n");', + 'fs.writeFileSync(args[args.indexOf("-o") + 1], "Use `.kilo/plugin/`; `session.created` is documented.");', + ].join("\n")); + const preload = path.join(tmp, "preload.cjs"); + fs.writeFileSync(preload, [ + 'const childProcess = require("node:child_process");', + 'const spawn = childProcess.spawn;', + `childProcess.spawn = (command, args, options) => command === "codex"`, + ` ? spawn(process.execPath, [${JSON.stringify(stub)}, ...args], options)`, + ' : spawn(command, args, options);', + 'require("node:module").syncBuiltinESMExports();', + ].join("\n")); + const calls = path.join(tmp, "calls.jsonl"); + const result = spawnSync(process.execPath, [ + "--require", preload, + path.join(ROOT, "scripts/skill-eval.mjs"), + "--host=codex", + "--case=Kilo-only session start gives guidance without files", + ], { encoding: "utf8", env: { ...process.env, SKILL_EVAL_CALLS: calls } }); + assert.equal(result.status, 0, result.stderr + result.stdout); + assert.match(result.stdout, /INCOMPLETE \(judge skipped\)/u); + assert.match(result.stdout, /0\/1 cases fully passed; 1 incomplete/u); + assert.match(result.stdout, /1 judge evaluation\(s\) skipped/u); + assert.doesNotMatch(result.stdout, /1\/1 cases fully passed/u); + assert.equal(fs.readFileSync(calls, "utf8").trim().split("\n").length, 1); +}); + +test("skill eval runs a Codex judge with Codex rather than Claude", (t) => { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "skill-eval-codex-judge-")); + t.after(() => fs.rmSync(tmp, { recursive: true, force: true })); + const stub = path.join(tmp, "codex.cjs"); + fs.writeFileSync(stub, [ + 'const fs = require("node:fs");', + 'const args = process.argv.slice(2);', + 'fs.appendFileSync(process.env.SKILL_EVAL_CALLS, JSON.stringify(args) + "\\n");', + 'fs.writeFileSync(args[args.indexOf("-o") + 1], args.at(-1).includes("You grade a test") ? "PASS\\nvalid guidance" : "Use `.kilo/plugin/`; `session.created` is documented.");', + ].join("\n")); + const preload = path.join(tmp, "preload.cjs"); + fs.writeFileSync(preload, [ + 'const childProcess = require("node:child_process");', + 'const spawn = childProcess.spawn;', + `childProcess.spawn = (command, args, options) => ["codex", "claude"].includes(command)`, + ` ? spawn(process.execPath, [${JSON.stringify(stub)}, command, ...args], options)`, + ' : spawn(command, args, options);', + 'require("node:module").syncBuiltinESMExports();', + ].join("\n")); + const calls = path.join(tmp, "calls.jsonl"); + const result = spawnSync(process.execPath, [ + "--require", preload, + path.join(ROOT, "scripts/skill-eval.mjs"), + "--host=codex", + "--judge", + "--case=Kilo-only session start gives guidance without files", + ], { encoding: "utf8", env: { ...process.env, SKILL_EVAL_CALLS: calls } }); + assert.equal(result.status, 0, result.stderr + result.stdout); + assert.match(result.stdout, /PASS 08-hook-generate/u); + assert.match(result.stdout, /\(PASS \| valid guidance\)/u); + const invocations = fs.readFileSync(calls, "utf8").trim().split("\n").map(JSON.parse); + assert.equal(invocations.length, 2); + assert.ok(invocations.every(([host]) => host === "codex")); + assert.ok(invocations[1].at(-1).includes("You grade a test")); +}); + +test("skill eval checks documented event names and native hook JSON shapes", (t) => { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "skill-eval-hook-json-")); + t.after(() => fs.rmSync(tmp, { recursive: true, force: true })); + const stub = path.join(tmp, "codex.cjs"); + fs.writeFileSync(stub, [ + 'const fs = require("node:fs");', + 'const path = require("node:path");', + 'const args = process.argv.slice(2);', + 'if (!args.at(-1).includes("You grade a test")) {', + ' const files = {', + ' ".claude/settings.json": { hooks: { SessionStart: [{ hooks: [{ type: "command", command: "/hook.sh" }] }] } },', + ' ".cursor/hooks.json": { version: 1, hooks: { sessionStart: [{ command: "/hook.sh" }] } },', + ' ".github/hooks/aidd.json": { version: 1, hooks: { SessionStart: [{ type: "command", command: "/hook.sh" }] } },', + ' };', + ' for (const [file, value] of Object.entries(files)) {', + ' fs.mkdirSync(path.dirname(file), { recursive: true });', + ' fs.writeFileSync(file, JSON.stringify(value));', + ' }', + '}', + 'fs.writeFileSync(args[args.indexOf("-o") + 1], args.at(-1).includes("You grade a test") ? "PASS\\nnative hook entries are valid" : "Published Claude, Cursor and Copilot session-start hooks.");', + ].join("\n")); + const preload = path.join(tmp, "preload.cjs"); + fs.writeFileSync(preload, [ + 'const childProcess = require("node:child_process");', + 'const spawn = childProcess.spawn;', + `childProcess.spawn = (command, args, options) => command === "codex"`, + ` ? spawn(process.execPath, [${JSON.stringify(stub)}, ...args], options)`, + ' : spawn(command, args, options);', + 'require("node:module").syncBuiltinESMExports();', + ].join("\n")); + const result = spawnSync(process.execPath, [ + "--require", preload, + path.join(ROOT, "scripts/skill-eval.mjs"), + "--host=codex", + "--judge", + "--case=declarative session start publishes supported non-Codex targets", + ], { encoding: "utf8" }); + assert.equal(result.status, 0, result.stdout + result.stderr); + assert.match(result.stdout, /valid JSON: \.claude\/settings\.json/u); + assert.match(result.stdout, /\.cursor\/hooks\.json matches/u); + assert.match(result.stdout, /\.github\/hooks\/aidd\.json matches/u); + assert.match(result.stdout, /judge: .*\(PASS \| native hook entries are valid\)/u); +}); + +test("skill eval detects unlisted writes in a guidance-only project tree", (t) => { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "skill-eval-guidance-write-")); + t.after(() => fs.rmSync(tmp, { recursive: true, force: true })); + const stub = path.join(tmp, "codex.cjs"); + fs.writeFileSync(stub, [ + 'const fs = require("node:fs");', + 'const args = process.argv.slice(2);', + 'fs.mkdirSync(".kilo/plugin", { recursive: true });', + 'fs.writeFileSync(".kilo/plugin/unrequested.js", "export {};\\n");', + 'fs.writeFileSync(args[args.indexOf("-o") + 1], "Use `.kilo/plugin/`; `session.created` is documented.");', + ].join("\n")); + const preload = path.join(tmp, "preload.cjs"); + fs.writeFileSync(preload, [ + 'const childProcess = require("node:child_process");', + 'const spawn = childProcess.spawn;', + `childProcess.spawn = (command, args, options) => command === "codex"`, + ` ? spawn(process.execPath, [${JSON.stringify(stub)}, ...args], options)`, + ' : spawn(command, args, options);', + 'require("node:module").syncBuiltinESMExports();', + ].join("\n")); + const result = spawnSync(process.execPath, [ + "--require", preload, + path.join(ROOT, "scripts/skill-eval.mjs"), + "--host=codex", + "--case=Kilo-only session start gives guidance without files", + ], { encoding: "utf8" }); + assert.equal(result.status, 1, result.stdout + result.stderr); + assert.match(result.stdout, /project tree changed:.*\.kilo\//u); +}); + +test("skill eval reports an executed idempotent rerun separately from its initial generation", (t) => { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "skill-eval-idempotence-")); + t.after(() => fs.rmSync(tmp, { recursive: true, force: true })); + const stub = path.join(tmp, "codex.cjs"); + fs.writeFileSync(stub, [ + 'const fs = require("node:fs");', + 'const path = require("node:path");', + 'const args = process.argv.slice(2);', + 'fs.appendFileSync(process.env.SKILL_EVAL_CALLS, "call\\n");', + 'const target = path.join(process.cwd(), ".kilo/skills/verify-payload");', + 'if (!fs.existsSync(target)) {', + ' fs.mkdirSync(path.join(target, "actions"), { recursive: true });', + ' fs.writeFileSync(path.join(target, "SKILL.md"), "---\\nname: verify-payload\\ndescription: verify fixture\\n---\\n");', + ' fs.writeFileSync(path.join(target, "actions/01-verify.md"), "# Verify\\n");', + '}', + 'fs.writeFileSync(args[args.indexOf("-o") + 1], "completed");', + ].join("\n")); + const preload = path.join(tmp, "preload.cjs"); + fs.writeFileSync(preload, [ + 'const childProcess = require("node:child_process");', + 'const spawn = childProcess.spawn;', + `childProcess.spawn = (command, args, options) => command === "codex"`, + ` ? spawn(process.execPath, [${JSON.stringify(stub)}, ...args], options)`, + ' : spawn(command, args, options);', + 'require("node:module").syncBuiltinESMExports();', + ].join("\n")); + const calls = path.join(tmp, "calls.txt"); + const result = spawnSync(process.execPath, [ + "--require", preload, + path.join(ROOT, "scripts/skill-eval.mjs"), + "--host=codex", + "--case=Kilo native skill uses one canonical tree", + ], { encoding: "utf8", env: { ...process.env, SKILL_EVAL_CALLS: calls } }); + assert.equal(result.status, 0, result.stderr + result.stdout); + assert.match(result.stdout, /idempotent rerun left project bytes and mtimes unchanged/u); + assert.equal(fs.readFileSync(calls, "utf8").trim().split("\n").length, 2); +}); diff --git a/scripts/__tests__/skill-eval-snapshot.test.js b/scripts/__tests__/skill-eval-snapshot.test.js new file mode 100644 index 000000000..6f4216ff0 --- /dev/null +++ b/scripts/__tests__/skill-eval-snapshot.test.js @@ -0,0 +1,53 @@ +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const os = require("node:os"); +const path = require("node:path"); +const { spawnSync } = require("node:child_process"); +const { test } = require("node:test"); +const { treeSnapshot } = require("../skill-eval-snapshot.mjs"); + +test("skill-eval snapshots nested file bytes and directory entries", (t) => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "skill-eval-snapshot-")); + t.after(() => fs.rmSync(root, { recursive: true, force: true })); + fs.mkdirSync(path.join(root, "nested")); + fs.writeFileSync(path.join(root, "nested", "fixture.txt"), "controlled bytes"); + fs.writeFileSync(path.join(root, "z-last.txt"), "last"); + fs.writeFileSync(path.join(root, "a-first.txt"), "first"); + fs.writeFileSync(path.join(root, ".skill-eval-last-message.txt"), "ignored output"); + + const snapshot = treeSnapshot(root); + assert.equal(snapshot.get("nested/fixture.txt").split(":").at(-1), Buffer.from("controlled bytes").toString("base64")); + assert.match(snapshot.get("nested/"), /^directory:/); + assert.deepEqual([...snapshot.keys()], ["a-first.txt", "nested/", "nested/fixture.txt", "z-last.txt"]); + assert.equal(snapshot.has(".skill-eval-last-message.txt"), false); +}); + +test("skill-eval refuses a file replaced between inspection and reading", (t) => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "skill-eval-snapshot-race-")); + t.after(() => fs.rmSync(root, { recursive: true, force: true })); + const target = path.join(root, "fixture.txt"); + fs.writeFileSync(target, "controlled bytes"); + const fixture = path.join(__dirname, "fixtures/skill-eval-snapshot-race.mjs"); + const result = spawnSync(process.execPath, [fixture], { + encoding: "utf8", + env: { ...process.env, AIDD_SNAPSHOT_ROOT: root, AIDD_SNAPSHOT_TARGET: target }, + }); + + assert.equal(result.status, 0, result.stderr); + assert.match(result.stdout, /snapshot refused a substituted file/u); +}); + +test("skill-eval records a symlink without reading its target", { skip: process.platform === "win32" }, (t) => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "skill-eval-snapshot-link-")); + const outside = fs.mkdtempSync(path.join(os.tmpdir(), "skill-eval-outside-")); + t.after(() => { + fs.rmSync(root, { recursive: true, force: true }); + fs.rmSync(outside, { recursive: true, force: true }); + }); + fs.writeFileSync(path.join(outside, "secret.txt"), "outside bytes"); + fs.symlinkSync(outside, path.join(root, "link"), "dir"); + + const snapshot = treeSnapshot(root); + assert.equal(snapshot.get("link"), "other:true"); + assert.equal(snapshot.has("link/secret.txt"), false); +}); diff --git a/scripts/__tests__/update-memory.test.js b/scripts/__tests__/update-memory.test.js index fc9440cf4..348ffe38a 100644 --- a/scripts/__tests__/update-memory.test.js +++ b/scripts/__tests__/update-memory.test.js @@ -9,8 +9,7 @@ const test = require("node:test"); // where a "type": "module" package.json decides how it is parsed. Driving it as a // subprocess is the only way to test what actually ships. const HOOK = path.resolve(__dirname, "../../plugins/aidd-context/hooks/update_memory.js"); -// Run from inside a Claude Code session, an inherited CLAUDE_PROJECT_DIR would send the hook -// into that real project and rewrite its CLAUDE.md and memory README. +// Run from inside a Claude Code session; inherited CLAUDE_PROJECT_DIR must not redirect the hook into a real project. const HOOK_ENV = { ...process.env, CLAUDE_PROJECT_DIR: "" }; const OPEN = ""; @@ -235,6 +234,590 @@ test("a nested context file prefixes its links with the climb back out", () => { ); }); +/** Real files and subprocesses; explicit sync must validate the whole selected set first. */ +function inProject(files, check) { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "memory-preflight-")); + try { + for (const [relative, content] of Object.entries({ + "aidd_docs/memory/architecture.md": "# Architecture\n\n- Runtime memory\n", + ...files, + })) { + const file = path.join(root, relative); + fs.mkdirSync(path.dirname(file), { recursive: true }); + fs.writeFileSync(file, content); + } + const env = Object.fromEntries(Object.entries(process.env).filter(([name]) => + !name.startsWith("GIT_") && name !== "CLAUDE_PROJECT_DIR")); + const invoke = (...args) => { + const result = spawnSync(process.execPath, [HOOK, ...args], { cwd: root, env, encoding: "utf8" }); + return result; + }; + invoke.withRace = (preload, args) => { + const preloadPath = path.join(root, "race-preload.cjs"); + fs.writeFileSync(preloadPath, preload); + const result = spawnSync(process.execPath, [HOOK, ...args], { + cwd: root, + env: { ...env, NODE_OPTIONS: `--require=${preloadPath}` }, + encoding: "utf8", + }); + return result; + }; + const read = (name) => fs.readFileSync(path.join(root, name), "utf8"); + check({ root, invoke, read }); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +} + +const EMPTY_BLOCK = `${OPEN}\n${CLOSE}\n`; + +function readDescriptor(fd, size) { + const bytes = Buffer.alloc(Number(size)); + let offset = 0; + while (offset < bytes.length) { + const count = fs.readSync(fd, bytes, offset, bytes.length - offset, offset); + if (count === 0) break; + offset += count; + } + return bytes.subarray(0, offset); +} + +function openWitness(filePath) { + const fd = fs.openSync(filePath, "r"); + const stat = fs.fstatSync(fd, { bigint: true }); + return { fd, stat, bytes: readDescriptor(fd, stat.size) }; +} + +function assertWitnessUnchanged(witness) { + const after = fs.fstatSync(witness.fd, { bigint: true }); + for (const key of ["dev", "ino", "nlink", "mode", "size", "mtimeNs"]) { + assert.equal(after[key], witness.stat[key], `witness ${key} changed`); + } + assert.deepEqual(readDescriptor(witness.fd, after.size), witness.bytes); +} + +function assertPathStillNamesWitness(filePath, witness) { + const fd = fs.openSync(filePath, "r"); + try { + const stat = fs.fstatSync(fd, { bigint: true }); + assert.equal(stat.dev, witness.stat.dev); + assert.equal(stat.ino, witness.stat.ino); + } finally { + fs.closeSync(fd); + } +} + +function closeWitness(witness) { + if (witness?.fd !== undefined) { + const fd = witness.fd; + witness.fd = undefined; + fs.closeSync(fd); + } +} + +const DESCRIPTOR_TRACKER = [ + "{", + "const fs = require('node:fs');", + "const { syncBuiltinESMExports } = require('node:module');", + "const open = fs.openSync; const close = fs.closeSync; const tracked = new Set();", + "fs.openSync = function (file, ...args) { const fd = open.call(this, file, ...args); if (typeof file === 'string' && /(?:CLAUDE|AGENTS|README)\\.md$/.test(file)) { tracked.add(fd); process.stderr.write('OPEN:' + fd + '\\n'); } return fd; };", + "fs.closeSync = function (fd) { if (tracked.has(fd)) { tracked.delete(fd); process.stderr.write('CLOSE:' + fd + '\\n'); } return close.call(this, fd); };", + "syncBuiltinESMExports();", + "}", +].join("\n"); + +function assertTrackedDescriptorsClosed(stderr) { + const opened = [...stderr.matchAll(/OPEN:(\d+)/gu)].map((match) => match[1]); + const closed = [...stderr.matchAll(/CLOSE:(\d+)/gu)].map((match) => match[1]); + assert.ok(opened.length > 0, `the hook should open at least one tracked destination; stderr: ${stderr}`); + assert.deepEqual(closed, opened, "every opened destination descriptor should be explicitly closed"); +} + +test("preflight closes descriptors after --check and after a later marker error", () => { + for (const malformed of [false, true]) { + inProject({ + "CLAUDE.md": EMPTY_BLOCK, + "AGENTS.md": malformed ? `${OPEN}\n` : EMPTY_BLOCK, + }, ({ invoke }) => { + const result = invoke.withRace(DESCRIPTOR_TRACKER, ["--check", "claude", "codex"]); + assert.equal(result.status, malformed ? 1 : 0, result.stderr); + assertTrackedDescriptorsClosed(result.stderr); + }); + } +}); + +test("preflight closes descriptors when the memory bank is absent", () => { + inProject({ "CLAUDE.md": EMPTY_BLOCK }, ({ root, invoke }) => { + fs.rmSync(path.join(root, "aidd_docs"), { recursive: true, force: true }); + const result = invoke.withRace(DESCRIPTOR_TRACKER, ["claude"]); + assert.equal(result.status, 0, result.stderr); + assertTrackedDescriptorsClosed(result.stderr); + }); +}); + +test("preflight closes a descriptor when reading an opened destination fails", () => { + inProject({ "CLAUDE.md": EMPTY_BLOCK }, ({ invoke }) => { + const preload = [ + DESCRIPTOR_TRACKER, + "{ const fs = require('node:fs'); const read = fs.readFileSync;", + "fs.readFileSync = function (file, ...args) { if (typeof file === 'number') { const error = new Error('injected read failure'); error.code = 'EIO'; throw error; } return read.call(this, file, ...args); };", + "const { syncBuiltinESMExports } = require('node:module'); syncBuiltinESMExports(); }", + ].join("\n"); + const result = invoke.withRace(preload, ["claude"]); + assert.equal(result.status, 1); + assert.match(result.stderr, /injected read failure/u); + assertTrackedDescriptorsClosed(result.stderr); + }); +}); + +test("automatic sync closes context and README descriptors when the README write fails", () => { + inProject({ + "CLAUDE.md": EMPTY_BLOCK, + "aidd_docs/memory/README.md": "# Memory\n\n\n", + }, ({ invoke, read }) => { + const preload = [ + DESCRIPTOR_TRACKER, + "{ const fs = require('node:fs'); const path = require('node:path'); const open = fs.openSync; const write = fs.writeSync; const readmeFds = new Set();", + "fs.openSync = function (file, ...args) { const fd = open.call(this, file, ...args); if (typeof file === 'string' && file.endsWith(path.join('aidd_docs', 'memory', 'README.md'))) readmeFds.add(fd); return fd; };", + "fs.writeSync = function (fd, ...args) { if (readmeFds.has(fd)) { const error = new Error('injected README write failure'); error.code = 'EIO'; throw error; } return write.call(this, fd, ...args); };", + "const { syncBuiltinESMExports } = require('node:module'); syncBuiltinESMExports(); }", + ].join("\n"); + const result = invoke.withRace(preload, []); + assert.equal(result.status, 1); + assert.match(result.stderr, /injected README write failure/u); + assertTrackedDescriptorsClosed(result.stderr); + assert.match(read("CLAUDE.md"), /@aidd_docs\/memory\/architecture\.md/u); + }); +}); + +test("Kilo selected alone fills root AGENTS.md without changing other tools or legacy paths", () => { + inProject({ + "AGENTS.md": `USER PREFIX\n${EMPTY_BLOCK}USER SUFFIX\n`, + "CLAUDE.md": EMPTY_BLOCK, + ".github/copilot-instructions.md": EMPTY_BLOCK, + ".kilocode/keep.md": "legacy user content\n", + }, ({ root, invoke, read }) => { + const result = invoke("kilo"); + assert.equal(result.status, 0, result.stderr); + assert.equal(read("AGENTS.md"), `USER PREFIX\n${OPEN}\n\n[aidd_docs/memory/architecture.md](aidd_docs/memory/architecture.md)\n\n${CLOSE}\nUSER SUFFIX\n`); + assert.equal(read("CLAUDE.md"), EMPTY_BLOCK); + assert.equal(read(".github/copilot-instructions.md"), EMPTY_BLOCK); + assert.equal(read(".kilocode/keep.md"), "legacy user content\n"); + assert.deepEqual(fs.readdirSync(path.join(root, ".kilocode")), ["keep.md"]); + }); +}); + +test("shared Kilo/Codex/OpenCode/Cursor selection is stable without rewriting on a rerun", () => { + inProject({ "AGENTS.md": EMPTY_BLOCK }, ({ root, invoke, read }) => { + const args = ["kilo", "codex", "opencode", "cursor", "kilo"]; + const result = invoke(...args); + assert.equal(result.status, 0, result.stderr); + const once = read("AGENTS.md"); + const mtime = fs.statSync(path.join(root, "AGENTS.md"), { bigint: true }).mtimeNs; + assert.equal(invoke(...args).status, 0); + assert.equal(read("AGENTS.md"), once); + assert.equal(fs.statSync(path.join(root, "AGENTS.md"), { bigint: true }).mtimeNs, mtime); + assert.equal(once.split(OPEN).length - 1, 1); + assert.equal(once.split("[aidd_docs/memory/architecture.md]").length - 1, 1); + }); +}); + +test("explicit sync validates a last malformed destination before writing earlier targets or README", () => { + const files = { + "CLAUDE.md": EMPTY_BLOCK, + "AGENTS.md": EMPTY_BLOCK, + ".github/copilot-instructions.md": `${OPEN}\n`, + "aidd_docs/memory/README.md": "# Memory\n\n\n", + }; + inProject(files, ({ invoke, read }) => { + const result = invoke("claude", "codex", "copilot"); + assert.equal(result.status, 1); + assert.match(result.stderr, /unpaired project memory marker/); + for (const [name, content] of Object.entries(files)) assert.equal(read(name), content, name); + }); +}); + +for (const [name, invalid] of Object.entries({ + duplicate: EMPTY_BLOCK + EMPTY_BLOCK, + nested: `${OPEN}\n${OPEN}\n${CLOSE}\n`, + reversed: `${CLOSE}\n${OPEN}\n`, + mixed: `\n${CLOSE}\n`, + separateFamilies: EMPTY_BLOCK + "\n\n", + extraClose: EMPTY_BLOCK + `${CLOSE}\n`, +})) { + test(`explicit sync refuses ${name} memory markers without changing any selected file`, () => { + inProject({ "CLAUDE.md": EMPTY_BLOCK, "AGENTS.md": invalid }, ({ invoke, read }) => { + const result = invoke("claude", "codex"); + assert.equal(result.status, 1, result.stderr); + assert.match(result.stderr, /memory marker/); + assert.equal(read("CLAUDE.md"), EMPTY_BLOCK); + assert.equal(read("AGENTS.md"), invalid); + }); + }); +} + +test("preflight accepts absent/blockless context files but performs no Upsert or Fill", () => { + inProject({ "AGENTS.md": "# User\nNo block yet.\n" }, ({ root, invoke, read }) => { + const result = invoke("--check", "claude", "codex", "copilot"); + assert.equal(result.status, 0, result.stderr); + assert.equal(read("AGENTS.md"), "# User\nNo block yet.\n"); + assert.equal(fs.existsSync(path.join(root, "CLAUDE.md")), false); + assert.equal(fs.existsSync(path.join(root, ".github")), false); + }); +}); + +test("preflight refuses malformed existing context before any missing target is created", () => { + inProject({ "AGENTS.md": `${OPEN}\n` }, ({ root, invoke, read }) => { + const result = invoke("--check", "claude", "codex"); + assert.equal(result.status, 1); + assert.equal(fs.existsSync(path.join(root, "CLAUDE.md")), false); + assert.equal(read("AGENTS.md"), `${OPEN}\n`); + }); +}); + +test("a malformed opted-in README prevents all explicit context writes", () => { + inProject({ "CLAUDE.md": EMPTY_BLOCK, "aidd_docs/memory/README.md": "\n" }, ({ invoke, read }) => { + const result = invoke("claude"); + assert.equal(result.status, 1, result.stderr); + assert.equal(read("CLAUDE.md"), EMPTY_BLOCK); + assert.equal(read("aidd_docs/memory/README.md"), "\n"); + }); +}); + +test("explicit sync ignores marker examples in prose and fences", () => { + const examples = `Quoted \`${OPEN}\` only.\n\n\`\`\`\`markdown\n\`\`\`\n${CLOSE}\n\`\`\`\n\`\`\`\`\n`; + inProject({ "AGENTS.md": examples }, ({ invoke, read }) => { + const result = invoke("codex"); + assert.equal(result.status, 0, result.stderr); + assert.equal(read("AGENTS.md"), examples); + }); +}); + +for (const [name, content] of Object.entries({ + quoted: `Quoted \`${OPEN}\` only.\n`, + fenced: `\`\`\`markdown\n${CLOSE}\n\`\`\`\n`, +})) { + test(`explicit sync ignores a lone ${name} marker example`, () => { + inProject({ "AGENTS.md": content }, ({ invoke, read }) => { + const result = invoke("codex"); + assert.equal(result.status, 0, result.stderr); + assert.equal(read("AGENTS.md"), content); + }); + }); +} + +test("explicit fill preserves BOM, CRLF, marker indentation and user bytes without a final newline", () => { + const prefix = "\ufeff# User é\r\nKeep\n "; + const suffix = ` ${CLOSE}\r\nUSER END`; + inProject({ "AGENTS.md": `${prefix}${OPEN}\r\nold\r\n${suffix}` }, ({ invoke, read }) => { + const result = invoke("codex"); + assert.equal(result.status, 0, result.stderr); + assert.equal(read("AGENTS.md"), `${prefix}${OPEN}\r\n\r\n[aidd_docs/memory/architecture.md](aidd_docs/memory/architecture.md)\r\n\r\n${suffix}`); + }); +}); + +for (const kind of ["target", "ancestor", "directory"]) { + test(`explicit sync refuses a ${kind} unsafe destination before writing a valid sibling`, () => { + inProject({ "CLAUDE.md": EMPTY_BLOCK, "outside.md": EMPTY_BLOCK }, ({ root, invoke, read }) => { + if (kind === "target") fs.symlinkSync(path.join(root, "outside.md"), path.join(root, "AGENTS.md")); + if (kind === "ancestor") { + fs.mkdirSync(path.join(root, "elsewhere")); + fs.writeFileSync(path.join(root, "elsewhere/copilot-instructions.md"), EMPTY_BLOCK); + fs.symlinkSync(path.join(root, "elsewhere"), path.join(root, ".github"), "dir"); + } + if (kind === "directory") fs.mkdirSync(path.join(root, "AGENTS.md")); + const result = invoke("claude", kind === "ancestor" ? "copilot" : "codex"); + assert.equal(result.status, 1, result.stderr); + assert.equal(read("CLAUDE.md"), EMPTY_BLOCK); + assert.equal(read("outside.md"), EMPTY_BLOCK); + }); + }); +} + +test("an invalid unselected context cannot prevent a selected tool from filling", () => { + inProject({ "AGENTS.md": `${OPEN}\n`, "CLAUDE.md": EMPTY_BLOCK }, ({ invoke, read }) => { + const result = invoke("claude"); + assert.equal(result.status, 0, result.stderr); + assert.match(read("CLAUDE.md"), /@aidd_docs\/memory\/architecture.md/); + assert.equal(read("AGENTS.md"), `${OPEN}\n`); + }); +}); + +test("the automatic hook remains best-effort when a later context has an unpaired marker", () => { + inProject({ "CLAUDE.md": EMPTY_BLOCK, "AGENTS.md": `${OPEN}\n` }, ({ invoke, read }) => { + const result = invoke(); + assert.equal(result.status, 0, result.stderr); + assert.match(read("CLAUDE.md"), /@aidd_docs\/memory\/architecture.md/); + assert.equal(read("AGENTS.md"), `${OPEN}\n`); + }); +}); + +test("a read-only last destination fails preflight before any earlier write", () => { + inProject({ "CLAUDE.md": EMPTY_BLOCK, "AGENTS.md": EMPTY_BLOCK }, ({ root, invoke, read }) => { + fs.chmodSync(path.join(root, "AGENTS.md"), 0o444); + const result = invoke("claude", "codex"); + assert.equal(result.status, 1, result.stderr); + assert.equal(read("CLAUDE.md"), EMPTY_BLOCK); + assert.equal(read("AGENTS.md"), EMPTY_BLOCK); + }); +}); + +test("a blockless README symlink is not an opted-in destination", () => { + inProject({ "CLAUDE.md": EMPTY_BLOCK, "notes.md": "# User notes\n" }, ({ root, invoke, read }) => { + fs.symlinkSync(path.join(root, "notes.md"), path.join(root, "aidd_docs/memory/README.md")); + const result = invoke("claude"); + assert.equal(result.status, 0, result.stderr); + assert.match(read("CLAUDE.md"), /@aidd_docs\/memory\/architecture\.md/); + assert.equal(read("notes.md"), "# User notes\n"); + }); +}); + +test("preflight refuses a denied creation parent without creating any context", () => { + inProject({ "AGENTS.md": "# User\n" }, ({ root, invoke, read }) => { + const parent = path.join(root, ".github"); + fs.mkdirSync(parent); + const preload = [ + "const fs = require('node:fs');", + "const { syncBuiltinESMExports } = require('node:module');", + `const denied = ${JSON.stringify(parent)};`, + "const original = fs.accessSync;", + "fs.accessSync = function (candidate, ...args) {", + " if (candidate === denied) { const error = new Error('injected access denial'); error.code = 'EACCES'; throw error; }", + " return original.call(this, candidate, ...args);", + "}; syncBuiltinESMExports();", + ].join("\n"); + const result = invoke.withRace(preload, ["--check", "claude", "copilot"]); + assert.equal(result.status, 1, result.stderr); + assert.equal(fs.existsSync(path.join(root, "CLAUDE.md")), false); + assert.equal(fs.existsSync(path.join(parent, "copilot-instructions.md")), false); + assert.equal(read("AGENTS.md"), "# User\n"); + }); +}); + +test("an unsafe opted-in README refuses sync before any context write", () => { + inProject({ "CLAUDE.md": EMPTY_BLOCK, "notes.md": "\n\n" }, ({ root, invoke, read }) => { + fs.symlinkSync(path.join(root, "notes.md"), path.join(root, "aidd_docs/memory/README.md")); + const result = invoke("claude"); + assert.equal(result.status, 1, result.stderr); + assert.equal(read("CLAUDE.md"), EMPTY_BLOCK); + assert.equal(read("notes.md"), "\n\n"); + }); +}); + +test("descriptor writes retain the existing inode and reported mode", () => { + inProject({ "AGENTS.md": EMPTY_BLOCK, "outside.md": EMPTY_BLOCK }, ({ root, invoke, read }) => { + const target = path.join(root, "AGENTS.md"); + fs.chmodSync(target, 0o640); + const before = fs.statSync(target, { bigint: true }); + const reportedMode = before.mode & 0o777n; + if (process.platform !== "win32") assert.equal(reportedMode, 0o640n); + const result = invoke("codex"); + assert.equal(result.status, 0, result.stderr); + const after = fs.statSync(target, { bigint: true }); + assert.equal(after.ino, before.ino); + assert.equal(after.mode & 0o777n, reportedMode); + assert.match(read("AGENTS.md"), /aidd_docs\/memory\/architecture\.md/u); + }); +}); + +test("descriptor writes refuse hard-linked targets without changing any linked witness", () => { + const privateRoot = fs.mkdtempSync(path.join(os.tmpdir(), "update-memory-hardlink-")); + let targetWitness; + let hardlinkWitness; + let outsideWitness; + try { + const hardlink = path.join(privateRoot, "hardlink.md"); + const outsideHardlink = path.join(privateRoot, "outside-hardlink.md"); + inProject({ "AGENTS.md": EMPTY_BLOCK }, ({ root, invoke }) => { + const target = path.join(root, "AGENTS.md"); + fs.linkSync(target, hardlink); + fs.linkSync(target, outsideHardlink); + targetWitness = openWitness(target); + hardlinkWitness = openWitness(hardlink); + outsideWitness = openWitness(outsideHardlink); + assert.equal(targetWitness.stat.nlink, 3n); + assert.deepEqual(targetWitness.bytes, Buffer.from(EMPTY_BLOCK)); + const refused = invoke("codex"); + assert.equal(refused.status, 1, refused.stderr); + for (const witness of [targetWitness, hardlinkWitness, outsideWitness]) assertWitnessUnchanged(witness); + assertPathStillNamesWitness(target, targetWitness); + closeWitness(targetWitness); + closeWitness(hardlinkWitness); + closeWitness(outsideWitness); + targetWitness = undefined; + hardlinkWitness = undefined; + outsideWitness = undefined; + }); + } finally { + closeWitness(targetWitness); + closeWitness(hardlinkWitness); + closeWitness(outsideWitness); + fs.rmSync(privateRoot, { recursive: true, force: true }); + } +}); + +for (const mode of ["automatic", "explicit"]) { + test(`${mode} write failure before the first byte leaves the destination and outside witness unchanged`, () => { + const original = `USER PREFIX\n${EMPTY_BLOCK}USER SUFFIX\n`; + inProject({ "AGENTS.md": original }, ({ root, invoke, read }) => { + const outside = path.join(root, "outside.md"); + fs.writeFileSync(outside, "OUTSIDE_SENTINEL\n"); + const preload = [ + DESCRIPTOR_TRACKER, + "const fs = require('node:fs');", + "const { syncBuiltinESMExports } = require('node:module');", + "fs.writeSync = function () { const error = new Error('injected write failure'); error.code = 'EIO'; throw error; };", + "syncBuiltinESMExports();", + ].join("\n"); + const result = invoke.withRace(preload, mode === "explicit" ? ["codex"] : []); + assert.equal(result.status, 1); + assertTrackedDescriptorsClosed(result.stderr); + assert.equal(read("AGENTS.md"), original); + assert.equal(fs.readFileSync(outside, "utf8"), "OUTSIDE_SENTINEL\n"); + }); + }); +} + +for (const mode of ["automatic", "explicit"]) { + for (const replacement of ["file", "parent"]) { + test(`${mode} sync refuses a ${replacement} substitution after opening and preserves outside witnesses`, () => { + const contextPath = replacement === "file" ? "AGENTS.md" : ".github/copilot-instructions.md"; + const parentPath = path.dirname(contextPath); + const privateRoot = fs.mkdtempSync(path.join(os.tmpdir(), "update-memory-race-")); + const outside = path.join(privateRoot, "outside.md"); + const outsideDir = path.join(privateRoot, "outside-dir"); + const outsideFile = replacement === "file" ? outside : path.join(outsideDir, path.basename(contextPath)); + fs.writeFileSync(outside, EMPTY_BLOCK); + fs.mkdirSync(outsideDir); + fs.writeFileSync(path.join(outsideDir, path.basename(contextPath)), EMPTY_BLOCK); + let outsideWitness; + let projectWitness; + try { + outsideWitness = openWitness(outsideFile); + inProject({ [contextPath]: EMPTY_BLOCK }, ({ root, invoke }) => { + const absoluteTarget = path.join(root, contextPath); + const absoluteParent = path.join(root, parentPath); + const replacementPath = replacement === "file" ? outside : outsideDir; + const oldPath = replacement === "file" ? `${absoluteTarget}.held` : `${absoluteParent}.held`; + const windowsParentCheck = process.platform === "win32" && replacement === "parent"; + projectWitness = openWitness(absoluteTarget); + let injectedIdentity; + if (windowsParentCheck) { + const changedPath = absoluteParent; + injectedIdentity = [ + "const fs = require('node:fs');", + "const { syncBuiltinESMExports } = require('node:module');", + "const originalOpen = fs.openSync; const originalLstat = fs.lstatSync; let opened = false; let injected = false;", + `const target = ${JSON.stringify(absoluteTarget)};`, + `const changedPath = ${JSON.stringify(changedPath)};`, + "fs.openSync = function (candidate, ...args) { const fd = originalOpen.call(this, candidate, ...args); if (candidate === target) opened = true; return fd; };", + "fs.lstatSync = function (candidate, ...args) {", + " const stat = originalLstat.call(this, candidate, ...args);", + " if (opened && !injected && candidate === changedPath) {", + " if (typeof stat.ino !== 'bigint') throw new Error('identity stat did not request BigInt');", + " const changedIno = stat.ino + 1n;", + " if (changedIno === stat.ino) throw new Error('injected identity did not change');", + " injected = true; process.stderr.write('TEST_IDENTITY_CHANGE_INJECTED\\n');", + " return { dev: stat.dev, ino: changedIno, isSymbolicLink: () => stat.isSymbolicLink(), isFile: () => stat.isFile(), isDirectory: () => stat.isDirectory() };", + " }", + " return stat;", + "}; syncBuiltinESMExports();", + ].join("\n"); + } else { + injectedIdentity = [ + "const fs = require('node:fs');", + "const { syncBuiltinESMExports } = require('node:module');", + "const original = fs.openSync; let done = false;", + `const target = ${JSON.stringify(absoluteTarget)};`, + `const parent = ${JSON.stringify(absoluteParent)};`, + `const outside = ${JSON.stringify(replacementPath)};`, + `const held = ${JSON.stringify(oldPath)};`, + "fs.openSync = function (candidate, ...args) {", + " const fd = original.call(this, candidate, ...args);", + " if (!done && candidate === target) { done = true;", + ` fs.renameSync(${replacement === "file" ? "target" : "parent"}, held);`, + ` fs.symlinkSync(outside, ${replacement === "file" ? "target" : "parent"}, ${replacement === "file" ? "undefined" : "'dir'"});`, + " process.stderr.write('TEST_SUBSTITUTION_APPLIED\\n');", + " } return fd; }; syncBuiltinESMExports();", + ].join("\n"); + } + const result = invoke.withRace(injectedIdentity, mode === "explicit" ? [contextPath === "AGENTS.md" ? "codex" : "copilot"] : []); + assert.equal(result.status, mode === "explicit" ? 1 : 0, result.stderr); + assert.match(result.stderr, /unsafe destination|path changed while opening/u); + assert.match(result.stderr, windowsParentCheck ? /TEST_IDENTITY_CHANGE_INJECTED/u : /TEST_SUBSTITUTION_APPLIED/u); + assertWitnessUnchanged(projectWitness); + assertWitnessUnchanged(outsideWitness); + closeWitness(projectWitness); + closeWitness(outsideWitness); + projectWitness = undefined; + outsideWitness = undefined; + }); + } finally { + closeWitness(projectWitness); + closeWitness(outsideWitness); + fs.rmSync(privateRoot, { recursive: true, force: true }); + } + }); + } +} + +test("explicit sync distinguishes BigInt parent identities beyond Number precision", () => { + const contextPath = ".github/copilot-instructions.md"; + const privateRoot = fs.mkdtempSync(path.join(os.tmpdir(), "update-memory-bigint-identity-")); + const outside = path.join(privateRoot, "outside.md"); + const initialIno = BigInt(Number.MAX_SAFE_INTEGER) + 1n; + const changedIno = initialIno + 1n; + fs.writeFileSync(outside, EMPTY_BLOCK); + let outsideWitness; + let projectWitness; + try { + outsideWitness = openWitness(outside); + inProject({ [contextPath]: EMPTY_BLOCK }, ({ root, invoke }) => { + const target = path.join(root, contextPath); + const parent = path.dirname(target); + projectWitness = openWitness(target); + const preload = [ + "const fs = require('node:fs');", + "const { syncBuiltinESMExports } = require('node:module');", + "const originalOpen = fs.openSync; const originalLstat = fs.lstatSync; let opened = false; let injected = false;", + `const target = ${JSON.stringify(target)};`, + `const parent = ${JSON.stringify(parent)};`, + `const initialIno = ${initialIno}n; const changedIno = ${changedIno}n;`, + "fs.openSync = function (candidate, ...args) { const fd = originalOpen.call(this, candidate, ...args); if (candidate === target) opened = true; return fd; };", + "fs.lstatSync = function (candidate, ...args) {", + " const stat = originalLstat.call(this, candidate, ...args);", + " if (candidate !== parent) return stat;", + " const bigint = Boolean(args[0] && args[0].bigint);", + " const initial = bigint ? initialIno : Number(initialIno);", + " if (opened && !injected) {", + " injected = true;", + " const changed = bigint ? changedIno : Number(changedIno);", + " process.stderr.write('TEST_BIGINT_PARENT_IDENTITY_INJECTED\\n');", + " if (changed === initial) throw new Error('TEST_IDENTITY_CHANGE_NOT_DISTINCT');", + " return { dev: stat.dev, ino: changed, isSymbolicLink: () => stat.isSymbolicLink(), isFile: () => stat.isFile(), isDirectory: () => stat.isDirectory() };", + " }", + " return { dev: stat.dev, ino: initial, isSymbolicLink: () => stat.isSymbolicLink(), isFile: () => stat.isFile(), isDirectory: () => stat.isDirectory() };", + "}; syncBuiltinESMExports();", + ].join("\n"); + const result = invoke.withRace(preload, ["copilot"]); + assert.equal(Number(initialIno), Number(changedIno)); + assert.notEqual(initialIno, changedIno); + assert.equal(result.status, 1, result.stderr); + assert.match(result.stderr, /path changed while opening/u); + assert.match(result.stderr, /TEST_BIGINT_PARENT_IDENTITY_INJECTED/u); + assertWitnessUnchanged(projectWitness); + assertWitnessUnchanged(outsideWitness); + closeWitness(projectWitness); + projectWitness = undefined; + }); + } finally { + closeWitness(projectWitness); + closeWitness(outsideWitness); + fs.rmSync(privateRoot, { recursive: true, force: true }); + } +}); + + // The README sits inside the memory directory, so its links are relative to it. The path // is joined with the platform separator but the file paths are normalised to "/", so a // replace on the joined form silently matches nothing on Windows. diff --git a/scripts/skill-eval-snapshot.mjs b/scripts/skill-eval-snapshot.mjs new file mode 100644 index 000000000..917f86d55 --- /dev/null +++ b/scripts/skill-eval-snapshot.mjs @@ -0,0 +1,81 @@ +import { + closeSync, + constants, + fstatSync, + lstatSync, + openSync, + readFileSync, + readdirSync, +} from "node:fs"; +import { join } from "node:path"; + +function sameIdentity(left, right) { + return left.dev === right.dev && left.ino === right.ino; +} + +function sameFileState(left, right) { + return sameIdentity(left, right) && left.size === right.size && + left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs; +} + +function readStableFile(file) { + // O_NOFOLLOW is available on POSIX. On Windows, the post-open identity checks + // provide the fallback; the descriptor still pins the opened file for reads. + const flags = constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0); + const fd = openSync(file, flags); + try { + const openedState = fstatSync(fd, { bigint: true }); + if (!openedState.isFile()) { + throw new Error(`File changed while snapshotting: ${file}`); + } + + const contents = readFileSync(fd); + const finalState = fstatSync(fd, { bigint: true }); + const finalPathState = lstatSync(file, { bigint: true }); + if (!sameFileState(openedState, finalState) || !sameIdentity(finalState, finalPathState)) { + throw new Error(`File changed while snapshotting: ${file}`); + } + return `${finalState.mtimeNs}:${contents.toString("base64")}`; + } finally { + closeSync(fd); + } +} + +export function treeSnapshot(root) { + const snapshot = new Map(); + const walk = (dir, prefix = "") => { + const directoryState = lstatSync(dir, { bigint: true }); + if (!directoryState.isDirectory()) throw new Error(`Directory changed while snapshotting: ${dir}`); + + const entries = readdirSync(dir, { withFileTypes: true }).sort((left, right) => + left.name < right.name ? -1 : left.name > right.name ? 1 : 0, + ); + for (const entry of entries) { + const relative = prefix ? `${prefix}/${entry.name}` : entry.name; + if (relative === ".skill-eval-last-message.txt") continue; + const file = join(dir, entry.name); + if (entry.isDirectory()) { + const childState = lstatSync(file, { bigint: true }); + if (!childState.isDirectory()) throw new Error(`Directory changed while snapshotting: ${file}`); + snapshot.set(`${relative}/`, `directory:${Number(childState.mtimeNs) / 1e6}`); + walk(file, relative); + if (!sameIdentity(childState, lstatSync(file, { bigint: true }))) { + throw new Error(`Directory changed while snapshotting: ${file}`); + } + } else if (entry.isFile()) { + snapshot.set(relative, readStableFile(file)); + } else { + snapshot.set(relative, `other:${entry.isSymbolicLink()}`); + } + } + + // Node's portable path-based directory APIs do not expose a dirfd-relative + // walk. Rechecking identity catches ordinary directory replacement races; + // file bytes themselves are read through a pinned descriptor above. + if (!sameIdentity(directoryState, lstatSync(dir, { bigint: true }))) { + throw new Error(`Directory changed while snapshotting: ${dir}`); + } + }; + walk(root); + return snapshot; +} diff --git a/scripts/skill-eval.mjs b/scripts/skill-eval.mjs index fbd545ec2..47718246e 100644 --- a/scripts/skill-eval.mjs +++ b/scripts/skill-eval.mjs @@ -1,14 +1,14 @@ #!/usr/bin/env node -// Behavioral eval harness for the framework skills. Each case runs the skill for real -// through a headless `claude -p`, in an isolated temp project where the skill is installed -// under a unique name, so the worktree copy runs and never a globally-installed plugin of -// the same name. Local and opt-in: it needs an authenticated `claude` and spends tokens. +// Behavioral eval harness for framework skills. Each case runs the selected host CLI +// in an isolated temp project with a uniquely named worktree skill. Local and opt-in: +// the host needs authentication and each live case spends tokens. import { mkdtempSync, mkdirSync, writeFileSync, cpSync, rmSync, readFileSync, existsSync } from "node:fs"; import { tmpdir } from "node:os"; import { join, dirname, resolve } from "node:path"; import { spawn } from "node:child_process"; import { fileURLToPath } from "node:url"; +import { treeSnapshot } from "./skill-eval-snapshot.mjs"; const HERE = dirname(fileURLToPath(import.meta.url)); const ROOT = resolve(HERE, ".."); @@ -19,6 +19,11 @@ const args = process.argv.slice(2); const JUDGE = args.includes("--judge"); const KEEP = args.includes("--keep"); const MODEL = args.find((a) => a.startsWith("--model="))?.slice("--model=".length); +const HOST = args.find((a) => a.startsWith("--host="))?.slice("--host=".length) || "claude"; +if (!new Set(["claude", "codex"]).has(HOST)) { + console.error(`Unsupported host: ${HOST}. Use --host=claude or --host=codex.`); + process.exit(2); +} const JOBS = Number((args.find((a) => a.startsWith("--jobs=")) || "--jobs=4").split("=")[1]); const filter = args.find((a) => !a.startsWith("--")); const REPEAT = Number((args.find((a) => a.startsWith("--repeat=")) || "--repeat=1").split("=")[1]); @@ -34,20 +39,34 @@ if (cases.length === 0) { process.exit(2); } -function runClaude(prompt, cwd) { +function runHost(prompt, cwd, host = HOST) { return new Promise((ok, ko) => { - // --setting-sources project,local isolates the run from the user's global - // settings (hooks, output modes) so results are reproducible across machines. + const codexOutput = join(cwd, ".skill-eval-last-message.txt"); + const command = host === "codex" ? "codex" : "claude"; + // Codex uses the same project skill bytes in .agents/skills. Both hosts + // receive the isolated fixture as their working directory. + const commandArgs = host === "codex" + ? ["exec", "-C", cwd, "-s", "workspace-write", "--ephemeral", "--skip-git-repo-check", "-o", codexOutput, ...(MODEL ? ["-m", MODEL] : []), prompt] + : ["-p", prompt, ...(MODEL ? ["--model", MODEL] : []), "--setting-sources", "project,local", "--add-dir", cwd, "--dangerously-skip-permissions"]; const child = spawn( - "claude", - ["-p", prompt, ...(MODEL ? ["--model", MODEL] : []), "--setting-sources", "project,local", "--add-dir", cwd, "--dangerously-skip-permissions"], + command, + commandArgs, { cwd, timeout: 600000 }, ); let out = ""; child.stdout.on("data", (d) => (out += d)); child.stderr.on("data", (d) => (out += d)); child.on("error", ko); - child.on("close", () => ok(out)); + child.on("close", (code, signal) => { + if (code !== 0) return ko(new Error(`${command} exited ${code ?? signal}: ${out.trim().slice(-400)}`)); + if (host === "codex") { + if (!existsSync(codexOutput)) return ko(new Error("codex did not write its final response")); + const finalResponse = readFileSync(codexOutput, "utf8"); + if (!finalResponse.trim()) return ko(new Error("codex wrote an empty final response")); + return ok(finalResponse); + } + ok(out); + }); child.stdin.end(); }); } @@ -72,6 +91,17 @@ async function evaluate(expect, ctx) { const before = ctx.setup[name]; checks.push({ ok: before != null && fileText(name) === before, label: `file unchanged: ${name}` }); } + for (const name of expect.jsonValid || []) { + const text = fileText(name); + let ok = false; + try { + if (text != null) JSON.parse(text); + ok = text != null; + } catch { + ok = false; + } + checks.push({ ok, label: `valid JSON: ${name}` }); + } for (const [name, subs] of Object.entries(expect.fileContains || {})) { const text = fileText(name); for (const s of subs) checks.push({ ok: text != null && has(text, s), label: `${name} contains "${s}"` }); @@ -97,15 +127,20 @@ async function evaluate(expect, ctx) { if (expect.judge) { if (!JUDGE) { - checks.push({ ok: true, skipped: true, label: `judge (skipped, pass --judge): ${expect.judge}` }); + checks.push({ ok: null, skipped: true, label: `judge (skipped, pass --judge): ${expect.judge}` }); } else { const evidence = [ctx.stdout, ...(expect.judgeFiles || []).map((n) => fileText(n) || "")].join("\n\n"); - const verdict = await runClaude( + const verdict = await runHost( `You grade a test. Criterion: "${expect.judge}". Output under test follows between <<< and >>>.\n` + `Reply with exactly PASS or FAIL on the first line, then one line of reason.\n<<<\n${evidence}\n>>>`, ctx.tmp, + HOST, ); - checks.push({ ok: /^\s*pass\b/i.test(verdict), label: `judge: ${expect.judge}`, note: verdict.split("\n")[0].trim() }); + checks.push({ + ok: /^\s*pass\b/i.test(verdict), + label: `judge: ${expect.judge}`, + note: verdict.trim().split("\n").map((line) => line.trim()).join(" | "), + }); } } return checks; @@ -113,7 +148,7 @@ async function evaluate(expect, ctx) { function setupCase(c) { const tmp = mkdtempSync(join(tmpdir(), "skilleval-")); - const skillDst = join(tmp, ".claude", "skills", c.evalName); + const skillDst = join(tmp, HOST === "codex" ? ".agents" : ".claude", "skills", c.evalName); mkdirSync(skillDst, { recursive: true }); cpSync(join(skillsDir(c), c.skill), skillDst, { recursive: true }); // A skill may run a script its plugin bundles. Stage it next to the skill so @@ -137,17 +172,42 @@ async function runCase(c) { let tmp = null; let checks; let stdout = ""; + let beforeTree = null; try { tmp = setupCase(c); + if (c.expect.projectTreeUnchanged) beforeTree = treeSnapshot(tmp); const prompt = c.prompt.replaceAll("{{SKILL}}", c.evalName); - stdout = await runClaude(prompt, tmp); + stdout = await runHost(prompt, tmp); checks = await evaluate(c.expect, { tmp, stdout, setup: c.setup?.files || {} }); + if (c.expect.repeat) { + const before = treeSnapshot(tmp); + const repeatOutput = await runHost(prompt, tmp); + const after = treeSnapshot(tmp); + const changed = [...new Set([...before.keys(), ...after.keys()])].filter( + (name) => before.get(name) !== after.get(name), + ); + checks.push({ + ok: changed.length === 0, + label: `idempotent rerun${changed.length ? ` changed: ${changed.join(", ")}` : " left project bytes and mtimes unchanged"}`, + }); + if (!repeatOutput.trim()) checks.push({ ok: false, label: "idempotent rerun returned an empty response" }); + } } catch (err) { checks = [{ ok: false, label: `run error: ${err.message}` }]; } + if (beforeTree && tmp) { + const afterTree = treeSnapshot(tmp); + const changed = [...new Set([...beforeTree.keys(), ...afterTree.keys()])].filter( + (name) => beforeTree.get(name) !== afterTree.get(name), + ); + checks.push({ + ok: changed.length === 0, + label: `project tree ${changed.length ? `changed: ${changed.join(", ")}` : "unchanged"}`, + }); + } // A case that fails because the run itself failed looks identical to a case // that fails on the skill's behavior. Keep the tail so the two are separable. - const broke = checks.some((k) => !k.ok); + const broke = checks.some((k) => k.ok === false); if (broke) { const tail = stdout.trim().slice(-400).replace(/\s+/g, " "); checks.push({ ok: true, skipped: true, label: `run said: ${tail || "(nothing)"}` }); @@ -155,7 +215,9 @@ async function runCase(c) { if (!KEEP && tmp) rmSync(tmp, { recursive: true, force: true }); // Report as each case lands, in one write so parallel cases never interleave. // A long suite that is interrupted still leaves every finished case behind. - const lines = [`${broke ? "FAIL" : "PASS"} ${c.skill} :: ${c.name}`]; + const judgeSkipped = checks.some((k) => k.skipped && k.label.startsWith("judge (skipped")); + const status = broke ? "FAIL" : judgeSkipped ? "INCOMPLETE (judge skipped)" : "PASS"; + const lines = [`${status} ${c.skill} :: ${c.name}`]; for (const k of checks) { lines.push(` ${k.skipped ? "~" : k.ok ? "✓" : "✗"} ${k.label}${k.note ? ` (${k.note})` : ""}`); } @@ -176,9 +238,16 @@ async function runAll(list, jobs) { return out; } -console.log(`Running ${cases.length} case(s) on ${MODEL || "the host default model"}, ${JOBS} at a time${JUDGE ? ", with --judge" : ""}.\n`); +console.log(`Running ${cases.length} case(s) with ${HOST} on ${MODEL || "the host default model"}, ${JOBS} at a time${JUDGE ? ", with --judge" : ""}.\n`); const results = await runAll(cases, JOBS); -const failed = results.filter(({ checks }) => checks.some((k) => !k.ok)).length; - -console.log(`${cases.length - failed}/${cases.length} cases passed.`); +const failed = results.filter(({ checks }) => checks.some((k) => k.ok === false)).length; +const judgesSkipped = results.reduce((count, { checks }) => count + checks.filter((check) => + check.skipped && check.label.startsWith("judge (skipped") +).length, 0); +const incomplete = results.filter(({ checks }) => + checks.some((check) => check.skipped && check.label.startsWith("judge (skipped")) && + !checks.some((check) => check.ok === false), +).length; +const passed = cases.length - failed - incomplete; +console.log(`${passed}/${cases.length} cases fully passed; ${incomplete} incomplete (judge skipped); ${failed} failed; ${judgesSkipped} judge evaluation(s) skipped${JUDGE ? "" : " (pass --judge to run them)"}.`); process.exit(failed ? 1 : 0); diff --git a/scripts/skill-eval/README.md b/scripts/skill-eval/README.md index 5c7f7dac7..7c2280fb8 100644 --- a/scripts/skill-eval/README.md +++ b/scripts/skill-eval/README.md @@ -1,8 +1,8 @@ # skill-eval -Behavioral eval harness for the `aidd-refine` skills. Runs each skill for real -through a headless `claude -p` and asserts the outcomes its action `## Test` -blocks describe. +Behavioral eval harness for framework skills. Runs each skill through the +selected host CLI and checks the outcomes described by its action `## Test` +blocks. These cases are live agent evaluations, not offline unit tests. ## Run @@ -11,21 +11,34 @@ node scripts/skill-eval.mjs # every case, deterministic checks node scripts/skill-eval.mjs 03-shadow-areas # one skill node scripts/skill-eval.mjs --judge # also run LLM-judge criteria (metered) node scripts/skill-eval.mjs --keep # keep temp dirs to inspect +node scripts/skill-eval.mjs aidd-context --host=codex --case="Kilo native skill" ``` -Local and opt-in. Needs an authenticated `claude` CLI and spends tokens, so it -is not a CI gate. +Local and opt-in. The default `--host=claude` needs an authenticated Claude CLI. +`--host=codex` uses an authenticated Codex CLI and the same case contracts. Both +spend tokens, so neither is a CI gate. `--judge` runs each natural-language +criterion with the selected host. Without `--judge`, deterministic checks can +pass while the case is marked `INCOMPLETE (judge skipped)`; that is not a +successful judge evaluation. + +`PASS` means all deterministic checks and configured judges passed. `FAIL` +means at least one check failed. `INCOMPLETE (judge skipped)` means the +deterministic checks passed, but one or more configured judges did not run. The +process exits `1` if any case fails and `0` otherwise, including when cases are +incomplete. A zero exit code therefore does not prove every configured LLM +judgment ran. For a fully judged run, pass `--judge` and require every case to +fully pass, with zero incomplete and zero failed. No model override is passed unless you supply `--model=`. ## How it works Each case runs in a throwaway temp project. The skill under test is copied into -`.claude/skills//` and its frontmatter `name` is rewritten to a unique -`xeval-*` value. The unique name guarantees the worktree copy runs, never a -globally-installed plugin of the same name. The harness writes the case setup -files, runs `claude -p` in that project, then checks the written files and the -output. +`.claude/skills//` for Claude or `.agents/skills//` for +Codex. Its frontmatter `name` is rewritten to a unique `xeval-*` value, so the +worktree copy is selected. The harness writes case setup files, runs the host +in that project, then checks the written files and final response. A nonzero +host exit fails the case. `--keep` retains the temp project for inspection. ## Cases @@ -40,8 +53,11 @@ output. "prompt": "Use the {{SKILL}} skill on ./prd.md ...", "expect": { "filesExist": ["prd-shadow-report.md"], + "jsonValid": [".claude/settings.json"], + "fileMatches": { ".claude/settings.json": "SessionStart" }, "fileContains": { "prd-shadow-report.md": ["## Gaps by Category"] }, "fileNotContains": { "report.md": ["..."] }, + "projectTreeUnchanged": true, "stdoutContains": ["..."], "stdoutNotContains": ["tier 1"], "stdoutMatches": ["\\d+%"], @@ -51,15 +67,19 @@ output. } ``` -`{{SKILL}}` is replaced with the case `evalName`. Deterministic checks run -always; `judge` criteria run only with `--judge` and use a second `claude -p` -as grader, for outcomes that cannot be matched literally. +`{{SKILL}}` is replaced with the case `evalName`. `jsonValid` checks files parse +as JSON, `fileMatches` checks their text against regular expressions, and +`projectTreeUnchanged` checks that the run added, removed, or changed no project +files (the Codex final-response file is excluded). Deterministic checks run +always; `judge` criteria run only with `--judge` and use a second invocation of +the selected host. Its verdict and one-line reason are shown in the case output. ## Coverage and limits - Deterministic where possible (file written, filename rule, required sections, hedged unverified claim, forbidden mechanics absent). Fuzzy outcomes (claim-extraction quality, terseness) go to `--judge` and can flake. + Deterministic checks do not replace behavioral LLM judgments. - `01-brainstorm` is interactive (multi-turn Q&A) and is not covered here; a single headless turn cannot exercise its loop. diff --git a/scripts/skill-eval/cases.json b/scripts/skill-eval/cases.json index f0d73ab6e..e3611ebe4 100644 --- a/scripts/skill-eval/cases.json +++ b/scripts/skill-eval/cases.json @@ -140,7 +140,7 @@ "fileContains": { "aidd_docs/memory/ecosystem.md": [ "flowchart", - "·", + "\u00b7", "ependabot" ], "aidd_docs/GUIDELINES.md": [ @@ -318,5 +318,555 @@ ] } } + }, + { + "plugin": "aidd-context", + "skill": "02-project-memory", + "evalName": "xeval-memory", + "name": "Kilo-only sync shares root memory and preserves user content", + "setup": { + "files": { + "kilo.json": "{}\n", + "aidd_docs/memory/architecture.md": "# Architecture\n\n- Node\n", + "AGENTS.md": "# User context\n\nKEEP_USER_CONTEXT\n" + } + }, + "prompt": "Use the {{SKILL}} skill to sync the existing memory. Select only Kilo; approve adding its memory block and keeping the existing user text. Do not refresh or regenerate the bank. Do not wait for input.", + "expect": { + "filesExist": [ + "AGENTS.md" + ], + "fileContains": { + "AGENTS.md": [ + "KEEP_USER_CONTEXT", + "", + "[aidd_docs/memory/architecture.md](aidd_docs/memory/architecture.md)" + ] + }, + "filesUnchanged": [ + "kilo.json", + "aidd_docs/memory/architecture.md" + ], + "filesAbsent": [ + "CLAUDE.md", + ".github/copilot-instructions.md", + ".kilocode/AGENTS.md" + ] + } + }, + { + "plugin": "aidd-context", + "skill": "02-project-memory", + "evalName": "xeval-memory", + "name": "external project sync shares one AGENTS block across selected tools", + "setup": { + "files": { + "aidd_docs/memory/architecture.md": "# Architecture\n\n- Node\n", + "CLAUDE.md": "# Claude context\n\nKEEP_CLAUDE_CONTEXT\n", + "AGENTS.md": "# Shared context\n\nKEEP_SHARED_CONTEXT\n", + ".codex/config.toml": "", + ".opencode/opencode.json": "{}\n", + "kilo.json": "{}\n" + } + }, + "prompt": "Use the {{SKILL}} skill to sync the existing memory from this external project into Claude, Codex, OpenCode and Kilo. All four selections and adding their memory blocks are approved. Keep user text, do not refresh the bank, and do not wait for input.", + "expect": { + "fileContains": { + "CLAUDE.md": ["KEEP_CLAUDE_CONTEXT", "@aidd_docs/memory/architecture.md"], + "AGENTS.md": ["KEEP_SHARED_CONTEXT", "[aidd_docs/memory/architecture.md](aidd_docs/memory/architecture.md)"] + }, + "fileMatches": { + "AGENTS.md": "^(?:(?!)[\\s\\S])*(?:(?!)[\\s\\S])*(?:(?!)[\\s\\S])*$" + }, + "filesUnchanged": ["aidd_docs/memory/architecture.md", ".codex/config.toml", ".opencode/opencode.json", "kilo.json"], + "filesAbsent": [".github/copilot-instructions.md", ".kilocode/AGENTS.md"] + } + }, + { + "plugin": "aidd-context", + "skill": "02-project-memory", + "evalName": "xeval-memory", + "name": "sync preflight refuses before creating an earlier context", + "setup": { + "files": { + "kilo.json": "{}\n", + "aidd_docs/memory/architecture.md": "# Architecture\n\n- Node\n", + "AGENTS.md": "# User context\n\n" + } + }, + "prompt": "Use the {{SKILL}} skill to sync the existing memory into Claude and Kilo. Both selections are approved. Do not repair malformed markers; refuse unsafe synchronization. Do not wait for input.", + "expect": { + "filesAbsent": [ + "CLAUDE.md" + ], + "filesUnchanged": [ + "AGENTS.md", + "kilo.json", + "aidd_docs/memory/architecture.md" + ], + "judge": "The sync stops at preflight, reports the malformed AGENTS.md block, and creates or changes no context file." + } + }, + { + "plugin": "aidd-context", + "skill": "04-skill-generate", + "evalName": "xeval-kilo-skill", + "name": "Kilo native skill uses one canonical tree", + "setup": { + "files": { + "kilo.json": "{}\n" + } + }, + "prompt": "Use the {{SKILL}} skill. Create verify-payload with a single verify action that reads assets/payload.txt and replies with its content. Name, purpose, action and test are confirmed. Select Kilo native only; do not select portable. After validating it, repeat the same request once and verify nothing changed. Do not wait for input.", + "expect": { + "filesExist": [ + ".kilo/skills/verify-payload/SKILL.md", + ".kilo/skills/verify-payload/actions/01-verify.md" + ], + "filesAbsent": [ + ".agents/skills/verify-payload/SKILL.md" + ], + "fileContains": { + ".kilo/skills/verify-payload/SKILL.md": [ + "name: verify-payload", + "description:" + ] + }, + "fileNotContains": { + ".kilo/skills/verify-payload/SKILL.md": [ + "argument-hint:", + "allowed-tools:" + ] + }, + "repeat": true + } + }, + { + "plugin": "aidd-context", + "skill": "04-skill-generate", + "evalName": "xeval-kilo-skill", + "name": "Kilo portable choice shares exactly one tree with Codex", + "setup": { + "files": { + "kilo.json": "{}\n", + ".codex/config.toml": "# fixture\n" + } + }, + "prompt": "Use the {{SKILL}} skill. Create verify-payload with one verify action that replies VERIFIED. Select Kilo portable and Codex explicitly. Name, purpose, action and test confirmed. I explicitly approve .agents/skills placement. Complete the flow without waiting for input.", + "expect": { + "filesExist": [ + ".agents/skills/verify-payload/SKILL.md" + ], + "filesAbsent": [ + ".kilo/skills/verify-payload/SKILL.md" + ], + "fileContains": { + ".agents/skills/verify-payload/SKILL.md": [ + "name: verify-payload", + "description:" + ] + } + } + }, + { + "plugin": "aidd-context", + "skill": "04-skill-generate", + "evalName": "xeval-kilo-skill", + "name": "Kilo cannot choose portable without agreement", + "setup": { + "files": { + "kilo.json": "{}\n" + } + }, + "prompt": "Use the {{SKILL}} skill. Create verify-payload for Kilo but do not write until I choose a placement. I have not approved native or portable placement. Do not infer consent or answer for me.", + "expect": { + "filesAbsent": [ + ".agents/skills/verify-payload/SKILL.md", + ".kilo/skills/verify-payload/SKILL.md" + ], + "filesUnchanged": [ + "kilo.json" + ], + "stdoutMatches": ["placement|choose|native|portable"], + "judge": "The response asks for an explicit placement choice and does not write the skill." + } + }, + { + "plugin": "aidd-context", + "skill": "04-skill-generate", + "evalName": "xeval-kilo-skill", + "name": "existing Kilo copies require resolution before any write", + "setup": { + "files": { + "kilo.json": "{}\n", + ".kilo/skills/verify-payload/SKILL.md": "# NATIVE_USER_COPY\n", + ".agents/skills/verify-payload/SKILL.md": "# PORTABLE_USER_COPY\n" + } + }, + "prompt": "Use the {{SKILL}} skill. Modify verify-payload for Kilo. Two user copies exist; I have not resolved the collision. Do not repair, delete or choose between copies without asking.", + "expect": { + "filesUnchanged": [ + "kilo.json", + ".kilo/skills/verify-payload/SKILL.md", + ".agents/skills/verify-payload/SKILL.md" + ], + "judge": "The generator requests a resolution and changes no copy." + } + }, + { + "plugin": "aidd-context", + "skill": "04-skill-generate", + "evalName": "xeval-kilo-skill", + "name": "unsafe late Kilo target leaves earlier Claude target absent", + "setup": { + "files": { + "kilo.json": "{}\n", + ".kilo/skills/verify-payload": "USER_FILE_NOT_DIRECTORY\n" + } + }, + "prompt": "Use the {{SKILL}} skill. Create verify-payload for Claude first and Kilo native second. Name, purpose, one verify action returning VERIFIED and its test are confirmed. Both targets approved. Do not replace conflicting user files; refuse the whole publication if any target is unsafe.", + "expect": { + "filesAbsent": [ + ".claude/skills/verify-payload/SKILL.md" + ], + "filesUnchanged": [ + "kilo.json", + ".kilo/skills/verify-payload" + ], + "stdoutMatches": ["refus|unsafe|conflict|target"], + "judge": "The preflight refuses the non-directory Kilo target before creating any Claude files." + } + }, + { + "plugin": "aidd-context", + "skill": "04-skill-generate", + "evalName": "xeval-kilo-skill", + "name": "shared portable fields need resolution before writing", + "setup": { + "files": { + "kilo.json": "{}\n", + ".codex/config.toml": "# fixture\n" + } + }, + "prompt": "Use the {{SKILL}} skill. Create verify-payload for Kilo portable and Codex on one shared destination, and emit license MIT in its frontmatter. Both targets and this requested field are explicit. If their field contracts conflict, ask me for a resolution; do not drop my requested field or answer for me.", + "expect": { + "filesAbsent": [ + ".agents/skills/verify-payload/SKILL.md", + ".kilo/skills/verify-payload/SKILL.md" + ], + "filesUnchanged": [ + "kilo.json", + ".codex/config.toml" + ], + "judge": "The generator requests a target or field resolution before writing because the shared Codex output strips optional Kilo fields." + } + }, + { + "plugin": "aidd-context", + "skill": "04-skill-generate", + "evalName": "xeval-kilo-skill", + "name": "Kilo native plus Codex requires one placement resolution", + "setup": { + "files": { + "kilo.json": "{}\n", + ".codex/config.toml": "# fixture\n" + } + }, + "prompt": "Use the {{SKILL}} skill. Create verify-payload for Kilo native and Codex. I have not approved portable placement or deselecting a target. If these targets would duplicate one skill across native and portable roots, ask me to resolve the placement before writing. Do not answer for me.", + "expect": { + "filesAbsent": [ + ".agents/skills/verify-payload/SKILL.md", + ".kilo/skills/verify-payload/SKILL.md" + ], + "filesUnchanged": [ + "kilo.json", + ".codex/config.toml" + ], + "judge": "The generator requests shared portable consent or a revised target selection, and publishes neither tree." + } + }, + { + "plugin": "aidd-context", + "skill": "08-hook-generate", + "evalName": "xeval-kilo-hook", + "name": "Kilo-only session start gives guidance without files", + "setup": { + "files": { + "kilo.json": "{}\n", + "USER.txt": "KEEP\n" + } + }, + "prompt": "Use the {{SKILL}} skill for Kilo only. The requested moment is session start; recommend the explicitly documented .kilo/plugin/ project path and cite session.created. Explicitly state that the docs do not confirm .kilo/plugins/ as a project path. Do not create a plugin or hook file.", + "expect": { + "filesAbsent": [ + ".kilo/hooks.json", + ".kilo/plugin/aidd-hook.ts", + ".kilo/plugins/aidd-hook.ts" + ], + "filesUnchanged": [ + "kilo.json", + "USER.txt" + ], + "projectTreeUnchanged": true, + "stdoutMatches": ["\\.kilo/plugin/", "session\\.created"], + "judge": "The response recommends the documented .kilo/plugin/ JS/TS project path, explicitly states that .kilo/plugins/ is not a confirmed project path, cites session.created as documented, and creates no file." + } + }, + { + "plugin": "aidd-context", + "skill": "08-hook-generate", + "evalName": "xeval-kilo-hook", + "name": "unproved Kilo moment has no invented mapping", + "setup": { + "files": { + "kilo.json": "{}\n" + } + }, + "prompt": "Use the {{SKILL}} skill for Kilo only. I need a hook exactly when an agent switches from planning to editing; keep that exact lifecycle meaning. If Kilo has no documented event for it, say so. No file writes.", + "expect": { + "filesUnchanged": [ + "kilo.json" + ], + "judge": "The exact event is reported unsupported without claiming another event is equivalent, and no hook config, script or plugin is generated." + } + }, + { + "plugin": "aidd-context", + "skill": "08-hook-generate", + "evalName": "xeval-kilo-hook", + "name": "invalid final mixed target stops before prior writes", + "setup": { + "files": { + "kilo.json": "{}\n", + ".github/hooks/aidd.json": "{\"version\":1,\"hooks\":{\"SessionStart\":\"INVALID\"}}\n" + } + }, + "prompt": "Use the {{SKILL}} skill for Kilo, Claude and Copilot. Session start, shared project scope, simple no-op action, no matcher, host project, and every target is confirmed. Preserve user files. Preflight all targets before writing, including Copilot last; if its config is invalid, stop the entire write.", + "expect": { + "filesAbsent": [ + ".claude/settings.json", + ".claude/hooks/aidd.sh", + ".kilo/hooks.json" + ], + "filesUnchanged": [ + "kilo.json", + ".github/hooks/aidd.json" + ], + "stdoutMatches": ["invalid|refus|preflight|target"], + "judge": "The invalid Copilot event list is reported before Claude script/config writes; Kilo guidance is separate and no Kilo file is produced." + } + }, + { + "plugin": "aidd-context", + "skill": "08-hook-generate", + "evalName": "xeval-kilo-hook", + "name": "existing exact AIDD entry and user duplicates remain unchanged", + "setup": { + "files": { + "kilo.json": "{}\n", + ".claude/settings.json": "{\"hooks\":{\"SessionStart\":[{\"hooks\":[{\"type\":\"command\",\"command\":\"bash ${CLAUDE_PROJECT_DIR}/hooks/user.sh\"}]},{\"hooks\":[{\"type\":\"command\",\"command\":\"bash ${CLAUDE_PROJECT_DIR}/hooks/user.sh\"}]},{\"hooks\":[{\"type\":\"command\",\"command\":\"bash ${CLAUDE_PROJECT_DIR}/hooks/aidd.sh\"}]}]}}\n", + "hooks/aidd.sh": "#!/usr/bin/env bash\ncat >/dev/null\nexit 0\n" + } + }, + "prompt": "Use the {{SKILL}} skill for Kilo and Claude. Repeat the existing SessionStart shared-project hook with exact command bash ${CLAUDE_PROJECT_DIR}/hooks/aidd.sh, no matcher. Both targets and scope are confirmed. The existing AIDD entry is exact; preserve its bytes and the two user duplicate entries. Give Kilo guidance separately.", + "expect": { + "filesUnchanged": [ + "kilo.json", + ".claude/settings.json", + "hooks/aidd.sh" + ], + "filesAbsent": [ + ".kilo/hooks.json" + ], + "judge": "The existing exact AIDD entry is not duplicated, both user entries remain and no file is rewritten; Kilo receives only guidance." + } + }, + { + "plugin": "aidd-context", + "skill": "08-hook-generate", + "evalName": "xeval-multi-tool-hook", + "name": "OpenCode-only session start gives plugin guidance without files", + "setup": { + "files": { + "opencode.json": "{}\n", + "USER.txt": "KEEP\n" + } + }, + "prompt": "Use the {{SKILL}} skill for OpenCode only. I want a project plugin reacting to session start. Give sourced guidance for a documented event and path; do not generate a plugin, hook, config entry or script. No declarative scope or file choice is needed.", + "expect": { + "filesAbsent": [".opencode/plugins/aidd-hook.ts", ".opencode/hooks.json", "hooks/aidd.sh"], + "filesUnchanged": ["opencode.json", "USER.txt"], + "projectTreeUnchanged": true, + "stdoutMatches": ["\\.opencode/plugins/", "session\\.created"], + "judge": "OpenCode receives sourced JS/TS plugin guidance with the documented session.created event. The full project tree stays unchanged and no declarative hook file or script is requested." + } + }, + { + "plugin": "aidd-context", + "skill": "08-hook-generate", + "evalName": "xeval-multi-tool-hook", + "name": "declarative session start keeps documented event names", + "setup": { "files": { "USER.txt": "KEEP\n" } }, + "prompt": "Use the {{SKILL}} skill for Claude, Codex, Cursor and Copilot. The requested moment is exactly session start, with a simple no-op handler, no matcher, shared project scope, host project mode; all four targets and this scope are confirmed. Use each documented declarative event and format. Write Copilot's entry to .github/hooks/aidd.json. Preserve USER.txt.", + "expect": { + "filesExist": [".claude/settings.json", ".codex/hooks.json", ".cursor/hooks.json", ".github/hooks/aidd.json"], + "jsonValid": [".claude/settings.json", ".codex/hooks.json", ".cursor/hooks.json", ".github/hooks/aidd.json"], + "fileMatches": { + ".claude/settings.json": "\\\"SessionStart\\\"[\\s\\S]*\\\"type\\\"\\s*:\\s*\\\"command\\\"", + ".codex/hooks.json": "\\\"SessionStart\\\"[\\s\\S]*\\\"type\\\"\\s*:\\s*\\\"command\\\"", + ".cursor/hooks.json": "\\\"version\\\"\\s*:\\s*1[\\s\\S]*\\\"sessionStart\\\"[\\s\\S]*\\\"command\\\"\\s*:", + ".github/hooks/aidd.json": "\\\"version\\\"\\s*:\\s*1[\\s\\S]*\\\"SessionStart\\\"[\\s\\S]*\\\"type\\\"\\s*:\\s*\\\"command\\\"" + }, + "fileNotContains": { + ".claude/settings.json": ["\"matcher\""], + ".codex/hooks.json": ["\"matcher\""], + ".cursor/hooks.json": ["\"matcher\""], + ".github/hooks/aidd.json": ["\"matcher\""] + }, + "filesUnchanged": ["USER.txt"], + "judge": "Each selected declarative tool receives its documented session-start event and native entry shape; a single generated handler may serve multiple tools. User bytes survive." + } + }, + { + "plugin": "aidd-context", + "skill": "08-hook-generate", + "evalName": "xeval-multi-tool-hook", + "name": "declarative session start publishes supported non-Codex targets", + "setup": { "files": { "USER.txt": "KEEP\n" } }, + "prompt": "Use the {{SKILL}} skill for Claude, Cursor and Copilot. The requested moment is exactly session start, with a simple no-op handler, no matcher, shared project scope, host project mode; all three targets and this scope are confirmed. Use each documented declarative event and format. Write Copilot's entry to .github/hooks/aidd.json. Preserve USER.txt.", + "expect": { + "filesExist": [".claude/settings.json", ".cursor/hooks.json", ".github/hooks/aidd.json"], + "jsonValid": [".claude/settings.json", ".cursor/hooks.json", ".github/hooks/aidd.json"], + "fileMatches": { + ".claude/settings.json": "\\\"SessionStart\\\"[\\s\\S]*\\\"type\\\"\\s*:\\s*\\\"command\\\"", + ".cursor/hooks.json": "\\\"version\\\"\\s*:\\s*1[\\s\\S]*\\\"sessionStart\\\"[\\s\\S]*\\\"command\\\"\\s*:", + ".github/hooks/aidd.json": "\\\"version\\\"\\s*:\\s*1[\\s\\S]*\\\"SessionStart\\\"[\\s\\S]*\\\"type\\\"\\s*:\\s*\\\"command\\\"" + }, + "fileNotContains": { + ".claude/settings.json": ["\"matcher\""], + ".cursor/hooks.json": ["\"matcher\""], + ".github/hooks/aidd.json": ["\"matcher\""] + }, + "filesAbsent": [".codex/hooks.json"], + "filesUnchanged": ["USER.txt"], + "judge": "Claude, Cursor and Copilot receive their documented session-start events in valid native entries. No Codex target is written and user bytes survive." + } + }, + { + "plugin": "aidd-context", + "skill": "08-hook-generate", + "evalName": "xeval-multi-tool-hook", + "name": "unsupported exact moment skips every declarative target", + "setup": { "files": { "USER.txt": "KEEP\n" } }, + "prompt": "Use the {{SKILL}} skill for Claude, Codex, Cursor and Copilot. I require a hook exactly when an agent switches from planning to editing, not merely after a response or before a tool. Keep that exact meaning. If the documentation does not establish an equivalent event and its timing, do not publish for that target. No alternative moment is approved.", + "expect": { + "filesAbsent": [".claude/settings.json", ".codex/hooks.json", ".cursor/hooks.json", ".github/hooks/aidd.json", "hooks/aidd.sh"], + "filesUnchanged": ["USER.txt"], + "judge": "For each target without a documented equivalent of the exact planning-to-editing transition, the response explains the missing guarantee and publishes nothing. A nearby event is not silently substituted." + } + }, + { + "plugin": "aidd-context", + "skill": "08-hook-generate", + "evalName": "xeval-multi-tool-hook", + "name": "tool failure does not become Codex PostToolUse", + "setup": { "files": { "USER.txt": "KEEP\n" } }, + "prompt": "Use the {{SKILL}} skill for Codex only. I need a hook that fires after tool failures and never after successful tool calls. A nearby event that also fires on success is unacceptable. No different moment has been approved. Explain any documented limitation and do not publish an approximate hook.", + "expect": { + "filesAbsent": [".codex/hooks.json", "hooks/aidd.sh"], + "filesUnchanged": ["USER.txt"], + "judge": "Codex PostToolUse is rejected as an automatic equivalent because it observes success as well as failure. No Codex hook or handler is published; any broader alternative is described as different and requires confirmation." + } + }, + { + "plugin": "aidd-context", + "skill": "08-hook-generate", + "evalName": "xeval-multi-tool-hook", + "name": "plugin-only guidance and declarative hook stay separate", + "setup": { "files": { "kilo.json": "{}\n", "opencode.json": "{}\n", "USER.txt": "KEEP\n" } }, + "prompt": "Use the {{SKILL}} skill for OpenCode, Kilo and Claude. Session start is the exact requested moment. For Claude, the shared project scope, no matcher, simple no-op handler and host project mode are confirmed. Give sourced plugin guidance for OpenCode and Kilo, and publish only the documented Claude declarative hook. Do not write plugin-only files.", + "expect": { + "filesExist": [".claude/settings.json"], + "filesAbsent": [".kilo/hooks.json", ".kilo/plugin/aidd-hook.ts", ".opencode/plugins/aidd-hook.ts", ".opencode/hooks.json"], + "filesUnchanged": ["kilo.json", "opencode.json", "USER.txt"], + "judge": "Claude receives a valid SessionStart publication while OpenCode and Kilo receive separately sourced plugin guidance. No plugin-only file, config, or script is generated for either guidance target." + } + }, + { + "plugin": "aidd-context", + "skill": "06-agent-generate", + "evalName": "xeval-kilo-agent", + "name": "Kilo agent rerun preserves its destination and user files", + "setup": { + "files": { + "kilo.json": "{}\n", + "USER.txt": "KEEP\n" + } + }, + "prompt": "Use the {{SKILL}} skill to create a verify-agent subagent with one clear purpose for Kilo only. The name, quality score of 9, host-project mode and Kilo target are confirmed. When this same request is repeated, reuse the existing approved artifact without changing its destination or rewriting unchanged content. Do not wait for input.", + "expect": { + "filesExist": [".kilo/agents/verify-agent.md"], + "filesAbsent": [".kilocode/agents/verify-agent.md", ".claude/agents/verify-agent.md"], + "filesUnchanged": ["kilo.json", "USER.txt"], + "fileContains": { ".kilo/agents/verify-agent.md": ["description:", "mode: subagent"] }, + "repeat": true + } + }, + { + "plugin": "aidd-context", + "skill": "07-command-generate", + "evalName": "xeval-kilo-command", + "name": "Kilo workflow rerun preserves its destination and user files", + "setup": { + "files": { + "kilo.json": "{}\n", + "USER.txt": "KEEP\n" + } + }, + "prompt": "Use the {{SKILL}} skill to create a one-shot verify-workflow command for Kilo only. The name, root command location, host-project mode and Kilo target are confirmed. Give it one objective, no arguments and no optional frontmatter. When this same request is repeated, reuse the existing approved artifact without changing its destination or rewriting unchanged content. Do not wait for input.", + "expect": { + "filesExist": [".kilo/commands/verify-workflow.md"], + "filesAbsent": [".kilocode/commands/verify-workflow.md", ".claude/commands/verify-workflow.md"], + "filesUnchanged": ["kilo.json", "USER.txt"], + "repeat": true + } + }, + { + "plugin": "aidd-context", + "skill": "06-agent-generate", + "evalName": "xeval-kilo-agent", + "name": "last Kilo agent collision refuses before Claude write", + "setup": { + "files": { + ".kilo/agents/verify-agent.md": "USER AGENT\n", + "USER.txt": "KEEP\n" + } + }, + "prompt": "Use the {{SKILL}} skill to create a verify-agent subagent with a clear one-purpose role and no optional fields. The name verify-agent, a quality score of 9, host-project mode and targets Claude first and Kilo last are confirmed. This is create, not modify: the existing Kilo agent is user-owned and must not be overwritten. Preflight every selected target before any write. If the last target collides, refuse the complete publication without creating the earlier Claude agent.", + "expect": { + "filesAbsent": [".claude/agents/verify-agent.md"], + "filesUnchanged": [".kilo/agents/verify-agent.md", "USER.txt"], + "judge": "The existing user-owned Kilo agent is reported as a preflight collision. No selected target is written, including the earlier Claude agent; the user file remains unchanged." + } + }, + { + "plugin": "aidd-context", + "skill": "07-command-generate", + "evalName": "xeval-kilo-command", + "name": "last Kilo workflow collision refuses before Claude write", + "setup": { + "files": { + ".kilo/commands/verify-workflow.md": "USER WORKFLOW\n", + "USER.txt": "KEEP\n" + } + }, + "prompt": "Use the {{SKILL}} skill to create a one-shot verify-workflow command with one objective, no arguments and no optional frontmatter. The name verify-workflow, root command location, host-project mode and targets Claude first and Kilo last are confirmed. This is create, not modify: the existing Kilo workflow is user-owned and must not be overwritten. Preflight every selected target before any write. If the last target collides, refuse the complete publication without creating the earlier Claude command.", + "expect": { + "filesAbsent": [".claude/commands/verify-workflow.md"], + "filesUnchanged": [".kilo/commands/verify-workflow.md", "USER.txt"], + "projectTreeUnchanged": true, + "stdoutMatches": ["refus|unsafe|conflict|collision|preflight"], + "judge": "The existing user-owned Kilo workflow is reported as a preflight collision, and the earlier Claude command is not created." + } } ]