From fd5823ecfc662b9ca5938ff0fe1f898bdb64c82b Mon Sep 17 00:00:00 2001 From: Dmytro Smirnov Date: Tue, 19 May 2026 21:06:19 +0300 Subject: [PATCH 1/7] Expose repo workflow contract --- .rabbit/context.yaml | 9 +- bin/dev-kit | 104 ++++++++++++++- bin/scripts/install.sh | 1 + changes.md | 3 + docs/context-coverage.md | 2 + docs/repo-contract-boundary.md | 17 +++ lib/commands/env.sh | 78 +++++++++++ lib/commands/repo.sh | 16 +++ lib/modules/repo_factors.sh | 126 +++++++++++++++++- lib/modules/repo_signals.sh | 3 + lib/modules/repo_workflows.sh | 120 ++++++++++++++++- src/templates/repo.json | 1 + .../infra_configs/staging/k8s-configmap.yaml | 2 + tests/suite.sh | 14 +- 14 files changed, 481 insertions(+), 15 deletions(-) create mode 100644 tests/fixtures/docker-repo/.rabbit/infra_configs/staging/k8s-configmap.yaml diff --git a/.rabbit/context.yaml b/.rabbit/context.yaml index d5719ae..cc40d38 100644 --- a/.rabbit/context.yaml +++ b/.rabbit/context.yaml @@ -6,7 +6,7 @@ generator: tool: dev.kit repo: https://github.com/udx/dev.kit version: 0.10.0 - generated_at: 2026-05-13T00:55:54Z + generated_at: 2026-05-19T17:49:21Z repo: name: dev.kit @@ -70,14 +70,16 @@ gaps: dependencies: - repo: udx/reusable-workflows kind: reusable workflow - resolved: false + resolved: true + archetype: workflow-repo used_by: - .github/workflows/context7-ops.yml - .github/workflows/npm-release-ops.yml - repo: udx/worker kind: manifest contract (deploy) - resolved: false + resolved: true declared_as: udx.io/worker-v1/deploy + archetype: manifest-repo used_by: - deploy.yml @@ -163,4 +165,3 @@ manifests: - path reference: src/configs/context-config.yaml - path reference: src/configs/detection-signals.yaml - path reference: tests/suite.sh - diff --git a/bin/dev-kit b/bin/dev-kit index 004bfa4..b1711de 100755 --- a/bin/dev-kit +++ b/bin/dev-kit @@ -309,6 +309,94 @@ dev_kit_home_context_reason() { fi } +dev_kit_home_repo_workflow_status() { + local repo_root="$1" + local context_status="$2" + local context_yaml_path="$3" + local gap_count=0 + + [ -n "$repo_root" ] || { + printf '%s' "unavailable" + return 0 + } + + case "$context_status" in + missing) + printf '%s' "needs_repo_context" + return 0 + ;; + stale) + printf '%s' "stale_context" + return 0 + ;; + esac + + if [ -n "$context_yaml_path" ] && [ -f "$context_yaml_path" ]; then + gap_count="$(dev_kit_context_section_item_count "$context_yaml_path" "gaps" '^ - factor:')" + else + gap_count="$(dev_kit_repo_gap_count "$repo_root")" + fi + + if [ "${gap_count:-0}" -gt 0 ]; then + printf '%s' "needs_repair" + return 0 + fi + + printf '%s' "ready" +} + +dev_kit_home_workflow_status() { + local repo_detected="$1" + local env_status="$2" + local repo_status="$3" + + if [ "$env_status" = "blocked" ]; then + printf '%s' "blocked" + return 0 + fi + + if [ "$repo_detected" != "true" ]; then + printf '%s' "workspace_only" + return 0 + fi + + case "$repo_status" in + needs_repo_context|stale_context) + printf '%s' "needs_repo_context" + ;; + needs_repair) + printf '%s' "needs_repair" + ;; + *) + printf '%s' "ready" + ;; + esac +} + +dev_kit_home_workflow_json() { + local repo_detected="$1" + local repo_root="$2" + local context_status="$3" + local context_yaml_path="$4" + local env_status="" + local repo_status="" + local workflow_status="" + local jobs_json="" + + env_status="$(dev_kit_env_workflow_status)" + repo_status="$(dev_kit_home_repo_workflow_status "$repo_root" "$context_status" "$context_yaml_path")" + workflow_status="$(dev_kit_home_workflow_status "$repo_detected" "$env_status" "$repo_status")" + jobs_json="$(dev_kit_env_workflow_job_json)" + + if [ "$repo_detected" = "true" ]; then + jobs_json="${jobs_json}, $(dev_kit_repo_workflow_job_json "$repo_root" "$repo_status" "write" "$context_status")" + fi + + printf '{ "id": "dev-kit", "label": "Normalize repo and environment", "status": "%s", "jobs": [%s] }' \ + "$(dev_kit_json_escape "$workflow_status")" \ + "$jobs_json" +} + dev_kit_run_home() { local format="${1:-text}" local state="installed" @@ -319,6 +407,9 @@ dev_kit_run_home() { local context_yaml_path="" local context_status="none" local context_reason="" + local env_workflow_status="" + local repo_workflow_status="" + local workflow_status="" repo_root="$(dev_kit_repo_root "$repo_dir")" if [ -n "$repo_root" ]; then @@ -344,11 +435,16 @@ dev_kit_run_home() { fi fi + env_workflow_status="$(dev_kit_env_workflow_status)" + repo_workflow_status="$(dev_kit_home_repo_workflow_status "$repo_root" "$context_status" "$context_yaml_path")" + workflow_status="$(dev_kit_home_workflow_status "$repo_detected" "$env_workflow_status" "$repo_workflow_status")" + if [ "$format" = "json" ]; then printf '{\n' printf ' "name": "dev.kit",\n' printf ' "home": "%s",\n' "$(dev_kit_json_escape "$DEV_KIT_HOME")" printf ' "state": "%s",\n' "$(dev_kit_json_escape "$state")" + printf ' "workflow": %s,\n' "$(dev_kit_home_workflow_json "$repo_detected" "$repo_root" "$context_status" "$context_yaml_path")" printf ' "workspace": {\n' printf ' "path": "%s",\n' "$(dev_kit_json_escape "$repo_dir")" printf ' "repo_detected": %s,\n' "$repo_detected" @@ -392,6 +488,13 @@ dev_kit_run_home() { # Text mode: print title immediately, then compute and print progressively dev_kit_output_title "dev.kit" + dev_kit_output_section "workflow" + dev_kit_output_row "status" "$workflow_status" + dev_kit_output_row "env" "$env_workflow_status" + if [ "$repo_detected" = "true" ]; then + dev_kit_output_row "repo" "$repo_workflow_status" + fi + # ── Environment: list tools by category with what they enable ─────────────── local _env_line _env_cat _env_val _prev_cat="" while IFS= read -r _env_line; do @@ -422,7 +525,6 @@ EOF context_yaml_path="$(dev_kit_context_yaml_path "$repo_root")" dev_kit_output_summary "${summary_name} • ${summary_archetype}" - dev_kit_output_section "context" dev_kit_output_row "path" "$context_yaml_path" dev_kit_output_row "status" "$context_status" diff --git a/bin/scripts/install.sh b/bin/scripts/install.sh index e96d05c..fbf2376 100755 --- a/bin/scripts/install.sh +++ b/bin/scripts/install.sh @@ -173,6 +173,7 @@ main() { find "$DEV_KIT_HOME/bin" -type f -exec chmod +x {} \; ln -sfn "$DEV_KIT_HOME/bin/dev-kit" "$target" + ln -sfn "$DEV_KIT_HOME/bin/dev-kit" "${DEV_KIT_BIN_DIR}/dev-kit" if command -v dev_kit_output_title >/dev/null 2>&1; then dev_kit_output_title "Installed dev.kit" diff --git a/changes.md b/changes.md index a495a50..f60500d 100644 --- a/changes.md +++ b/changes.md @@ -2,6 +2,9 @@ ### unreleased +- Expose repo-centric workflow status in `dev.kit`, `dev.kit env`, and `dev.kit repo` output so environment checks, context refresh, and gap repair appear as one repo-owned loop +- Clarify the repo contract boundary between deterministic structured parsing and interpreted prose/session intent + ### 0.10.0 - Remove `dev.kit agent` so `.rabbit/context.yaml` is the single generated repo contract diff --git a/docs/context-coverage.md b/docs/context-coverage.md index 8261cee..442a167 100644 --- a/docs/context-coverage.md +++ b/docs/context-coverage.md @@ -14,6 +14,8 @@ It should answer three questions: For the boundary between generated contract data and durable repo docs, see [Repo Contract Boundary](repo-contract-boundary.md). +Structured sources such as YAML, JSON, workflow refs, and manifest metadata should be parsed programmatically where possible. Prose sources such as Markdown docs, prompts, reviews, and session notes should be treated as interpreted intent unless they point to a concrete script, manifest, or command. + Typical sections include: - generator metadata diff --git a/docs/repo-contract-boundary.md b/docs/repo-contract-boundary.md index 98c1c6d..70db651 100644 --- a/docs/repo-contract-boundary.md +++ b/docs/repo-contract-boundary.md @@ -20,6 +20,21 @@ Use **scripts and manifests** for programmatic execution: - deploy manifests such as `deploy.yml` - checked-in config examples when they are part of the runnable contract +Use **structured refs** for deterministic parsing: + +- YAML and JSON manifests +- package and tool config files +- workflow references +- version, kind, and description metadata +- explicit command definitions + +Use **prose and session material** for interpreted intent: + +- Markdown docs and design notes +- issue, PR, and review discussions +- agent prompts and session summaries +- manual decisions that are not encoded in scripts yet + Use **`.rabbit/context.yaml`** for generated operational summary: - direct-read refs @@ -38,6 +53,8 @@ If a repo needs a compact generated summary for tooling or safe repo-scoped exec If a repo needs something to be executed safely by tools, keep that in a script or manifest rather than only in prose docs. +When both structured and prose sources exist, `dev.kit` should parse the structured source first and use prose to explain intent, repair guidance, or open decisions. That keeps automation deterministic while still preserving the human and agent context behind the repo design. + ## Repair loop 1. `dev.kit repo` detects a gap or weak contract diff --git a/lib/commands/env.sh b/lib/commands/env.sh index c4f4d68..3a0f839 100644 --- a/lib/commands/env.sh +++ b/lib/commands/env.sh @@ -2,6 +2,76 @@ # @description: Inspect environment tools and dev.kit usage config +dev_kit_env_workflow_status() { + local missing_tools="" + local disabled_tools="" + local disabled_credentials="" + + missing_tools="$(dev_kit_env_missing_base_tools)" + disabled_tools="$(dev_kit_env_config_list "disabled_tools")" + disabled_credentials="$(dev_kit_env_config_list "disabled_credentials")" + + if [ -n "$missing_tools" ]; then + printf '%s' "blocked" + return 0 + fi + + if [ -n "$disabled_tools" ] || [ -n "$disabled_credentials" ]; then + printf '%s' "customized" + return 0 + fi + + printf '%s' "ready" +} + +dev_kit_env_workflow_step_summaries() { + local config_path="" + + config_path="$(dev_kit_env_config_path)" + printf '%s\n' "Detect local tools and auth capabilities." + printf '%s\n' "Resolve repo-facing capabilities from the current machine." + if [ -f "$config_path" ]; then + printf '%s\n' "Review environment config overrides at ${config_path}." + else + printf '%s\n' "Create ${config_path} with dev.kit env --config when overrides are needed." + fi +} + +dev_kit_env_workflow_job_json() { + local config_path="" + local disabled_tools="" + local disabled_credentials="" + local missing_tools="" + local workflow_status="" + local config_status="ready" + + config_path="$(dev_kit_env_config_path)" + disabled_tools="$(dev_kit_env_config_list "disabled_tools")" + disabled_credentials="$(dev_kit_env_config_list "disabled_credentials")" + missing_tools="$(dev_kit_env_missing_base_tools)" + workflow_status="$(dev_kit_env_workflow_status)" + + if [ -n "$disabled_tools" ] || [ -n "$disabled_credentials" ]; then + config_status="customized" + fi + + printf '{ "id": "env", "label": "Resolve local environment", "command": "dev.kit env", "status": "%s", "steps": [' \ + "$(dev_kit_json_escape "$workflow_status")" + printf '{ "id": "detect_tools", "type": "inspect", "label": "Detect local tools", "status": "%s", "missing_required": %s, "tools": %s }, ' \ + "$(if [ -n "$missing_tools" ]; then printf 'blocked'; else printf 'ready'; fi)" \ + "$(printf '%s' "$missing_tools" | tr ' ' '\n' | awk 'NF' | dev_kit_lines_to_json_array)" \ + "$(dev_kit_env_tools_json)" + printf '{ "id": "resolve_capabilities", "type": "inspect", "label": "Resolve capabilities from the current machine", "status": "ready", "capabilities": %s }, ' \ + "$(dev_kit_global_context_capabilities_json)" + printf '{ "id": "review_config", "type": "read", "label": "Review environment config overrides", "status": "%s", "path": "%s", "exists": %s, "disabled_tools": %s, "disabled_credentials": %s }' \ + "$(dev_kit_json_escape "$config_status")" \ + "$(dev_kit_json_escape "$config_path")" \ + "$([ -f "$config_path" ] && printf 'true' || printf 'false')" \ + "$(printf '%s' "$disabled_tools" | dev_kit_lines_to_json_array)" \ + "$(printf '%s' "$disabled_credentials" | dev_kit_lines_to_json_array)" + printf '] }' +} + dev_kit_cmd_env() { local format="${1:-text}" local manage_config=0 @@ -35,6 +105,7 @@ dev_kit_cmd_env() { printf '{\n' printf ' "command": "env",\n' printf ' "home": "%s",\n' "$(dev_kit_json_escape "$DEV_KIT_HOME")" + printf ' "workflow": { "id": "dev-kit", "label": "Normalize repo and environment", "jobs": [%s] },\n' "$(dev_kit_env_workflow_job_json)" printf ' "tools": %s,\n' "$(dev_kit_env_tools_json)" printf ' "capabilities": %s,\n' "$(dev_kit_global_context_capabilities_json)" printf ' "config": {\n' @@ -49,6 +120,13 @@ dev_kit_cmd_env() { dev_kit_output_title "dev.kit env" + dev_kit_output_section "workflow" + dev_kit_output_row "job" "env" + dev_kit_output_row "status" "$(dev_kit_env_workflow_status)" + dev_kit_output_list_from_lines </dev/null || true)" gap_count="${gap_count:-0}" + workflow_status="$(dev_kit_repo_workflow_status "$repo_dir" "$gap_count")" actions_json="$(dev_kit_repo_actions_json "$gap_count")" # JSON mode: compute everything up front then emit template if [ "$format" = "json" ]; then if [ "$mode" = "write" ]; then dev_kit_context_yaml_write "$repo_dir" "$force_resolve" >/dev/null + workflow_context_status="current" + elif [ -f "$context_yaml_path" ]; then + workflow_context_status="existing" + else + workflow_context_status="missing" fi dev_kit_template_render "repo.json" \ "command=repo" \ @@ -93,6 +101,7 @@ dev_kit_cmd_repo() { "factors=$(dev_kit_repo_factor_summary_json "$repo_dir")" \ "gaps=$gaps_json" \ "actions=$actions_json" \ + "workflow={ \"id\": \"dev-kit\", \"label\": \"Normalize repo and environment\", \"jobs\": [$(dev_kit_repo_workflow_job_json "$repo_dir" "$workflow_status" "$mode" "$workflow_context_status" "$gap_count")] }" \ "context=$(dev_kit_json_escape "$context_yaml_path")" \ "dependencies=$(dev_kit_deps_json "$repo_dir")" \ "recommended_repos=$(dev_kit_repo_recommended_repos_json)" @@ -109,6 +118,13 @@ dev_kit_cmd_repo() { dev_kit_output_summary "${repo_name} • ${archetype} • mode: ${mode}" + dev_kit_output_section "workflow" + dev_kit_output_row "job" "repo" + dev_kit_output_row "status" "$workflow_status" + dev_kit_output_list_from_lines </dev/null || true)" + repo_slug="$(dev_kit_repo_current_slug "$repo_dir" "$repo_name" 2>/dev/null || true)" + real_repo_dir="$(cd "$repo_dir" 2>/dev/null && pwd)" + + [ "$real_repo_dir" = "$REPO_DIR" ] || { [ "$repo_slug" = "udx/dev.kit" ] && [ "$repo_name" = "dev.kit" ]; } +} + +dev_kit_repo_reference_doc_default() { + local repo_dir="$1" + local first_doc="" + + first_doc="$(dev_kit_repo_first_existing_signal "$repo_dir" "documentation_files" 2>/dev/null || true)" + if [ -n "$first_doc" ]; then + printf '%s' "$first_doc" + return 0 + fi + + if dev_kit_repo_has_dir "$repo_dir" "docs"; then + printf '%s' "docs/" + return 0 + fi + + printf '%s' "README.md" +} + +dev_kit_repo_dependency_reference_local() { + local repo_dir="$1" + local first_manifest="" + local first_workflow="" + + first_manifest="$(dev_kit_repo_contract_manifest_files "$repo_dir" | awk 'NF { print; exit }')" + if [ -n "$first_manifest" ]; then + printf '%s' "$first_manifest" + return 0 + fi + + first_workflow="$(dev_kit_repo_first_existing_signal "$repo_dir" "workflow_primary_files" 2>/dev/null || true)" + if [ -n "$first_workflow" ]; then + printf '%s' "$first_workflow" + return 0 + fi + + if dev_kit_has_file "$repo_dir" "deploy.yml"; then + printf '%s' "deploy.yml" + return 0 + fi + + dev_kit_repo_reference_doc_default "$repo_dir" +} + +dev_kit_repo_config_reference_local() { + local repo_dir="$1" + local runtime_config="" + local config_doc="" + + config_doc="$(dev_kit_repo_documented_env_var_sources "$repo_dir" | awk 'NF { print; exit }')" + if [ -n "$config_doc" ]; then + printf '%s' "$config_doc" + return 0 + fi + + runtime_config="$(dev_kit_repo_first_existing_signal "$repo_dir" "config_runtime_files" 2>/dev/null || true)" + if [ -n "$runtime_config" ]; then + printf '%s' "$runtime_config" + return 0 + fi + + if dev_kit_has_file "$repo_dir" ".env.example"; then + printf '%s' ".env.example" + return 0 + fi + + dev_kit_repo_reference_doc_default "$repo_dir" +} + +dev_kit_repo_pipeline_reference_local() { + local repo_dir="$1" + local verify_source="" + local first_workflow="" + + verify_source="$(dev_kit_repo_command_detection_result "$repo_dir" "verification" 2>/dev/null | cut -d'|' -f3)" + if [ -n "$verify_source" ]; then + printf '%s' "$verify_source" + return 0 + fi + + first_workflow="$(dev_kit_repo_first_existing_signal "$repo_dir" "workflow_primary_files" 2>/dev/null || true)" + if [ -n "$first_workflow" ]; then + printf '%s' "$first_workflow" + return 0 + fi + + if dev_kit_has_file "$repo_dir" "deploy.yml"; then + printf '%s' "deploy.yml" + return 0 + fi + + dev_kit_repo_reference_doc_default "$repo_dir" +} + dev_kit_repo_factor_reference() { + local repo_dir="" local factor="$1" local status="$2" - local maybe_status="$3" - if [ -n "${maybe_status:-}" ]; then + if [ "$#" -ge 3 ]; then + repo_dir="$1" factor="$2" status="$3" fi + if [ -n "$repo_dir" ] && ! dev_kit_repo_prefers_internal_references "$repo_dir"; then + case "${factor}:${status}" in + dependencies:partial|dependencies:missing) + dev_kit_repo_dependency_reference_local "$repo_dir" + ;; + config:partial|config:missing) + dev_kit_repo_config_reference_local "$repo_dir" + ;; + pipeline:partial|pipeline:missing) + dev_kit_repo_pipeline_reference_local "$repo_dir" + ;; + esac + return 0 + fi + case "${factor}:${status}" in dependencies:partial|dependencies:missing) printf '%s' "docs/references/dependency-contracts.md" diff --git a/lib/modules/repo_signals.sh b/lib/modules/repo_signals.sh index a99eeb8..a90fe02 100644 --- a/lib/modules/repo_signals.sh +++ b/lib/modules/repo_signals.sh @@ -539,6 +539,9 @@ dev_kit_repo_contract_manifest_refs() { while IFS= read -r manifest_rel; do [ -n "$manifest_rel" ] || continue case "$manifest_rel" in + .rabbit/*/*) + continue + ;; .rabbit/*.yml|.rabbit/*.yaml|*.yml|*.yaml) printf './%s\n' "$manifest_rel" ;; diff --git a/lib/modules/repo_workflows.sh b/lib/modules/repo_workflows.sh index 2fdb8af..55babe8 100644 --- a/lib/modules/repo_workflows.sh +++ b/lib/modules/repo_workflows.sh @@ -26,11 +26,39 @@ dev_kit_repo_entrypoint_source() { printf "%s" "$(printf '%s' "$result" | cut -d'|' -f3)" } +dev_kit_repo_gap_count() { + local repo_dir="$1" + + dev_kit_repo_factor_summary_json "$repo_dir" | jq -r ' + to_entries + | map(select(.value.status == "missing" or .value.status == "partial")) + | length + ' 2>/dev/null || printf '0' +} + +dev_kit_repo_workflow_status() { + local repo_dir="$1" + local gap_count="${2:-}" + + if [ -z "$gap_count" ]; then + gap_count="$(dev_kit_repo_gap_count "$repo_dir")" + fi + + if [ "${gap_count:-0}" -gt 0 ]; then + printf '%s' "needs_repair" + return 0 + fi + + printf '%s' "ready" +} + dev_kit_repo_workflow_steps() { local repo_dir="$1" local verify_cmd="" local build_cmd="" local run_cmd="" + local context_path="" + local gap_count=0 printf "read_repo|Read the highest-priority repo refs first|%s\n" "$(dev_kit_repo_priority_refs "$repo_dir" | dev_kit_lines_to_csv)" @@ -49,8 +77,49 @@ dev_kit_repo_workflow_steps() { printf "run|Use the canonical runtime command instead of ad hoc startup paths|%s\n" "$run_cmd" fi + context_path="$(dev_kit_context_yaml_path "$repo_dir")" + if [ -n "$context_path" ]; then + printf "read_context|Review the generated repo contract|%s\n" "${context_path#"${repo_dir}/"}" + fi + + gap_count="$(dev_kit_repo_gap_count "$repo_dir")" + if [ "${gap_count:-0}" -gt 0 ]; then + printf "confirm_repair|Confirm whether to start the repair loop|repo-owned gaps\n" + printf "repair_loop|Repair the strongest gap and rerun dev.kit repo|repo-owned gaps\n" + fi } +dev_kit_repo_workflow_step_summaries() { + local repo_dir="$1" + local line="" + local step_id="" + local command="" + + while IFS= read -r line; do + [ -n "$line" ] || continue + step_id="${line%%|*}" + command="${line##*|}" + case "$step_id" in + read_repo) + printf '%s\n' "Read the highest-priority repo refs." + ;; + verify|build|run) + printf '%s\n' "$command" + ;; + read_context) + printf '%s\n' "Review .rabbit/context.yaml." + ;; + confirm_repair) + printf '%s\n' "Confirm whether to start the repo repair loop." + ;; + repair_loop) + printf '%s\n' "Repair the strongest gap and rerun dev.kit repo." + ;; + esac + done <