diff --git a/.rabbit/context.yaml b/.rabbit/context.yaml index 7e1527e..d5719ae 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:02:14Z + generated_at: 2026-05-13T00:55:54Z repo: name: dev.kit @@ -22,6 +22,7 @@ refs: - ./changes.md - ./Makefile - ./docs/references/command-surfaces.md + - ./docs/references/repo-design.md - ./src/configs/archetypes.yaml - ./src/configs/audit-rules.yaml - ./src/configs/context-config.yaml @@ -69,16 +70,14 @@ gaps: dependencies: - repo: udx/reusable-workflows kind: reusable workflow - resolved: true - archetype: workflow-repo + resolved: false used_by: - .github/workflows/context7-ops.yml - .github/workflows/npm-release-ops.yml - repo: udx/worker kind: manifest contract (deploy) - resolved: true + resolved: false declared_as: udx.io/worker-v1/deploy - archetype: manifest-repo used_by: - deploy.yml @@ -146,22 +145,20 @@ manifests: declared_as: udx.io/worker-v1/deploy source_repo: udx/worker used_by: - - .rabbit/dev.kit/lessons-dev.kit-2026-04-14.md - - .rabbit/dev.kit/lessons-dev.kit-2026-04-15.md - Makefile - docs/references/command-surfaces.md - docs/references/config-contract-surfaces.md + - docs/repo-contract-boundary.md - lib/modules/repo_factors.sh - src/configs/context-config.yaml - src/configs/detection-signals.yaml - tests/suite.sh evidence: - version: udx.io/worker-v1/deploy - - path reference: .rabbit/dev.kit/lessons-dev.kit-2026-04-14.md - - path reference: .rabbit/dev.kit/lessons-dev.kit-2026-04-15.md - path reference: Makefile - path reference: docs/references/command-surfaces.md - path reference: docs/references/config-contract-surfaces.md + - path reference: docs/repo-contract-boundary.md - path reference: lib/modules/repo_factors.sh - path reference: src/configs/context-config.yaml - path reference: src/configs/detection-signals.yaml diff --git a/lib/commands/repo.sh b/lib/commands/repo.sh index c2da90d..411bdeb 100644 --- a/lib/commands/repo.sh +++ b/lib/commands/repo.sh @@ -164,7 +164,11 @@ EOF write_status=$? if [ "$write_status" -ne 0 ]; then dev_kit_output_section "error" - dev_kit_output_list_item "Context write did not finish within the allowed time" + if [ "$write_status" -eq 124 ]; then + dev_kit_output_list_item "Context write did not finish within the allowed time" + else + dev_kit_output_list_item "Context write failed with exit status $write_status" + fi return "$write_status" fi fi diff --git a/lib/commands/uninstall.sh b/lib/commands/uninstall.sh index e265ba0..1b04628 100644 --- a/lib/commands/uninstall.sh +++ b/lib/commands/uninstall.sh @@ -8,6 +8,10 @@ dev_kit_cmd_uninstall() { local home="${DEV_KIT_HOME:-$HOME/.udx/dev.kit}" local yes_mode="false" local arg="" + local stdout_file="" + local stderr_file="" + local uninstall_status=0 + local uninstall_error="" shift || true if [ "$format" = "json" ]; then @@ -23,7 +27,38 @@ dev_kit_cmd_uninstall() { return 1 fi - "$REPO_DIR/bin/scripts/uninstall.sh" "$@" >/dev/null + stdout_file="$(mktemp "${TMPDIR:-/tmp}/dev-kit-uninstall-out.XXXXXX")" || { + printf '{ "command": "uninstall", "ok": false, "error": "failed to create temp output file" }\n' + return 1 + } + stderr_file="$(mktemp "${TMPDIR:-/tmp}/dev-kit-uninstall-err.XXXXXX")" || { + rm -f "$stdout_file" + printf '{ "command": "uninstall", "ok": false, "error": "failed to create temp error file" }\n' + return 1 + } + + set +e + "$REPO_DIR/bin/scripts/uninstall.sh" "$@" >"$stdout_file" 2>"$stderr_file" + uninstall_status=$? + set -e + + [ -s "$stderr_file" ] && cat "$stderr_file" >&2 + + if [ "$uninstall_status" -ne 0 ]; then + uninstall_error="$(awk 'NF { print; exit }' "$stderr_file")" + [ -n "$uninstall_error" ] || uninstall_error="$(awk 'NF { print; exit }' "$stdout_file")" + [ -n "$uninstall_error" ] || uninstall_error="uninstall failed" + rm -f "$stdout_file" "$stderr_file" + printf '{ "command": "uninstall", "ok": false, "error": "%s", "binary": "%s", "binary_removed": %s, "home": "%s", "home_removed": %s }\n' \ + "$(dev_kit_json_escape "$uninstall_error")" \ + "$(dev_kit_json_escape "$target")" \ + "$([ -e "$target" ] && printf 'false' || printf 'true')" \ + "$(dev_kit_json_escape "$home")" \ + "$([ -e "$home" ] && printf 'false' || printf 'true')" + return "$uninstall_status" + fi + + rm -f "$stdout_file" "$stderr_file" printf '{ "command": "uninstall", "ok": true, "binary": "%s", "binary_removed": %s, "home": "%s", "home_removed": %s }\n' \ "$(dev_kit_json_escape "$target")" \ "$([ -e "$target" ] && printf 'false' || printf 'true')" \ diff --git a/lib/modules/output.sh b/lib/modules/output.sh index 126eea2..baa8f35 100644 --- a/lib/modules/output.sh +++ b/lib/modules/output.sh @@ -125,6 +125,48 @@ dev_kit_spinner_notice() { printf ' - %s\n' "$message" >&2 } +dev_kit_process_descendants() { + local root_pid="$1" + + ps -eo pid=,ppid= | awk -v root="$root_pid" ' + { + pid = $1 + ppid = $2 + children[ppid] = children[ppid] " " pid + } + + function walk(node, list, count, idx) { + count = split(children[node], list, " ") + for (idx = 1; idx <= count; idx++) { + if (list[idx] == "") { + continue + } + walk(list[idx]) + print list[idx] + } + } + + END { + walk(root) + } + ' +} + +dev_kit_process_signal_tree() { + local signal="$1" + local root_pid="$2" + local child_pid="" + + while IFS= read -r child_pid; do + [ -n "$child_pid" ] || continue + kill "-${signal}" "$child_pid" 2>/dev/null || true + done </dev/null || true +} + dev_kit_run_guarded() { local label="$1" local soft_timeout="${2:-$DEV_KIT_PROGRESS_SOFT_TIMEOUT}" @@ -166,7 +208,11 @@ dev_kit_run_guarded() { fi if [ "$hard_timeout" -gt 0 ] && [ "$elapsed" -ge "$hard_timeout" ]; then - kill "$pid" 2>/dev/null || true + dev_kit_process_signal_tree TERM "$pid" + sleep 2 + if kill -0 "$pid" 2>/dev/null; then + dev_kit_process_signal_tree KILL "$pid" + fi wait "$pid" 2>/dev/null || true dev_kit_spinner_stop "" [ -s "$stdout_file" ] && cat "$stdout_file" diff --git a/lib/modules/repo_scaffold.sh b/lib/modules/repo_scaffold.sh index 1dc88f6..373215f 100644 --- a/lib/modules/repo_scaffold.sh +++ b/lib/modules/repo_scaffold.sh @@ -179,7 +179,7 @@ dev_kit_repo_is_contract_evidence_path() { local path="$1" case "$path" in - ""|.git/*|.rabbit/context.yaml|AGENTS.md|.udx/*|.claude/*|.copilot/*|.cursor/*) + ""|.git/*|.rabbit/context.yaml|.rabbit/dev.kit/*|AGENTS.md|.udx/*|.claude/*|.copilot/*|.cursor/*) return 1 ;; esac diff --git a/tests/suite.sh b/tests/suite.sh index 8cac642..7e1c4fc 100644 --- a/tests/suite.sh +++ b/tests/suite.sh @@ -51,6 +51,28 @@ should_run() { return 1 } +replace_in_file() { + local file_path="$1" + local before="$2" + local after="$3" + local tmp_file="" + + tmp_file="$(mktemp "${TMPDIR:-/tmp}/dev-kit-replace.XXXXXX")" || return 1 + awk -v before="$before" -v after="$after" ' + BEGIN { replaced = 0 } + { + line = $0 + pos = index(line, before) + if (pos > 0 && replaced == 0) { + line = substr(line, 1, pos - 1) after substr(line, pos + length(before)) + replaced = 1 + } + print line + } + END { exit(replaced == 0) } + ' "$file_path" >"$tmp_file" && mv "$tmp_file" "$file_path" +} + while [ "$#" -gt 0 ]; do case "$1" in --only) @@ -88,6 +110,14 @@ done < "$1"; wait "$child"' _ "$guard_child_pid_file" 2>&1 + )" + guard_group_status=$? + set -e + [ "$guard_group_status" -eq 124 ] || fail "guard: stops child process groups" + guard_child_pid="$(cat "$guard_child_pid_file")" + if kill -0 "$guard_child_pid" 2>/dev/null; then + kill "$guard_child_pid" 2>/dev/null || true + fail "guard: terminates child processes on timeout" + fi + pass "guard: terminates child processes on timeout" + assert_contains "$guard_group_output" "dev.kit timeout: guard child test exceeded 2s" "guard: reports child timeout" + cp -R "$DOCUMENTED_SHELL_REPO" "$HOME_ACTION_REPO" rm -rf "$HOME_ACTION_REPO/.dev-kit" rm -f "$HOME_ACTION_REPO/.rabbit/context.yaml" @@ -153,6 +201,20 @@ if should_run "core"; then assert_file_missing "$uninstall_bin_dir/dev.kit" "uninstall json: removes binary target" assert_file_missing "$uninstall_home_dir" "uninstall json: removes home target" + set +e + uninstall_failure_json="$( + REPO_DIR="$TEST_HOME/missing-repo" \ + DEV_KIT_BIN_DIR="$uninstall_bin_dir" \ + DEV_KIT_HOME="$uninstall_home_dir" \ + dev_kit_cmd_uninstall json --yes 2>/dev/null + )" + uninstall_failure_status=$? + set -e + [ "$uninstall_failure_status" -ne 0 ] || fail "uninstall json: fails when uninstall script fails" + pass "uninstall json: fails when uninstall script fails" + assert_contains "$uninstall_failure_json" "\"ok\": false" "uninstall json: reports failure" + assert_contains "$uninstall_failure_json" "\"error\":" "uninstall json: includes error message" + home_repeat_json="$(cd "$HOME_ACTION_REPO" && DEV_KIT_REPO_HARD_TIMEOUT=1 dev.kit --json)" assert_contains "$home_repeat_json" "\"context_status\": \"existing\"" "home: reuses existing context" assert_contains "$home_repeat_json" "\"context_reason\": null" "home: fresh context has no stale reason" @@ -166,8 +228,10 @@ if should_run "core"; then assert_contains "$home_repeat_text" "reference: docs/references/config-contract-surfaces.md" "home text: shows gap reference" assert_contains "$home_repeat_text" "repair: fix repo-owned gaps, then rerun dev.kit repo" "home text: prints repair loop next step" - perl -0pi -e 's/No repo-owned configuration contract was found in docs, manifests, or checked-in example files\./Add .env.example, .env.sample, or .env.template when repo configuration is required./' \ - "$HOME_ACTION_REPO/.rabbit/context.yaml" + replace_in_file \ + "$HOME_ACTION_REPO/.rabbit/context.yaml" \ + "No repo-owned configuration contract was found in docs, manifests, or checked-in example files." \ + "Add .env.example, .env.sample, or .env.template when repo configuration is required." stale_home_json="$(cd "$HOME_ACTION_REPO" && dev.kit --json)" assert_contains "$stale_home_json" "\"context_status\": \"stale\"" "home: marks outdated context as stale" @@ -196,9 +260,26 @@ if should_run "core"; then assert_contains "$repo_text" "[next]" "repo text: renders next section" assert_contains "$repo_text" "confirm whether to start the research-and-fix loop now" "repo text: confirms before repair loop" + set +e + repo_write_failure_output="$( + DEV_KIT_SPINNER_DISABLE=1 + dev_kit_context_yaml_write() { + printf 'boom\n' >&2 + return 42 + } + dev_kit_cmd_repo text "$DOCUMENTED_SHELL_REPO" 2>&1 + )" + repo_write_failure_status=$? + set -e + [ "$repo_write_failure_status" -eq 42 ] || fail "repo text: preserves non-timeout write failures" + pass "repo text: preserves non-timeout write failures" + assert_contains "$repo_write_failure_output" "Context write failed with exit status 42" "repo text: distinguishes non-timeout write failures" + assert_contains "$repo_write_failure_output" "boom" "repo text: preserves underlying write error" + self_repo_json="$(cd "$REPO_DIR" && dev.kit repo --json)" assert_not_contains "$self_repo_json" "\"repo\": \"udx/dev.kit\"" "repo: omits self dependency contracts" assert_not_contains "$(cat "$REPO_DIR/.rabbit/context.yaml")" "source_repo: udx/dev.kit" "repo: omits self source repo provenance" + assert_not_contains "$(cat "$REPO_DIR/.rabbit/context.yaml")" ".rabbit/dev.kit/" "repo: excludes generated rabbit evidence" cp -R "$SIMPLE_REPO" "$SIMPLE_ACTION_REPO" rm -rf "$SIMPLE_ACTION_REPO/.dev-kit"