diff --git a/.gitignore b/.gitignore index 9db5ffa..620b899 100644 --- a/.gitignore +++ b/.gitignore @@ -142,6 +142,7 @@ vite.config.ts.timestamp-* # Local APS install artifacts (source lives under scaffold/) aps-planning/ +.aps/context/ .claude/commands/ .claude/settings.local.json .claude/.aps-session-baseline diff --git a/bin/aps b/bin/aps index 6651f9b..cadc0bb 100755 --- a/bin/aps +++ b/bin/aps @@ -9,6 +9,9 @@ # aps lint [file|dir] Validate APS documents (default: plans/) # aps lint --json Output as JSON # aps next [module] Show the next ready work item +# aps start Mark a Ready work item as In Progress +# aps complete Mark an In Progress work item as Complete +# aps graph [module] Show work item dependency graph # aps --help Show this help # @@ -47,6 +50,9 @@ Usage: aps lint [file|dir] Validate APS documents aps lint --json Output results as JSON aps next [module] Show the next ready work item + aps start Mark a Ready work item as In Progress + aps complete Mark an In Progress work item as Complete + aps graph [module] Show work item dependency graph aps --help Show this help Options: @@ -67,6 +73,10 @@ Examples: aps lint . --json # Lint current dir, JSON output aps next # Show next ready item in plans/ aps next auth # Show next ready item in auth module + aps start AUTH-003 # Mark AUTH-003 as In Progress + aps complete AUTH-003 # Mark AUTH-003 Complete + aps complete AUTH-003 --learning "retry on 5xx" + aps graph auth # Show auth dependency graph EOF } @@ -94,6 +104,18 @@ main() { shift cmd_next "$@" ;; + start) + shift + cmd_start "$@" + ;; + complete) + shift + cmd_complete "$@" + ;; + graph) + shift + cmd_graph "$@" + ;; --help|-h|help|"") show_help exit 0 diff --git a/lib/orchestrate.sh b/lib/orchestrate.sh index c9c0482..0e79f21 100644 --- a/lib/orchestrate.sh +++ b/lib/orchestrate.sh @@ -7,6 +7,7 @@ declare -a ORCH_ITEM_IDS=() declare -a ORCH_ITEM_TITLES=() declare -a ORCH_ITEM_STATUSES=() declare -a ORCH_ITEM_DEPS=() +declare -a ORCH_ITEM_LINES=() declare -a ORCH_ITEM_MODULES=() declare -a ORCH_ITEM_FILES=() declare -A ORCH_MODULE_STATUSES=() @@ -16,6 +17,7 @@ orch_reset_state() { ORCH_ITEM_TITLES=() ORCH_ITEM_STATUSES=() ORCH_ITEM_DEPS=() + ORCH_ITEM_LINES=() ORCH_ITEM_MODULES=() ORCH_ITEM_FILES=() ORCH_MODULE_STATUSES=() @@ -55,6 +57,7 @@ orch_item_content() { awk -v start="$start_line" ' NR == start { found=1; next } + found && /^## / { exit } found && /^### / { exit } found { print } ' "$file" @@ -113,6 +116,7 @@ orch_load_index_modules() { orch_load_work_items() { local plan_root="$1" + local load_all="${2:-false}" local module_dir="$plan_root/modules" [[ -d "$module_dir" ]] || return 1 @@ -127,7 +131,9 @@ orch_load_work_items() { [[ -n "$module_id" ]] || module_id=$(basename "$file" .aps.md | tr '[:lower:]' '[:upper:]') ORCH_MODULE_STATUSES["$module_id"]="$module_status" - [[ "$module_status" == "Complete" || "$module_status" == "Draft" || "$module_status" == "Blocked" ]] && continue + if [[ "$load_all" != "true" ]]; then + [[ "$module_status" == "Complete" || "$module_status" == "Draft" || "$module_status" == "Blocked" ]] && continue + fi while IFS=: read -r line_num header; do [[ -n "$header" ]] || continue @@ -150,6 +156,7 @@ orch_load_work_items() { ORCH_ITEM_TITLES+=("$title") ORCH_ITEM_STATUSES+=("$status") ORCH_ITEM_DEPS+=("$deps") + ORCH_ITEM_LINES+=("$line_num") ORCH_ITEM_MODULES+=("$module_id") ORCH_ITEM_FILES+=("$file") done <<< "$(get_work_items "$file")" @@ -212,6 +219,91 @@ orch_deps_display() { echo "${deps:-None}" } +orch_dep_ids() { + local deps="$1" + + printf '%s\n' "$deps" | grep -oE '[A-Z]+-[0-9]+|[A-Z]{2,}' || true +} + +orch_context_root() { + local plan_root="$1" + local parent + + parent=$(dirname "$plan_root") + printf '%s/.aps/context' "$parent" +} + +orch_emit_section() { + local file="$1" + local section="$2" + + awk -v section="$section" ' + $0 == "## " section { found=1; print; next } + found && /^## / { exit } + found { print } + ' "$file" +} + +orch_context_package() { + local plan_root="$1" + local idx="$2" + local id="${ORCH_ITEM_IDS[$idx]}" + local title="${ORCH_ITEM_TITLES[$idx]}" + local file="${ORCH_ITEM_FILES[$idx]}" + local line="${ORCH_ITEM_LINES[$idx]}" + local deps="${ORCH_ITEM_DEPS[$idx]}" + local context_dir context_file dep dep_idx related_files + + context_dir=$(orch_context_root "$plan_root") + mkdir -p "$context_dir" || { error "Cannot create context directory: $context_dir"; return 1; } + context_file="$context_dir/$id.md" + related_files=$(orch_field_value "$(orch_item_content "$file" "$line")" "Files") + + { + echo "# Context: $id - $title" + echo + echo "## Work Item" + orch_item_content "$file" "$line" + echo + echo "## Module Scope" + orch_emit_section "$file" "Purpose" + echo + orch_emit_section "$file" "In Scope" + echo + orch_emit_section "$file" "Out of Scope" + echo + orch_emit_section "$file" "Interfaces" + echo + echo "## Decisions" + orch_emit_section "$file" "Decisions" || true + echo + echo "## Dependency Learnings" + local found_learning="false" + while IFS= read -r dep; do + [[ -n "$dep" ]] || continue + dep_idx=$(orch_item_index "$dep" || true) + [[ -n "$dep_idx" ]] || continue + local dep_content dep_learning + dep_content=$(orch_item_content "${ORCH_ITEM_FILES[$dep_idx]}" "${ORCH_ITEM_LINES[$dep_idx]}") + dep_learning=$(orch_field_value "$dep_content" "Learning") + if [[ -n "$dep_learning" ]]; then + echo "- $dep: $dep_learning" + found_learning="true" + fi + done < <(orch_dep_ids "$deps") + [[ "$found_learning" == "true" ]] || echo "- None" + echo + echo "## Related Files" + if [[ -n "$related_files" ]]; then + printf '%s\n' "$related_files" | sed 's/^/- /' + else + echo "- None specified" + fi + } > "$context_file" + + printf '%s' "$context_file" +} + cmd_next() { local plan_root="plans" local module_filter="" @@ -256,7 +348,7 @@ EOF orch_reset_state orch_load_index_modules "$plan_root" - orch_load_work_items "$plan_root" || { + orch_load_work_items "$plan_root" "true" || { error "No modules directory found: $plan_root/modules" return 1 } @@ -264,6 +356,10 @@ EOF local i for i in "${!ORCH_ITEM_IDS[@]}"; do orch_item_matches_module "$i" "$module_filter" || continue + case "${ORCH_MODULE_STATUSES[${ORCH_ITEM_MODULES[$i]}]:-Unknown}" in + Ready|"In Progress") ;; + *) continue ;; + esac [[ "${ORCH_ITEM_STATUSES[$i]}" == "Ready" ]] || continue orch_deps_complete "${ORCH_ITEM_DEPS[$i]}" || continue @@ -280,3 +376,418 @@ EOF fi return 1 } + +orch_today() { + date -u +%Y-%m-%d +} + +orch_rewrite_work_item() { + local file="$1" + local id="$2" + local mode="$3" # "status" or "learning" + local value="$4" + + [[ -f "$file" ]] || { error "Cannot rewrite: file not found: $file"; return 1; } + + local tmp + tmp=$(mktemp) || { error "Cannot create temp file"; return 1; } + + awk -v target="$id" -v mode="$mode" -v value="$value" ' + function emit_buffer( i) { + for (i = 0; i < bcount; i++) print buffer[i] + bcount = 0 + } + + function meta_line(idx) { + return buffer[idx] ~ /^- \*\*[A-Za-z][^*]*:\*\*/ + } + + function continuation_line(idx) { + return buffer[idx] ~ /^[[:space:]]+[^[:space:]]/ + } + + function flush_target( i, status_line, learning_line, status_idx, last_meta, validation_idx, insert_idx) { + status_idx = -1 + validation_idx = -1 + last_meta = -1 + for (i = 0; i < bcount; i++) { + if (buffer[i] ~ /^- \*\*Status:\*\*/) status_idx = i + if (buffer[i] ~ /^- \*\*Validation:\*\*/) validation_idx = i + if (meta_line(i)) last_meta = i + else if (continuation_line(i) && last_meta >= 0) last_meta = i + } + + if (mode == "status") { + status_line = "- **Status:** " value + if (status_idx >= 0) { + buffer[status_idx] = status_line + emit_buffer() + return + } + if (last_meta < 0) last_meta = bcount - 1 + for (i = 0; i <= last_meta; i++) print buffer[i] + print status_line + for (i = last_meta + 1; i < bcount; i++) print buffer[i] + bcount = 0 + return + } + + if (mode == "learning") { + learning_line = "- **Learning:** \"" value "\"" + if (validation_idx >= 0) { + insert_idx = validation_idx + # advance past any multi-line continuation under Validation + while (insert_idx + 1 < bcount && continuation_line(insert_idx + 1)) insert_idx++ + } else if (last_meta >= 0) { + insert_idx = last_meta + } else { + insert_idx = bcount - 1 + } + for (i = 0; i <= insert_idx; i++) print buffer[i] + print learning_line + for (i = insert_idx + 1; i < bcount; i++) print buffer[i] + bcount = 0 + return + } + + emit_buffer() + } + + /^### / { + if (state == "in") flush_target() + if ($0 ~ "^### " target ":") { + state = "in" + bcount = 0 + buffer[bcount++] = $0 + next + } + state = "out" + print + next + } + + /^## / && state == "in" { + flush_target() + state = "out" + print + next + } + + state == "in" { + buffer[bcount++] = $0 + next + } + + { print } + + END { + if (state == "in") flush_target() + } + ' "$file" > "$tmp" || { rm -f "$tmp"; error "Rewrite failed for $id in $file"; return 1; } + + mv "$tmp" "$file" +} + +orch_rewrite_status() { + orch_rewrite_work_item "$1" "$2" "status" "$3" +} + +orch_append_learning() { + orch_rewrite_work_item "$1" "$2" "learning" "$3" +} + +cmd_start() { + local plan_root="plans" + local id="" + + while [[ $# -gt 0 ]]; do + case "$1" in + --plans) + plan_root="${2:-}" + [[ -n "$plan_root" ]] || { error "--plans requires a directory"; return 1; } + shift 2 + ;; + --help|-h) + cat < [options] + +Mark a Ready work item as In Progress in its .aps.md file. + +Arguments: + ID Work item ID, e.g. AUTH-003 + +Options: + --plans DIR Plan root directory (default: plans) + --help Show this help + +Validates that the item is Ready and its dependencies are Complete before +mutating the markdown. Suggests a branch name (work/) - branch creation +is left to the user per ORCH D-003. +EOF + return 0 + ;; + -*) + error "Unknown option: $1" + return 1 + ;; + *) + [[ -z "$id" ]] || { error "Unexpected argument: $1"; return 1; } + id="$1" + shift + ;; + esac + done + + [[ -n "$id" ]] || { error "Usage: aps start "; return 1; } + + if [[ ! -d "$plan_root" ]]; then + error "Path not found: $plan_root" + return 1 + fi + + orch_reset_state + orch_load_index_modules "$plan_root" + orch_load_work_items "$plan_root" "true" || { + error "No modules directory found: $plan_root/modules" + return 1 + } + + local idx + idx=$(orch_item_index "$id" || true) + [[ -n "$idx" ]] || { error "Work item not found: $id"; return 1; } + + local current="${ORCH_ITEM_STATUSES[$idx]}" + local file="${ORCH_ITEM_FILES[$idx]}" + local module_id="${ORCH_ITEM_MODULES[$idx]}" + local module_status="${ORCH_MODULE_STATUSES[$module_id]:-Unknown}" + local deps="${ORCH_ITEM_DEPS[$idx]}" + local already_started="false" + + case "$module_status" in + Ready|"In Progress") ;; + *) + error "$id belongs to module $module_id (status: $module_status) - module must be Ready or In Progress to start work items" + return 1 + ;; + esac + + case "$current" in + Ready) ;; + "In Progress") + already_started="true" + ;; + Complete) + error "$id is already Complete - cannot restart" + return 1 + ;; + *) + error "$id has status '$current' - cannot start (must be Ready)" + return 1 + ;; + esac + + if ! orch_deps_complete "$deps"; then + error "$id has unmet dependencies: $(orch_deps_display "$deps")" + return 1 + fi + + if [[ "$already_started" != "true" ]]; then + orch_rewrite_status "$file" "$id" "In Progress" || return 1 + ORCH_ITEM_STATUSES[$idx]="In Progress" + fi + + local context_file + context_file=$(orch_context_package "$plan_root" "$idx") || return 1 + + local lower_id="${id,,}" + if [[ "$already_started" == "true" ]]; then + warn "$id is already In Progress (no status change)" + else + echo "Marked $id as In Progress" + fi + echo "Suggested branch: work/$lower_id" + echo "File: $file" + echo "Context package: $context_file" +} + +cmd_complete() { + local plan_root="plans" + local id="" + local learning="" + + while [[ $# -gt 0 ]]; do + case "$1" in + --plans) + plan_root="${2:-}" + [[ -n "$plan_root" ]] || { error "--plans requires a directory"; return 1; } + shift 2 + ;; + --learning) + learning="${2:-}" + [[ -n "$learning" ]] || { error "--learning requires a value"; return 1; } + shift 2 + ;; + --help|-h) + cat < [options] + +Mark an In Progress work item as Complete in its .aps.md file. + +Arguments: + ID Work item ID, e.g. AUTH-003 + +Options: + --plans DIR Plan root directory (default: plans) + --learning "..." Append a learning line after Validation (ORCH D-002) + --help Show this help + +Validates that the item is In Progress before mutating the markdown. +Stamps Status as "Complete: YYYY-MM-DD" using today's UTC date. +EOF + return 0 + ;; + -*) + error "Unknown option: $1" + return 1 + ;; + *) + [[ -z "$id" ]] || { error "Unexpected argument: $1"; return 1; } + id="$1" + shift + ;; + esac + done + + [[ -n "$id" ]] || { error "Usage: aps complete "; return 1; } + + if [[ ! -d "$plan_root" ]]; then + error "Path not found: $plan_root" + return 1 + fi + + orch_reset_state + orch_load_index_modules "$plan_root" + orch_load_work_items "$plan_root" "true" || { + error "No modules directory found: $plan_root/modules" + return 1 + } + + local idx + idx=$(orch_item_index "$id" || true) + [[ -n "$idx" ]] || { error "Work item not found: $id"; return 1; } + + local current="${ORCH_ITEM_STATUSES[$idx]}" + local file="${ORCH_ITEM_FILES[$idx]}" + + case "$current" in + "In Progress") ;; + Complete) + warn "$id is already Complete (no change)" + return 0 + ;; + *) + error "$id has status '$current' - cannot complete (must be In Progress)" + return 1 + ;; + esac + + if [[ -z "$learning" && -t 0 ]]; then + read -r -p "Learning (optional): " learning + fi + + local today + today=$(orch_today) + orch_rewrite_status "$file" "$id" "Complete: $today" || return 1 + + if [[ -n "$learning" ]]; then + orch_append_learning "$file" "$id" "$learning" || return 1 + fi + + echo "Marked $id as Complete: $today" + [[ -n "$learning" ]] && echo "Learning recorded for $id" + echo "File: $file" +} + +cmd_graph() { + local plan_root="plans" + local module_filter="" + + while [[ $# -gt 0 ]]; do + case "$1" in + --plans) + plan_root="${2:-}" + [[ -n "$plan_root" ]] || { error "--plans requires a directory"; return 1; } + shift 2 + ;; + --help|-h) + cat <> "$aps_dir/.gitignore" chmod +x "$aps_dir/bin/aps" } @@ -1296,7 +1300,7 @@ cmd_migrate() { fi # Remove only known APS files from lib/, then remove dir if empty if [[ -d "$target/lib" ]] && [[ -f "$target/lib/output.sh" ]]; then - local aps_lib_files=(output.sh Output.psm1 lint.sh Lint.psm1 scaffold.sh Scaffold.psm1) + local aps_lib_files=(output.sh Output.psm1 lint.sh Lint.psm1 orchestrate.sh scaffold.sh Scaffold.psm1) local aps_rule_files=(common.sh Common.psm1 module.sh Module.psm1 index.sh Index.psm1 workitem.sh WorkItem.psm1 issues.sh Issues.psm1 design.sh Design.psm1) for f in "${aps_lib_files[@]}"; do rm -f "$target/lib/$f"; done diff --git a/plans/modules/orchestrate.aps.md b/plans/modules/orchestrate.aps.md index e16e039..4930a25 100644 --- a/plans/modules/orchestrate.aps.md +++ b/plans/modules/orchestrate.aps.md @@ -206,7 +206,7 @@ A rich agent definition (like BMAD's BMad Master) that: statuses and cross-module dependencies - **Confidence:** high - **Dependencies:** VAL (parser) -- **Status:** In Progress +- **Status:** Complete ### ORCH-002: Implement `aps start` and `aps complete` @@ -220,6 +220,7 @@ A rich agent definition (like BMAD's BMad Master) that: learning appended when provided - **Confidence:** high - **Dependencies:** ORCH-001 +- **Status:** Complete ### ORCH-003: Implement context packaging @@ -231,6 +232,7 @@ A rich agent definition (like BMAD's BMad Master) that: produces fresh output - **Confidence:** medium - **Dependencies:** ORCH-002 +- **Status:** Complete ### ORCH-004: Implement `aps graph` @@ -241,6 +243,7 @@ A rich agent definition (like BMAD's BMad Master) that: and cross-module deps - **Confidence:** medium - **Dependencies:** ORCH-001 +- **Status:** Complete ### ORCH-005: Create Conductor agent diff --git a/test/fixtures/orchestrate/plans/modules/auth.aps.md b/test/fixtures/orchestrate/plans/modules/auth.aps.md index d2b8fa8..4afd44b 100644 --- a/test/fixtures/orchestrate/plans/modules/auth.aps.md +++ b/test/fixtures/orchestrate/plans/modules/auth.aps.md @@ -54,6 +54,8 @@ Validate dependency-aware item selection. - **Intent:** Select the first ready item with complete dependencies - **Expected Outcome:** `aps next` returns this item - **Validation:** `bash test/orchestrate.sh` +- **Files:** + - src/auth/refresh.sh - **Dependencies:** - AUTH-001 - AUTH-002 @@ -72,3 +74,13 @@ Validate dependency-aware item selection. - **Expected Outcome:** This item is not selected by `aps next` - **Validation:** `bash test/orchestrate.sh` - **Status:** Waiting + +### AUTH-006: Final item before decisions + +- **Intent:** Ensure final work item extraction stops at the next module section +- **Expected Outcome:** Context packages do not include following sections +- **Validation:** `bash test/orchestrate.sh` + +## Decisions + +- **D-001:** Fixture decision after work items - *decided: yes* diff --git a/test/fixtures/orchestrate/plans/modules/core.aps.md b/test/fixtures/orchestrate/plans/modules/core.aps.md index ceb691f..c4af7d6 100644 --- a/test/fixtures/orchestrate/plans/modules/core.aps.md +++ b/test/fixtures/orchestrate/plans/modules/core.aps.md @@ -2,7 +2,7 @@ | ID | Owner | Status | |----|-------|--------| -| CORE | @test | Ready | +| CORE | @test | Complete | ## Purpose @@ -39,4 +39,5 @@ Provide completed cross-module dependencies. - **Intent:** Provide a completed dependency from another module - **Expected Outcome:** Auth work can depend on this item - **Validation:** `true` +- **Learning:** "Parser output is stable across modules" - **Status:** (Complete) 2026-05-04 diff --git a/test/orchestrate.sh b/test/orchestrate.sh index 1cac5e4..c4e1e20 100644 --- a/test/orchestrate.sh +++ b/test/orchestrate.sh @@ -3,7 +3,8 @@ set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" APS="$ROOT/bin/aps" -PLANS="$ROOT/test/fixtures/orchestrate/plans" +FIXTURE="$ROOT/test/fixtures/orchestrate" +PLANS="$FIXTURE/plans" assert_contains() { local output="$1" @@ -15,6 +16,25 @@ assert_contains() { fi } +assert_file_contains() { + local file="$1" + local expected="$2" + + if ! grep -qF -e "$expected" "$file"; then + printf 'Expected file %s to contain: %s\n' "$file" "$expected" >&2 + printf -- '--- file contents ---\n' >&2 + cat "$file" >&2 + exit 1 + fi +} + +# Use a temp copy for any tests that mutate state, so the committed fixture +# stays stable. +WORK_DIR=$(mktemp -d) +trap 'rm -rf "$WORK_DIR"' EXIT +cp -r "$FIXTURE" "$WORK_DIR/orchestrate" +WORK_PLANS="$WORK_DIR/orchestrate/plans" + output=$("$APS" next --plans "$PLANS") assert_contains "$output" "AUTH-003: Add token refresh" assert_contains "$output" "Dependencies: AUTH-001" @@ -32,4 +52,106 @@ fi assert_contains "$output" "No ready work item found for module: billing" +# --- aps start --- + +# Ready item with all deps Complete transitions to In Progress +output=$("$APS" start AUTH-003 --plans "$WORK_PLANS") +assert_contains "$output" "Marked AUTH-003 as In Progress" +assert_contains "$output" "Suggested branch: work/auth-003" +assert_contains "$output" "Context package:" +assert_file_contains "$WORK_PLANS/modules/auth.aps.md" "- **Status:** In Progress" +CONTEXT_FILE="$WORK_DIR/orchestrate/.aps/context/AUTH-003.md" +assert_file_contains "$CONTEXT_FILE" "# Context: AUTH-003 - Add token refresh" +assert_file_contains "$CONTEXT_FILE" "## Work Item" +assert_file_contains "$CONTEXT_FILE" "## Module Scope" +assert_file_contains "$CONTEXT_FILE" "## Dependency Learnings" +assert_file_contains "$CONTEXT_FILE" "CORE-001: \"Parser output is stable across modules\"" +assert_file_contains "$CONTEXT_FILE" "- src/auth/refresh.sh" + +# Regenerating context overwrites stale output. +printf '\nSTALE\n' >> "$CONTEXT_FILE" +output=$("$APS" start AUTH-003 --plans "$WORK_PLANS" 2>&1) +if grep -qF -e "STALE" "$CONTEXT_FILE"; then + printf 'Expected context regeneration to remove stale content\n' >&2 + exit 1 +fi +assert_contains "$output" "already In Progress" + +# Already In Progress is a no-op warning +output=$("$APS" start AUTH-003 --plans "$WORK_PLANS" 2>&1) +assert_contains "$output" "already In Progress" + +# Item with unmet deps is rejected +if output=$("$APS" start AUTH-004 --plans "$WORK_PLANS" 2>&1); then + printf 'Expected start AUTH-004 to fail (unmet deps)\n' >&2 + exit 1 +fi +assert_contains "$output" "unmet dependencies" + +# Unknown ID is rejected +if output=$("$APS" start NOPE-999 --plans "$WORK_PLANS" 2>&1); then + printf 'Expected start NOPE-999 to fail\n' >&2 + exit 1 +fi +assert_contains "$output" "Work item not found" + +# Final work item extraction stops before following module sections. +output=$("$APS" start AUTH-006 --plans "$WORK_PLANS") +BILL_CONTEXT="$WORK_DIR/orchestrate/.aps/context/AUTH-006.md" +assert_file_contains "$BILL_CONTEXT" "# Context: AUTH-006 - Final item before decisions" +work_item_section=$(awk '/^## Work Item/{flag=1; next} flag && /^## Module Scope/{exit} flag' \ + "$BILL_CONTEXT") +if [[ "$work_item_section" == *"## Decisions"* || "$work_item_section" == *"## Module Scope"* ]]; then + printf 'Expected final work item context not to include following sections\n%s\n' "$work_item_section" >&2 + exit 1 +fi + +# --- aps complete --- + +# Cannot complete an item that is still Ready +if output=$("$APS" complete AUTH-004 --plans "$WORK_PLANS" 2>&1); then + printf 'Expected complete AUTH-004 to fail (not In Progress)\n' >&2 + exit 1 +fi +assert_contains "$output" "must be In Progress" + +# Completing an In Progress item with a learning stamps the date and inserts +# the learning after Validation (per ORCH D-002). +output=$("$APS" complete AUTH-003 --plans "$WORK_PLANS" --learning "Rotate refresh tokens") +assert_contains "$output" "Marked AUTH-003 as Complete:" +assert_contains "$output" "Learning recorded" +assert_file_contains "$WORK_PLANS/modules/auth.aps.md" "- **Status:** Complete:" +assert_file_contains "$WORK_PLANS/modules/auth.aps.md" '- **Learning:** "Rotate refresh tokens"' + +# Verify Learning immediately follows the Validation block within AUTH-003 +# (per ORCH D-002). Extract just the AUTH-003 work item block and inspect. +auth_block=$(awk '/^### AUTH-003:/{flag=1; next} flag && /^### /{exit} flag' \ + "$WORK_PLANS/modules/auth.aps.md") +validation_idx=$(printf '%s\n' "$auth_block" | grep -n 'Validation:' | head -1 | cut -d: -f1) +learning_idx=$(printf '%s\n' "$auth_block" | grep -n 'Learning:' | head -1 | cut -d: -f1) +if [[ -z "$validation_idx" || -z "$learning_idx" ]] || \ + ! [[ "$learning_idx" -gt "$validation_idx" ]]; then + printf 'Expected Learning to follow Validation in AUTH-003 block.\nValidation idx: %s\nLearning idx: %s\nBlock:\n%s\n' \ + "$validation_idx" "$learning_idx" "$auth_block" >&2 + exit 1 +fi + +# After AUTH-003 completes, aps next should resolve AUTH-004 +output=$("$APS" next --plans "$WORK_PLANS") +assert_contains "$output" "AUTH-004" + +# --- aps graph --- + +output=$("$APS" graph auth --plans "$WORK_PLANS") +assert_contains "$output" "AUTH-003 [Complete] Add token refresh" +assert_contains "$output" "<- AUTH-001[Complete] AUTH-002[Complete] CORE-001[Complete]" +assert_contains "$output" "AUTH-004 [Ready] Add session audit log" +assert_contains "$output" "<- AUTH-003[Complete]" + +if output=$("$APS" graph nope --plans "$WORK_PLANS" 2>&1); then + printf 'Expected unknown graph lookup to fail\n' >&2 + exit 1 +fi +assert_contains "$output" "No work items found for module: nope" + printf 'orchestrate tests passed\n' diff --git a/test/run.sh b/test/run.sh index df08658..94af762 100755 --- a/test/run.sh +++ b/test/run.sh @@ -92,5 +92,18 @@ echo "$output" | grep -q '"summary"' && pass || fail "JSON output invalid" echo -n "Test: plans/ directory passes lint... " $APS lint "$PROJECT_ROOT/plans/" > /dev/null 2>&1 && pass || fail "our own plans failed lint" +# Test 16: Orchestration suite (next, start, complete) +echo -n "Test: orchestrate (next/start/complete)... " +bash "$SCRIPT_DIR/orchestrate.sh" > /dev/null 2>&1 && pass || fail "orchestrate tests failed" + +# Test 17: Init installs CLI support files and ignores generated context +echo -n "Test: init installs orchestration support... " +INIT_DIR=$(mktemp -d) +trap 'rm -rf "$INIT_DIR"' EXIT +APS_LOCAL="$PROJECT_ROOT" $APS init "$INIT_DIR" --profile solo --scope small --tools generic > /dev/null 2>&1 || fail "init failed" +[[ -f "$INIT_DIR/.aps/lib/orchestrate.sh" ]] || fail "orchestrate lib not installed" +grep -qF 'context/' "$INIT_DIR/.aps/.gitignore" || fail "context ignore missing" +pass + echo "" echo -e "${GREEN}All tests passed!${NC}"