Зачем это нужно
Блок кастомизации (уроки 16.11-16.15) оставляет практический вопрос: в проекте стоят ядро, два пресета, три расширения и локальные оверрайды - что фактически видит агент? Какой слой победил для plan-template.md, а какой скрыт? Почему команда появилась/исчезла после установки пресета?
Группа команд specify artifact - read-only-поверхность интроспекции над этим инвентарем. Если specify preset resolve отвечает «какой файл выигрывает для этого имени», то specify artifact отвечает «что существует вообще и каков полный стек композиции за этим» - включая built-in артефакты, которых ни один пресет не касался.
Основная идея
Артефакт - любая команда, шаблон, скрипт или хук, который Spec Kit выставляет в проекте, независимо от слоя-источника: built-in-ассеты, установленный пресет, расширение или project-local оверрайд в .specify/templates/overrides/. artifact list дает полный инвентарь со стеком композиции; artifact info - стек одного артефакта; artifact lookup - манифестную декларацию, стоящую за слоем.
Все субкоманды сейчас требуют --json (без него - exit 2 и usage в stderr, stdout пуст): текстовый рендер отложен, JSON-форма - единственный контракт.
artifact list
specify artifact list --json
Выводит полный инвентарь видимых артефактов - по строке на пару (kind, name) со стеком композиции. Именованные сортируются по типу (command, затем template, затем script) и имени; строки хуков - за ними, по событию и приоритету первой декларации:
[
{
"id": "command:speckit.specify",
"name": "speckit.specify",
"kind": "command",
"description": "Create or update the feature specification.",
"stack": [
{
"id": "command:speckit.specify",
"layer": null,
"sourceId": null,
"presetId": null,
"presetName": null,
"strategy": "replace",
"active": true,
"hidden": false,
"manifestPath": null,
"lookupId": null,
"sourcePath": null
}
]
}
]
| Поле | Описание |
|---|---|
id |
{kind}:{name} - сокращение, которое принимает artifact info |
name |
логическое имя (команды используют namespace speckit.<stem>) |
kind |
command, template, script или hook |
description |
описание от высшего по приоритету слоя, имеющего описание; иначе "" |
stack |
стек композиции, той же формы строки, что у artifact info |
Built-in артефакты присутствуют всегда, даже если их никто не перекрывает. Описания берутся от высшего слоя: пресет или project-оверрайд, скрывающий встроенную команду, репортит собственное описание, а не текст скрытого built-in. Skills (.github/skills/**/SKILL.md) исключены - это интеграционно-специфический вывод, не семейство поставляемых ассетов.
artifact info
specify artifact info <name> --json # --kind command|template|script|hook
<name> - голое имя (speckit.specify) или сокращение kind:name (command:speckit.specify); при обоих --kind обязан согласоваться. Пример - команда, перекрытая пресетом compliance:
{
"id": "command:speckit.specify",
"name": "speckit.specify",
"kind": "command",
"description": "Create or update the feature specification.",
"stack": [
{
"id": "command:speckit.specify",
"layer": "preset",
"sourceId": "compliance",
"presetId": "compliance",
"presetName": "Compliance Preset",
"strategy": "replace",
"active": true,
"hidden": false,
"manifestPath": ".specify/presets/compliance/preset.yml",
"lookupId": "preset:compliance:command:speckit.specify",
"sourcePath": ".github/skills/speckit-specify/SKILL.md"
},
{
"id": "command:speckit.specify",
"layer": null,
"active": false,
"hidden": true
}
]
}
Индекс 0 стека - выигравший слой; вторая строка - скрытый built-in.
Семантика стека
Для command/template/script стек упорядочен по приоритету резолюции: индекс 0 - победитель. Стеки хуков аддитивны, а не winner-based (см. ниже). Каждая строка описывает один вкладывающий слой:
| Поле | Описание |
|---|---|
id |
{kind}:{name} - source-агностический round-trip-ключ, одинаков во всех строках стека |
layer |
project, preset или extension; null для built-in |
sourceId |
ID установленного пресета/расширения для резолвера; null без происхождения |
presetId |
ID каталога пресет-пака; null на built-in/project/extension-строках |
presetName |
отображаемое имя пресета из манифеста, иначе pack id |
strategy |
replace, wrap, prepend, append или additive |
active |
активен ли слой; для именованных артефактов true только на индексе 0 |
hidden |
true, когда replace-слой с меньшим индексом вырезает этот слой из композиции |
manifestPath |
путь к декларирующему манифесту относительно проекта |
lookupId |
детерминированный {layer}:{sourceId}:{kind}:{name}; null для built-in |
sourcePath |
POSIX-путь к конкретному файлу слоя; null для built-in/синтетических |
active и hidden - независимые метки, не противоположности. active идентифицирует высший слой по порядку резолюции - но не валидирует, что содержимое слоя читается и компонуется (сохраняется диагностическое поведение preset resolve: цепочка слоев репортится, даже если композиция потом даст warning). Композиционные стратегии (wrap, prepend, append) удерживают нижние слои в выводе - неактивный слой не обязательно скрыт; hidden помечаются только слои под первым replace.
Provenance (происхождение) слоя: установленный пресет/расширение, манифест декларации и конкретный файл. Lookup-ID используют установленный ID - имя реестра/каталога, которое может отличаться от логического id внутри манифеста; это держит различные установленные каталоги раздельными даже при одинаковых логических ID в манифестах. Project-local оверрайды несут синтетический project:_:{kind}:{name}; built-in слои lookupId не имеют. lookupId - не round-trip-ключ: для artifact info используйте id.
artifact lookup
specify artifact lookup <lookupId> --json
Резолвит манифестный lookupId стека в валидированную декларацию пресета или расширения, которой пользуется Spec Kit. Ответ включает стабильный lookup-ID, координаты провайдера, пути манифеста и источника и эффективную декларацию под contribution - она отражает нормальную обработку манифеста (канонические имена команд расширений, инжектированные дефолты вроде пустых списков алиасов, строчные стратегии пресетов), а не дословный YAML.
Cross-reference остается внутри artifact-команд: формы вывода команд пресетов/расширений и манифестов не меняются. Contribution-только-по-конвенции и project-оверрайды не имеют исходной манифестной декларации - lookup вернет {"error": "unknown contribution <lookupId>"} с exit 1. У built-in строк lookupId нет вовсе.
Артефакты-хуки
Строки хуков проецируют декларации хуков расширений, включаемых стандартным резолвером. Round-trip-сокращение:
hook:{encodedEventName}:{encodedTargetCommand}
Оба вызова выбирают один хук-артефакт:
specify artifact info hook:before_specify:speckit.compliance.pre-check --json
specify artifact info before_specify:speckit.compliance.pre-check --kind hook --json
Компоненты события и целевой команды кодируются UTF-8 percent-encoding (URL-quoting): ASCII-буквы, цифры, -, ., _, ~ остаются литеральными, остальные байты кодируются. Кодировка ограничена компонентами хука в id/name/lookupId; сами манифесты и рантайм-биндинги не меняются. Пример - событие custom:after с целью /skill:speckit-test-ext-hello:
hook:custom%3Aafter:%2Fskill%3Aspeckit-test-ext-hello
Хук-строки добавляют три top-level-поля:
| Поле | Описание |
|---|---|
eventName |
событие, триггерирующее декларацию |
targetCommand |
команда, вызываемая декларацией |
registered |
true, когда хотя бы одна декларирующая строка стека имеет включенный рантайм-биндинг |
Каждая строка стека хука несет нормализованные priority и optional. Строки хуков всегда strategy: "additive" и hidden: false: несколько расширений могут декларировать одну пару событие-команда, и каждая включенная декларация остается активной. Сортировка - по приоритету хука с детерминированным порядком деклараций резолвера как tiebreaker; порядок описывает инвентарь деклараций и не обещает, что равноприоритетные хуки исполняются в том же порядке в рантайме.
Для хуков active отражает состояние регистрации из .specify/extensions.yml, а не вычисление условий: декларация активна, только если рантайм-конфигурация содержит включенный биндинг с теми же extension id, событием и командой; условия вычисляются позже, при исполнении хука. Отсутствующая/битая рантайм-конфигурация оставляет декларации видимыми с registered: false и active: false.
Декларированные-но-незарегистрированные хуки видны, пока расширение включено резолвером. Реестрово-выключенные расширения исключаются целиком - как и их прочие вклады; невалидные индивидуальные манифесты опускаются резолвером и диагностируются командами инспекции расширений.
Lookup-ID хуков: {layer}:{sourceId}:hook:{encodedEventName}:{encodedTargetCommand}, где sourceId - ID установленного провайдера. Provenance хуков ограничена слоями preset и extension - built-in/core-слоя у хуков не бывает; сегодня манифестный API открывает декларации расширений, поэтому строки используют слой extension, а preset зарезервирован грамматикой идентификаторов на будущее.
JSON-ошибки
При сбое stdout пуст; в stderr уходит одно-ключевой JSON-конверт, exit 1:
{ "error": "unknown artifact command:nope" }
| Сообщение | Причина |
|---|---|
not a Spec Kit project: no .specify/ directory found |
запуск вне инициализированного проекта |
unknown artifact <name> |
нет артефакта с таким именем (и типом, если задан) |
ambiguous artifact <name>: matches kinds [...] |
голое имя совпало в нескольких типах - перезапустите с --kind |
artifact resolution failed |
реестр расширений не читается или сбор стека слоев упал |
Exit 2 зарезервирован за usage-ошибками (отсутствие --json, невалидный --kind) - обычный текст в stderr вместо JSON-конверта.
Практика
- В проекте с пресетом и расширением выполните
specify artifact list --jsonи найдите артефакты со стеком длиннее одной строки: какой слойactive, какиеhidden? - Возьмите команду, перекрытую пресетом, и пройдите цепочку:
artifact info(стек) ->artifact lookupпоlookupIdпобедившего слоя (декларация манифеста) -> сверьте сmanifestPath. - Сравните поведение
hiddenу replace и wrap: сделайте wrap-пресет и убедитесь, что нижний слой не помеченhidden, хотя неactive.
Краткое резюме
specify artifact - read-only-интроспекция инвентаря проекта: полный список (kind, name) со стеками композиции, стек одного артефакта (индекс 0 - победитель; active и hidden независимы) и lookup от lookupId к манифестной декларации. Хуки аддитивны, их active отражает рантайм-регистрацию, а не условия. Контракт - только JSON (--json обязателен), ошибки - одно-ключевой конверт в stderr.
Главное из урока:
- built-in строки всегда в инвентаре, но без provenance (
layer,lookupId= null); hiddenставится только слоям под первымreplace; композиционные стратегии держат нижние слои видимыми;- lookupId использует установленный ID, который может отличаться от логического ID манифеста; round-trip-ключ -
id; - компоненты хуков в идентификаторах percent-кодируются;
activeне гарантирует успешную композицию содержимого - это диагностическая, не валидирующая метка.
Вопросы для самопроверки
- Чем вопрос
specify artifactотличается от вопросаspecify preset resolve? Когда какого достаточно? - Опишите поля строки стека:
layer,sourceId,strategy,active,hidden,lookupId,sourcePath. Что означаетnullв каждом? - Почему
activeиhidden- не противоположности? Приведите пример неактивного, но не скрытого слоя. - Чем стек хука отличается от стека команды? Что означает
registered: falseпри видимой декларации? - Почему lookupId строится на установленном ID, а не на логическом ID манифеста? К чему это приводит при одноименных манифестах?
- Какие ошибки дают exit 1 с JSON-конвертом, а какие exit 2 с текстом? Почему
--jsonобязателен?
Что дальше
Блок D завершен: все механизмы разобраны. Урок 16.17 переносит их в реальные условия - внедрение Spec Kit в существующую кодовую базу и организацию работы в монорепозитории.