From c970926235b430dade17b7114e93b012a93a985b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 27 Jun 2026 12:31:50 +0000 Subject: [PATCH 1/7] Add SDLC workflow state manager for phase and gate tracking Builds on sdlc-pointer.sh to answer where am I, what is next, and how to shelf or resume work. - agent-context/sdlc-workflow.sh: status, resume, advance, skip, shelf, sync - Tracks phases, quality gates, skips, and shelved work under .sdlc/workflows/ - Infers progress from canvas, progress-log, reviews, and session brief - Wired into start-agent-session.sh and capture-session-memory.sh - Tests and CI workflow Co-authored-by: John Menke --- .github/workflows/test-sdlc-workflow.yml | 50 ++ agent-context/README.md | 40 ++ agent-context/sdlc-workflow.sh | 767 +++++++++++++++++++++++ scripts/capture-session-memory.sh | 8 + scripts/init-project.sh | 7 + scripts/start-agent-session.sh | 8 + scripts/upgrade-project.sh | 4 + scripts/verify-project-install.sh | 3 +- tests/test-sdlc-workflow.sh | 126 ++++ 9 files changed, 1012 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/test-sdlc-workflow.yml create mode 100755 agent-context/sdlc-workflow.sh create mode 100755 tests/test-sdlc-workflow.sh diff --git a/.github/workflows/test-sdlc-workflow.yml b/.github/workflows/test-sdlc-workflow.yml new file mode 100644 index 0000000..88fb880 --- /dev/null +++ b/.github/workflows/test-sdlc-workflow.yml @@ -0,0 +1,50 @@ +name: Test SDLC Workflow + +on: + pull_request: + paths: + - 'agent-context/sdlc-workflow.sh' + - 'agent-context/sdlc-pointer.sh' + - 'agent-context/README.md' + - 'scripts/start-agent-session.sh' + - 'scripts/capture-session-memory.sh' + - 'scripts/init-project.sh' + - 'scripts/upgrade-project.sh' + - 'scripts/verify-project-install.sh' + - 'tests/test-sdlc-workflow.sh' + - 'tests/test-sdlc-pointer.sh' + - '.github/workflows/test-sdlc-workflow.yml' + push: + branches: [main] + paths: + - 'agent-context/sdlc-workflow.sh' + - 'agent-context/sdlc-pointer.sh' + - 'agent-context/README.md' + - 'scripts/start-agent-session.sh' + - 'scripts/capture-session-memory.sh' + - 'scripts/init-project.sh' + - 'scripts/upgrade-project.sh' + - 'scripts/verify-project-install.sh' + - 'tests/test-sdlc-workflow.sh' + - 'tests/test-sdlc-pointer.sh' + - '.github/workflows/test-sdlc-workflow.yml' + +jobs: + test-sdlc-workflow: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Check shell syntax + run: | + bash -n agent-context/sdlc-pointer.sh + bash -n agent-context/sdlc-workflow.sh + bash -n tests/test-sdlc-workflow.sh + bash -n scripts/start-agent-session.sh + bash -n scripts/capture-session-memory.sh + + - name: Run SDLC pointer regression tests + run: ./tests/test-sdlc-pointer.sh + + - name: Run SDLC workflow regression tests + run: ./tests/test-sdlc-workflow.sh diff --git a/agent-context/README.md b/agent-context/README.md index 2a288b5..2b2da4e 100644 --- a/agent-context/README.md +++ b/agent-context/README.md @@ -77,6 +77,46 @@ sdlc_init `start-agent-session.sh` sets the pointer automatically when `--work-id` is provided. +## SDLC Workflow (phase + gate tracking) + +The workflow manager builds on the pointer to answer **where am I?**, **what is next?**, +and **how do I shelf or resume work?** State lives under `.sdlc/workflows/` (local, +gitignored). Committed artifacts (`progress-log.md`, canvas, reviews) remain the audit trail; +run `sync` to reconcile workflow state from those files. + +```bash +# Where am I on the current task? +./agent-context/sdlc-workflow.sh status + +# Pick up a shelved task (auto-shelves the current pointer if different) +./agent-context/sdlc-workflow.sh resume FEAT-001-order-status-api + +# Resume at a specific phase (e.g. after intentionally skipping ahead) +./agent-context/sdlc-workflow.sh resume FEAT-001-order-status-api --phase code + +# Move to the next phase after finishing a step +./agent-context/sdlc-workflow.sh advance + +# Jump ahead to a later phase +./agent-context/sdlc-workflow.sh advance --to review + +# Skip a phase with a recorded reason +./agent-context/sdlc-workflow.sh skip api-test --reason "no HTTP surface" + +# Park current work and clear the pointer +./agent-context/sdlc-workflow.sh shelf --reason "blocked on dependency" + +# Re-read canvas, progress log, and session brief into workflow state +./agent-context/sdlc-workflow.sh sync + +# List shelved work ids +./agent-context/sdlc-workflow.sh list-shelved +``` + +`start-agent-session.sh` and `capture-session-memory.sh` update workflow timestamps +automatically. After shelving, run `resume ` then `start-agent-session.sh` +with the suggested phase to sync back into the chat workflow. + ## Session Persistence Use scripts to keep agent sessions durable across chat boundaries: diff --git a/agent-context/sdlc-workflow.sh b/agent-context/sdlc-workflow.sh new file mode 100755 index 0000000..9645b6c --- /dev/null +++ b/agent-context/sdlc-workflow.sh @@ -0,0 +1,767 @@ +#!/usr/bin/env bash +# Workflow state manager for SDLC-SPDD — tracks phase, gates, shelf/resume on top of sdlc-pointer.sh +# +# Usage: +# source agent-context/sdlc-workflow.sh +# sdlc_workflow_status +# sdlc_workflow_resume WORK_ID [--phase PHASE] +# sdlc_workflow_advance [--to PHASE] +# sdlc_workflow_skip PHASE [--reason TEXT] +# sdlc_workflow_shelf [--reason TEXT] +# sdlc_workflow_sync [--work-id WORK_ID] +# +# State: .sdlc/workflows/.state and .sdlc/workflows/.history + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + set -euo pipefail +fi + +_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=/dev/null +source "${_SCRIPT_DIR}/sdlc-pointer.sh" + +SDLC_WORKFLOW_DIR="${SDLC_DIR}/workflows" +SDLC_WORKFLOW_LOCK="${SDLC_DIR}/workflow.lock" + +mkdir -p "${SDLC_WORKFLOW_DIR}" + +SDLC_PHASE_ORDER=(init analysis plan architect code api-test review prompt-update retro sync) + +SDLC_GATE_NAMES=( + requirement_documented + canvas_exists + architect_review + operations_task_sized + code_maps_to_ops + tests_updated + review_completed + safeguards_checked + retro_completed + canvas_synced +) + +SDLC_GATE_LABELS=( + "Requirement documented" + "REASONS Canvas exists" + "Architect review completed" + "Operations are task-sized" + "Code changes map to approved operations" + "Tests added or updated" + "Review completed" + "Safeguards checked" + "Retro completed" + "Canvas synced with implementation" +) + +_have_flock() { + command -v flock >/dev/null 2>&1 +} + +_wf_now() { + date -u +"%Y-%m-%dT%H:%M:%SZ" +} + +_wf_state_file() { + printf '%s/%s.state' "${SDLC_WORKFLOW_DIR}" "$1" +} + +_wf_history_file() { + printf '%s/%s.history' "${SDLC_WORKFLOW_DIR}" "$1" +} + +_wf_valid_phase() { + local phase="$1" + local p + for p in "${SDLC_PHASE_ORDER[@]}"; do + [[ "${p}" == "${phase}" ]] && return 0 + done + return 1 +} + +_wf_phase_index() { + local phase="$1" + local i=0 + for p in "${SDLC_PHASE_ORDER[@]}"; do + if [[ "${p}" == "${phase}" ]]; then + echo "${i}" + return 0 + fi + i=$((i + 1)) + done + echo "-1" + return 1 +} + +_wf_phase_at() { + local idx="$1" + if (( idx < 0 || idx >= ${#SDLC_PHASE_ORDER[@]} )); then + return 1 + fi + echo "${SDLC_PHASE_ORDER[$idx]}" +} + +_wf_next_phase() { + local phase="$1" + local idx next + idx="$(_wf_phase_index "${phase}")" + next=$((idx + 1)) + _wf_phase_at "${next}" || true +} + +_wf_read_state_var() { + local file="$1" + local key="$2" + local default="${3:-}" + if [[ ! -f "${file}" ]]; then + printf '%s' "${default}" + return 0 + fi + local line + line="$(grep -m1 "^${key}=" "${file}" 2>/dev/null || true)" + if [[ -z "${line}" ]]; then + printf '%s' "${default}" + return 0 + fi + printf '%s' "${line#*=}" +} + +_wf_write_state_file() { + local work_id="$1" + local file + file="$(_wf_state_file "${work_id}")" + local tmp="${file}.tmp.$$" + { + printf 'work_id=%s\n' "${work_id}" + printf 'phase=%s\n' "$(_wf_read_state_var "${file}" phase init)" + printf 'operation=%s\n' "$(_wf_read_state_var "${file}" operation)" + printf 'active=%s\n' "$(_wf_read_state_var "${file}" active 1)" + printf 'shelved_at=%s\n' "$(_wf_read_state_var "${file}" shelved_at)" + printf 'shelved_reason=%s\n' "$(_wf_read_state_var "${file}" shelved_reason)" + printf 'milestone=%s\n' "$(_wf_read_state_var "${file}" milestone)" + printf 'last_session_at=%s\n' "$(_wf_read_state_var "${file}" last_session_at)" + printf 'last_capture_at=%s\n' "$(_wf_read_state_var "${file}" last_capture_at)" + local gate + for gate in "${SDLC_GATE_NAMES[@]}"; do + printf 'gate_%s=%s\n' "${gate}" "$(_wf_read_state_var "${file}" "gate_${gate}" pending)" + done + if [[ -f "${file}" ]]; then + grep '^skip_' "${file}" 2>/dev/null || true + fi + } > "${tmp}" + mv -f "${tmp}" "${file}" +} + +_wf_set_state_var() { + local work_id="$1" + local key="$2" + local value="$3" + local file tmp + file="$(_wf_state_file "${work_id}")" + tmp="${file}.tmp.$$" + if [[ ! -f "${file}" ]]; then + _wf_write_state_file "${work_id}" + fi + if grep -q "^${key}=" "${file}" 2>/dev/null; then + grep -v "^${key}=" "${file}" > "${tmp}" || true + else + cp "${file}" "${tmp}" + fi + printf '%s=%s\n' "${key}" "${value}" >> "${tmp}" + mv -f "${tmp}" "${file}" +} + +_wf_with_workflow_lock() { + if _have_flock; then + ( + flock -x 200 || exit 1 + "$@" + ) 200>"${SDLC_WORKFLOW_LOCK}" + else + "$@" + fi +} + +_wf_log_history() { + local work_id="$1" + local action="$2" + shift 2 + local hist + hist="$(_wf_history_file "${work_id}")" + printf '%s\t%s\t%s\n' "$(_wf_now)" "${action}" "$*" >> "${hist}" +} + +_wf_ensure_state() { + local work_id="$1" + local file + file="$(_wf_state_file "${work_id}")" + if [[ ! -f "${file}" ]]; then + _wf_write_state_file "${work_id}" + _wf_set_state_var "${work_id}" phase init + _wf_log_history "${work_id}" create "work_id=${work_id}" + fi +} + +sdlc_workflow_recommended_command() { + local phase="${1:-init}" + local work_id="${2:-}" + case "${phase}" in + init) echo "/sdlc-spdd-init" ;; + analysis) echo "/sdlc-spdd-analysis @requirements/.md" ;; + plan) echo "/sdlc-spdd-plan @spdd/analysis/${work_id:-}-analysis.md" ;; + architect) echo "/sdlc-spdd-architect @spdd/canvas/${work_id:-}.md" ;; + code) echo "/sdlc-spdd-code @spdd/canvas/${work_id:-}.md operation " ;; + api-test) echo "/sdlc-spdd-api-test @spdd/canvas/${work_id:-}.md" ;; + review) echo "/sdlc-spdd-review @spdd/canvas/${work_id:-}.md" ;; + prompt-update) echo "/sdlc-spdd-prompt-update @spdd/canvas/${work_id:-}.md" ;; + retro) echo "/sdlc-spdd-retro @spdd/canvas/${work_id:-}.md" ;; + sync) echo "/sdlc-spdd-sync @spdd/canvas/${work_id:-}.md" ;; + resume) echo "Read agent-context/sessions/current-session.md, then choose the next phase command." ;; + *) echo "/sdlc-spdd-init" ;; + esac +} + +_wf_infer_phase_from_artifacts() { + local work_id="$1" + local root="${SDLC_ROOT}" + local inferred="init" + local req feature_req analysis canvas review retro sync_log progress + + feature_req="${root}/agent-context/features/${work_id}/requirement.md" + req="${root}/requirements/milestones/${work_id}.md" + analysis="${root}/spdd/analysis/${work_id}-analysis.md" + canvas="${root}/spdd/canvas/${work_id}.md" + review="${root}/spdd/reviews/${work_id}-review.md" + retro="${root}/agent-context/features/${work_id}/retro.md" + sync_log="${root}/spdd/sync/${work_id}-sync.md" + progress="${root}/agent-context/features/${work_id}/progress-log.md" + + if [[ -f "${req}" || -f "${feature_req}" ]]; then + inferred="analysis" + fi + if [[ -f "${analysis}" ]]; then + inferred="plan" + fi + if [[ -f "${canvas}" ]]; then + inferred="architect" + if grep -Eqi 'ready[[:space:]]+for[[:space:]]+coding' "${canvas}" 2>/dev/null; then + inferred="code" + fi + fi + if [[ -f "${progress}" ]] && grep -Eqi '(T[0-9]{2}.*complete|implemented|merged)' "${progress}" 2>/dev/null; then + inferred="code" + fi + if [[ -f "${review}" ]]; then + inferred="review" + fi + if [[ -f "${retro}" ]]; then + inferred="retro" + fi + if [[ -f "${sync_log}" ]]; then + inferred="sync" + fi + + local session_file="${root}/agent-context/sessions/current-session.md" + if [[ -f "${session_file}" ]] && grep -Fq "${work_id}" "${session_file}"; then + local session_phase + session_phase="$(grep -m1 '^- Phase:' "${session_file}" 2>/dev/null | sed 's/^- Phase:[[:space:]]*//' || true)" + if [[ -n "${session_phase}" ]] && _wf_valid_phase "${session_phase}"; then + local stored_idx inferred_idx session_idx + stored_idx="$(_wf_phase_index "${inferred}")" + session_idx="$(_wf_phase_index "${session_phase}")" + inferred_idx="${stored_idx}" + if (( session_idx > inferred_idx )); then + inferred="${session_phase}" + fi + fi + fi + + echo "${inferred}" +} + +_wf_infer_gates_from_artifacts() { + local work_id="$1" + local root="${SDLC_ROOT}" + local req feature_req analysis canvas review retro sync_log progress + + feature_req="${root}/agent-context/features/${work_id}/requirement.md" + req="${root}/requirements/milestones/${work_id}.md" + analysis="${root}/spdd/analysis/${work_id}-analysis.md" + canvas="${root}/spdd/canvas/${work_id}.md" + review="${root}/spdd/reviews/${work_id}-review.md" + retro="${root}/agent-context/features/${work_id}/retro.md" + sync_log="${root}/spdd/sync/${work_id}-sync.md" + progress="${root}/agent-context/features/${work_id}/progress-log.md" + + [[ -f "${req}" || -f "${feature_req}" ]] && echo "requirement_documented=passed" + [[ -f "${canvas}" || -f "${root}/agent-context/features/${work_id}/reasons-canvas.md" ]] && echo "canvas_exists=passed" + if [[ -f "${canvas}" ]] && grep -Eqi 'ready[[:space:]]+for[[:space:]]+coding' "${canvas}" 2>/dev/null; then + echo "architect_review=passed" + echo "operations_task_sized=passed" + fi + if [[ -f "${canvas}" ]] && grep -Eqi '^###[[:space:]]+T[0-9]{2}' "${canvas}" 2>/dev/null; then + echo "operations_task_sized=passed" + fi + if [[ -f "${progress}" ]] && grep -Eqi '(T[0-9]{2}.*complete|implemented|merged|mvn test|pytest)' "${progress}" 2>/dev/null; then + echo "code_maps_to_ops=passed" + echo "tests_updated=passed" + fi + [[ -f "${review}" ]] && echo "review_completed=passed" && echo "safeguards_checked=passed" + [[ -f "${retro}" ]] && echo "retro_completed=passed" + if [[ -f "${sync_log}" ]]; then + echo "canvas_synced=passed" + elif [[ -f "${canvas}" && -f "${root}/agent-context/features/${work_id}/reasons-canvas.md" ]] \ + && cmp -s "${canvas}" "${root}/agent-context/features/${work_id}/reasons-canvas.md" 2>/dev/null; then + echo "canvas_synced=passed" + fi + [[ -f "${analysis}" ]] && true +} + +_wf_resolve_phase() { + local stored="$1" + local inferred="$2" + local explicit="${3:-}" + if [[ -n "${explicit}" ]]; then + echo "${explicit}" + return 0 + fi + local stored_idx inferred_idx + stored_idx="$(_wf_phase_index "${stored}")" + inferred_idx="$(_wf_phase_index "${inferred}")" + if (( inferred_idx > stored_idx )); then + echo "${inferred}" + else + echo "${stored}" + fi +} + +_wf_sync_impl() { + local work_id="$1" + local file stored inferred resolved gate_line gate value current + _wf_ensure_state "${work_id}" + file="$(_wf_state_file "${work_id}")" + stored="$(_wf_read_state_var "${file}" phase init)" + inferred="$(_wf_infer_phase_from_artifacts "${work_id}")" + resolved="$(_wf_resolve_phase "${stored}" "${inferred}")" + _wf_set_state_var "${work_id}" phase "${resolved}" + + while IFS= read -r gate_line; do + [[ -z "${gate_line}" ]] && continue + gate="${gate_line%%=*}" + value="${gate_line#*=}" + current="$(_wf_read_state_var "${file}" "gate_${gate}" pending)" + if [[ "${current}" != "skipped" ]]; then + _wf_set_state_var "${work_id}" "gate_${gate}" "${value}" + fi + done < <(_wf_infer_gates_from_artifacts "${work_id}") + + _wf_log_history "${work_id}" sync "phase=${resolved}" +} + +sdlc_workflow_sync() { + local work_id="${1:-}" + if [[ -z "${work_id}" ]]; then + work_id="$(sdlc_get_pointer)" + fi + if [[ -z "${work_id}" ]]; then + echo "sdlc_workflow_sync: no work id (set pointer or pass --work-id)" >&2 + return 2 + fi + + _wf_with_workflow_lock _wf_sync_impl "${work_id}" + echo "workflow synced for ${work_id}" +} + +_wf_touch_session_impl() { + local work_id="$1" + local phase="$2" + local milestone="${3:-}" + _wf_ensure_state "${work_id}" + _wf_set_state_var "${work_id}" phase "${phase}" + _wf_set_state_var "${work_id}" active 1 + _wf_set_state_var "${work_id}" shelved_at "" + _wf_set_state_var "${work_id}" shelved_reason "" + _wf_set_state_var "${work_id}" last_session_at "$(_wf_now)" + [[ -n "${milestone}" ]] && _wf_set_state_var "${work_id}" milestone "${milestone}" + _wf_log_history "${work_id}" session "phase=${phase}" +} + +sdlc_workflow_touch_session() { + local work_id="$1" + local phase="$2" + local milestone="${3:-}" + _wf_with_workflow_lock _wf_touch_session_impl "${work_id}" "${phase}" "${milestone}" +} + +_wf_record_capture_impl() { + local work_id="$1" + local phase="${2:-resume}" + _wf_ensure_state "${work_id}" + _wf_set_state_var "${work_id}" last_capture_at "$(_wf_now)" + [[ "${phase}" != "resume" ]] && _wf_set_state_var "${work_id}" phase "${phase}" + _wf_log_history "${work_id}" capture "phase=${phase}" +} + +sdlc_workflow_record_capture() { + local work_id="$1" + local phase="${2:-resume}" + _wf_with_workflow_lock _wf_record_capture_impl "${work_id}" "${phase}" +} + +sdlc_workflow_resume() { + local work_id="$1" + local phase="${2:-}" + local auto_shelf="${3:-1}" + + if [[ -z "${work_id}" ]]; then + echo "sdlc_workflow_resume: work id required" >&2 + return 2 + fi + + local current + current="$(sdlc_get_pointer)" + if [[ -n "${current}" && "${current}" != "${work_id}" && "${auto_shelf}" == "1" ]]; then + sdlc_workflow_shelf "auto-shelf before resuming ${work_id}" >/dev/null + fi + + sdlc_set_pointer "${work_id}" >/dev/null + _wf_ensure_state "${work_id}" + sdlc_workflow_sync "${work_id}" >/dev/null + + if [[ -n "${phase}" ]]; then + if ! _wf_valid_phase "${phase}"; then + echo "sdlc_workflow_resume: invalid phase '${phase}'" >&2 + return 2 + fi + _wf_set_state_var "${work_id}" phase "${phase}" + _wf_log_history "${work_id}" resume "phase=${phase} (explicit)" + else + _wf_log_history "${work_id}" resume "phase=$(_wf_read_state_var "$(_wf_state_file "${work_id}")" phase init)" + fi + + _wf_set_state_var "${work_id}" active 1 + _wf_set_state_var "${work_id}" shelved_at "" + _wf_set_state_var "${work_id}" shelved_reason "" + + local resolved_phase + resolved_phase="$(_wf_read_state_var "$(_wf_state_file "${work_id}")" phase init)" + echo "Resumed ${work_id} at phase: ${resolved_phase}" + echo "Recommended command: $(sdlc_workflow_recommended_command "${resolved_phase}" "${work_id}")" + echo "Start session:" + echo " ./scripts/sdlc-spdd/start-agent-session.sh --target . --work-id ${work_id} --phase ${resolved_phase}" +} + +sdlc_workflow_advance() { + local to_phase="${1:-}" + local work_id + work_id="$(sdlc_get_pointer)" + if [[ -z "${work_id}" ]]; then + echo "sdlc_workflow_advance: no active pointer" >&2 + return 2 + fi + + _wf_ensure_state "${work_id}" + local file current next + file="$(_wf_state_file "${work_id}")" + current="$(_wf_read_state_var "${file}" phase init)" + + if [[ -n "${to_phase}" ]]; then + if ! _wf_valid_phase "${to_phase}"; then + echo "sdlc_workflow_advance: invalid phase '${to_phase}'" >&2 + return 2 + fi + local cur_idx to_idx + cur_idx="$(_wf_phase_index "${current}")" + to_idx="$(_wf_phase_index "${to_phase}")" + if (( to_idx < cur_idx )); then + echo "sdlc_workflow_advance: cannot move backward to '${to_phase}' (use resume --phase)" >&2 + return 2 + fi + next="${to_phase}" + else + next="$(_wf_next_phase "${current}")" + if [[ -z "${next}" ]]; then + echo "Already at final phase (sync). Capture memory and refresh roadmap." >&2 + return 0 + fi + fi + + _wf_set_state_var "${work_id}" phase "${next}" + _wf_log_history "${work_id}" advance "${current}->${next}" + echo "Advanced ${work_id}: ${current} -> ${next}" + echo "Recommended command: $(sdlc_workflow_recommended_command "${next}" "${work_id}")" +} + +sdlc_workflow_skip() { + local phase="$1" + local reason="${2:-manual skip}" + local work_id + work_id="$(sdlc_get_pointer)" + if [[ -z "${work_id}" ]]; then + echo "sdlc_workflow_skip: no active pointer" >&2 + return 2 + fi + if ! _wf_valid_phase "${phase}"; then + echo "sdlc_workflow_skip: invalid phase '${phase}'" >&2 + return 2 + fi + + _wf_ensure_state "${work_id}" + local file current cur_idx skip_idx next + file="$(_wf_state_file "${work_id}")" + current="$(_wf_read_state_var "${file}" phase init)" + cur_idx="$(_wf_phase_index "${current}")" + skip_idx="$(_wf_phase_index "${phase}")" + + if (( skip_idx < cur_idx )); then + echo "sdlc_workflow_skip: '${phase}' is before current phase '${current}'" >&2 + return 2 + fi + + _wf_set_state_var "${work_id}" "skip_${phase}" "$(_wf_now)|${reason}" + _wf_log_history "${work_id}" skip "phase=${phase} reason=${reason}" + + if (( skip_idx == cur_idx )); then + next="$(_wf_next_phase "${phase}")" + if [[ -n "${next}" ]]; then + _wf_set_state_var "${work_id}" phase "${next}" + echo "Skipped ${phase} -> now at ${next}" + echo "Reason: ${reason}" + echo "Recommended command: $(sdlc_workflow_recommended_command "${next}" "${work_id}")" + else + echo "Skipped ${phase} (final optional phase)" + fi + else + echo "Recorded skip for future phase ${phase}" + echo "Reason: ${reason}" + fi +} + +sdlc_workflow_shelf() { + local reason="${1:-manual shelf}" + local work_id + work_id="$(sdlc_get_pointer)" + if [[ -z "${work_id}" ]]; then + echo "sdlc_workflow_shelf: no active pointer" >&2 + return 2 + fi + + _wf_ensure_state "${work_id}" + _wf_set_state_var "${work_id}" active 0 + _wf_set_state_var "${work_id}" shelved_at "$(_wf_now)" + _wf_set_state_var "${work_id}" shelved_reason "${reason}" + _wf_log_history "${work_id}" shelf "reason=${reason}" + sdlc_reset_pointer >/dev/null + echo "Shelved ${work_id}" + echo "Reason: ${reason}" + echo "Resume later: ./agent-context/sdlc-workflow.sh resume ${work_id}" +} + +sdlc_workflow_list_shelved() { + local file work_id phase shelved_at reason + shopt -s nullglob + for file in "${SDLC_WORKFLOW_DIR}"/*.state; do + if [[ "$(_wf_read_state_var "${file}" active 1)" == "0" ]]; then + work_id="$(_wf_read_state_var "${file}" work_id)" + phase="$(_wf_read_state_var "${file}" phase init)" + shelved_at="$(_wf_read_state_var "${file}" shelved_at)" + reason="$(_wf_read_state_var "${file}" shelved_reason)" + printf '%s\t%s\t%s\t%s\n' "${work_id}" "${phase}" "${shelved_at}" "${reason}" + fi + done + shopt -u nullglob +} + +sdlc_workflow_status() { + local work_id="${1:-}" + if [[ -z "${work_id}" ]]; then + work_id="$(sdlc_get_pointer)" + fi + + echo "SDLC Workflow Status" + echo "====================" + local pointer + pointer="$(sdlc_get_pointer)" + if [[ -n "${pointer}" ]]; then + echo "Active pointer: ${pointer}" + else + echo "Active pointer: (none — resume or set a work id)" + fi + + if [[ -z "${work_id}" ]]; then + echo + echo "Shelved work:" + if sdlc_workflow_list_shelved | grep -q .; then + sdlc_workflow_list_shelved | while IFS=$'\t' read -r wid ph at rs; do + echo " - ${wid} (phase: ${ph}, shelved: ${at})" + [[ -n "${rs}" ]] && echo " reason: ${rs}" + done + else + echo " (none)" + fi + return 0 + fi + + _wf_ensure_state "${work_id}" + local file phase operation active milestone last_session last_capture + file="$(_wf_state_file "${work_id}")" + phase="$(_wf_read_state_var "${file}" phase init)" + operation="$(_wf_read_state_var "${file}" operation)" + active="$(_wf_read_state_var "${file}" active 1)" + milestone="$(_wf_read_state_var "${file}" milestone)" + last_session="$(_wf_read_state_var "${file}" last_session_at)" + last_capture="$(_wf_read_state_var "${file}" last_capture_at)" + + echo + echo "Work ID: ${work_id}" + echo "Status: $([[ "${active}" == "1" ]] && echo active || echo shelved)" + echo "Phase: ${phase}" + + local idx total bar filled i + idx="$(_wf_phase_index "${phase}")" + total="${#SDLC_PHASE_ORDER[@]}" + filled=$((idx + 1)) + bar="" + for ((i = 0; i < total; i++)); do + if (( i < filled )); then bar+="="; else bar+="-"; fi + done + echo "Progress: [${bar}] $((filled))/${total} phases" + + if [[ -n "${operation}" ]]; then + echo "Operation in flight: ${operation}" + fi + [[ -n "${milestone}" ]] && echo "Milestone: ${milestone}" + [[ -n "${last_session}" ]] && echo "Last session: ${last_session}" + [[ -n "${last_capture}" ]] && echo "Last capture: ${last_capture}" + + echo + echo "Phase track:" + local p skip_line skip_reason + for p in "${SDLC_PHASE_ORDER[@]}"; do + skip_line="$(_wf_read_state_var "${file}" "skip_${p}")" + if [[ -n "${skip_line}" ]]; then + skip_reason="${skip_line#*|}" + echo " - ${p}: skipped (${skip_reason})" + elif [[ "$(_wf_phase_index "${p}")" -lt "$(_wf_phase_index "${phase}")" ]]; then + echo " - ${p}: done" + elif [[ "${p}" == "${phase}" ]]; then + echo " - ${p}: <-- current" + else + echo " - ${p}: pending" + fi + done + + echo + echo "Quality gates:" + local gi gate state label + for ((gi = 0; gi < ${#SDLC_GATE_NAMES[@]}; gi++)); do + gate="${SDLC_GATE_NAMES[$gi]}" + label="${SDLC_GATE_LABELS[$gi]}" + state="$(_wf_read_state_var "${file}" "gate_${gate}" pending)" + case "${state}" in + passed) echo " [x] ${label}" ;; + skipped) echo " [-] ${label} (skipped)" ;; + failed) echo " [!] ${label} (failed)" ;; + *) echo " [ ] ${label}" ;; + esac + done + + echo + echo "Shelved work:" + if sdlc_workflow_list_shelved | grep -q .; then + sdlc_workflow_list_shelved | while IFS=$'\t' read -r wid ph at rs; do + echo " - ${wid} (phase: ${ph}, shelved: ${at})" + done + else + echo " (none)" + fi + + echo + echo "Next step:" + echo " $(sdlc_workflow_recommended_command "${phase}" "${work_id}")" + echo + echo "Commands:" + echo " ./agent-context/sdlc-workflow.sh advance # move to next phase" + echo " ./agent-context/sdlc-workflow.sh skip api-test --reason \"no HTTP surface\"" + echo " ./agent-context/sdlc-workflow.sh shelf --reason \"blocked on review\"" + echo " ./agent-context/sdlc-workflow.sh sync # re-read artifacts" + echo " ./agent-context/sdlc-workflow.sh resume # pick up shelved work" +} + +# CLI when script executed directly +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + cmd="${1:-status}" + shift || true + case "${cmd}" in + status|/sdlc-workflow-status) + work_id="" + while [[ $# -gt 0 ]]; do + case "$1" in + --work-id) work_id="${2:-}"; shift 2 ;; + *) work_id="${1}"; shift ;; + esac + done + sdlc_workflow_status "${work_id}" + ;; + resume|/sdlc-workflow-resume) + work_id="${1:-}"; shift || true + phase="" + reason="" + while [[ $# -gt 0 ]]; do + case "$1" in + --phase) phase="${2:-}"; shift 2 ;; + --no-auto-shelf) AUTO_SHELF=0; shift ;; + *) shift ;; + esac + done + sdlc_workflow_resume "${work_id}" "${phase}" "${AUTO_SHELF:-1}" + ;; + advance|/sdlc-workflow-advance) + to="" + while [[ $# -gt 0 ]]; do + case "$1" in + --to) to="${2:-}"; shift 2 ;; + *) to="${1}"; shift ;; + esac + done + sdlc_workflow_advance "${to}" + ;; + skip|/sdlc-workflow-skip) + phase="${1:-}"; shift || true + reason="manual skip" + while [[ $# -gt 0 ]]; do + case "$1" in + --reason) reason="${2:-}"; shift 2 ;; + *) shift ;; + esac + done + sdlc_workflow_skip "${phase}" "${reason}" + ;; + shelf|/sdlc-workflow-shelf) + reason="manual shelf" + while [[ $# -gt 0 ]]; do + case "$1" in + --reason) reason="${2:-}"; shift 2 ;; + *) shift ;; + esac + done + sdlc_workflow_shelf "${reason}" + ;; + sync|/sdlc-workflow-sync) + work_id="" + while [[ $# -gt 0 ]]; do + case "$1" in + --work-id) work_id="${2:-}"; shift 2 ;; + *) work_id="${1}"; shift ;; + esac + done + sdlc_workflow_sync "${work_id}" + ;; + list-shelved|/sdlc-workflow-list-shelved) + sdlc_workflow_list_shelved + ;; + *) + echo "Usage: $0 {status|resume|advance|skip|shelf|sync|list-shelved} ..." >&2 + exit 2 + ;; + esac +fi diff --git a/scripts/capture-session-memory.sh b/scripts/capture-session-memory.sh index 7b4feb2..56ab80c 100755 --- a/scripts/capture-session-memory.sh +++ b/scripts/capture-session-memory.sh @@ -801,6 +801,14 @@ if [[ -f "${current_session}" ]]; then } >> "${current_session}" fi +workflow_script="${TARGET}/agent-context/sdlc-workflow.sh" +if [[ -f "${workflow_script}" ]]; then + SDLC_ROOT="${TARGET}" + # shellcheck source=/dev/null + source "${workflow_script}" + sdlc_workflow_record_capture "${WORK_ID}" "${PHASE}" +fi + echo "Captured session memory:" echo " ${session_history}" echo " ${session_entry_file}" diff --git a/scripts/init-project.sh b/scripts/init-project.sh index d0d1809..77f6b19 100755 --- a/scripts/init-project.sh +++ b/scripts/init-project.sh @@ -222,6 +222,13 @@ if [[ "${DRY_RUN}" -eq 0 && -f "${TARGET}/agent-context/sdlc-pointer.sh" ]]; the chmod +x "${TARGET}/agent-context/sdlc-pointer.sh" fi +copy_if_missing \ + "${REPO_ROOT}/agent-context/sdlc-workflow.sh" \ + "${TARGET}/agent-context/sdlc-workflow.sh" +if [[ "${DRY_RUN}" -eq 0 && -f "${TARGET}/agent-context/sdlc-workflow.sh" ]]; then + chmod +x "${TARGET}/agent-context/sdlc-workflow.sh" +fi + copy_if_missing \ "${REPO_ROOT}/agent-context/README.md" \ "${TARGET}/agent-context/README.md" diff --git a/scripts/start-agent-session.sh b/scripts/start-agent-session.sh index e5955a9..468f600 100755 --- a/scripts/start-agent-session.sh +++ b/scripts/start-agent-session.sh @@ -77,6 +77,14 @@ if [[ -f "${pointer_script}" && -n "${WORK_ID}" ]]; then sdlc_set_pointer "${WORK_ID}" >/dev/null fi +workflow_script="${TARGET}/agent-context/sdlc-workflow.sh" +if [[ -f "${workflow_script}" && -n "${WORK_ID}" ]]; then + SDLC_ROOT="${TARGET}" + # shellcheck source=/dev/null + source "${workflow_script}" + sdlc_workflow_touch_session "${WORK_ID}" "${PHASE}" "${MILESTONE}" +fi + timestamp="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" safe_timestamp="$(date -u +"%Y%m%dT%H%M%SZ")" session_dir="${TARGET}/agent-context/sessions" diff --git a/scripts/upgrade-project.sh b/scripts/upgrade-project.sh index bdbf58f..ecd21ba 100755 --- a/scripts/upgrade-project.sh +++ b/scripts/upgrade-project.sh @@ -395,6 +395,10 @@ copy_executable_framework_file \ "${REPO_ROOT}/agent-context/sdlc-pointer.sh" \ "${TARGET}/agent-context/sdlc-pointer.sh" +copy_executable_framework_file \ + "${REPO_ROOT}/agent-context/sdlc-workflow.sh" \ + "${TARGET}/agent-context/sdlc-workflow.sh" + for file in \ quality-gates.md \ validation-rules.md; do diff --git a/scripts/verify-project-install.sh b/scripts/verify-project-install.sh index 785028d..b6d4d37 100755 --- a/scripts/verify-project-install.sh +++ b/scripts/verify-project-install.sh @@ -154,7 +154,8 @@ run_part "SDLC (sessions, memory, playbooks)" \ SDLC "project memory file" "agent-context/memory/project-memory.md" file \ SDLC "session handoff playbook" "agent-context/playbooks/session-handoff-playbook.md" file \ SDLC "quality gates" "agent-context/harness/quality-gates.md" file \ - SDLC "pointer manager script" "agent-context/sdlc-pointer.sh" executable + SDLC "pointer manager script" "agent-context/sdlc-pointer.sh" executable \ + SDLC "workflow manager script" "agent-context/sdlc-workflow.sh" executable run_part "Runtime scripts and docs" \ Runtime "runtime scripts directory" "scripts/sdlc-spdd" dir \ diff --git a/tests/test-sdlc-workflow.sh b/tests/test-sdlc-workflow.sh new file mode 100755 index 0000000..81d9c69 --- /dev/null +++ b/tests/test-sdlc-workflow.sh @@ -0,0 +1,126 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Regression harness for agent-context/sdlc-workflow.sh +# +# Usage: ./tests/test-sdlc-workflow.sh + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +WORKFLOW="${REPO_ROOT}/agent-context/sdlc-workflow.sh" +POINTER="${REPO_ROOT}/agent-context/sdlc-pointer.sh" +START="${REPO_ROOT}/scripts/start-agent-session.sh" +CAPTURE="${REPO_ROOT}/scripts/capture-session-memory.sh" + +WORK="$(mktemp -d)" +trap 'rm -rf "${WORK}"' EXIT + +pass=0 +fail=0 +ok() { echo " ok $1"; pass=$((pass + 1)); } +bad() { echo " FAIL $1" >&2; fail=$((fail + 1)); } + +wf() { SDLC_ROOT="${1}" "${WORKFLOW}" "${@:2}"; } + +setup_feature() { + local t="$1" + local work_id="$2" + mkdir -p "${t}/agent-context/sessions" \ + "${t}/agent-context/features/${work_id}" \ + "${t}/spdd/canvas" \ + "${t}/spdd/analysis" + cp "${POINTER}" "${t}/agent-context/sdlc-pointer.sh" + cp "${WORKFLOW}" "${t}/agent-context/sdlc-workflow.sh" + chmod +x "${t}/agent-context/sdlc-pointer.sh" "${t}/agent-context/sdlc-workflow.sh" +} + +# --------------------------------------------------------------------------- +echo "== Test 1: resume sets pointer and creates workflow state ==" +T="${WORK}/resume" +setup_feature "${T}" "FEAT-001-alpha" +wf "${T}" resume FEAT-001-alpha >/dev/null +ptr="$(SDLC_ROOT="${T}" "${T}/agent-context/sdlc-pointer.sh" get)" +if [[ "${ptr}" == "FEAT-001-alpha" ]]; then ok "resume sets pointer"; else bad "pointer not set"; fi +if [[ -f "${T}/.sdlc/workflows/FEAT-001-alpha.state" ]]; then ok "workflow state created"; else bad "missing state file"; fi + +# --------------------------------------------------------------------------- +echo "== Test 2: advance moves through phases ==" +wf "${T}" advance >/dev/null +phase="$(grep '^phase=' "${T}/.sdlc/workflows/FEAT-001-alpha.state" | cut -d= -f2)" +if [[ "${phase}" == "analysis" ]]; then ok "advance to analysis"; else bad "expected analysis, got ${phase}"; fi + +# --------------------------------------------------------------------------- +echo "== Test 3: skip records reason and moves past phase ==" +wf "${T}" skip api-test --reason "no HTTP surface" >/dev/null +if grep -q '^skip_api-test=' "${T}/.sdlc/workflows/FEAT-001-alpha.state"; then + ok "skip recorded in state" +else + bad "skip not recorded" +fi + +# --------------------------------------------------------------------------- +echo "== Test 4: shelf and resume shelved work ==" +wf "${T}" shelf --reason "context switch" >/dev/null +ptr="$(SDLC_ROOT="${T}" "${T}/agent-context/sdlc-pointer.sh" get)" +if [[ -z "${ptr}" ]]; then ok "shelf clears pointer"; else bad "pointer should be empty"; fi +active="$(grep '^active=' "${T}/.sdlc/workflows/FEAT-001-alpha.state" | cut -d= -f2)" +if [[ "${active}" == "0" ]]; then ok "shelf marks inactive"; else bad "expected active=0"; fi + +setup_feature "${T}" "CHORE-002-beta" +wf "${T}" resume CHORE-002-beta >/dev/null +if wf "${T}" list-shelved | grep -q 'FEAT-001-alpha'; then ok "shelved list includes parked work"; else bad "shelved list missing FEAT-001-alpha"; fi +wf "${T}" resume FEAT-001-alpha >/dev/null +ptr="$(SDLC_ROOT="${T}" "${T}/agent-context/sdlc-pointer.sh" get)" +if [[ "${ptr}" == "FEAT-001-alpha" ]]; then ok "resume restores shelved pointer"; else bad "resume failed"; fi + +# --------------------------------------------------------------------------- +echo "== Test 5: sync infers phase from artifacts ==" +T="${WORK}/sync" +work_id="FEAT-003-gamma" +setup_feature "${T}" "${work_id}" +mkdir -p "${T}/requirements/milestones" +printf '# req\n' > "${T}/requirements/milestones/${work_id}.md" +printf '# analysis\n' > "${T}/spdd/analysis/${work_id}-analysis.md" +printf '# canvas\nReady For Coding\n' > "${T}/spdd/canvas/${work_id}.md" +wf "${T}" resume "${work_id}" >/dev/null +phase="$(grep '^phase=' "${T}/.sdlc/workflows/${work_id}.state" | cut -d= -f2)" +if [[ "${phase}" == "code" ]]; then ok "sync infers code from artifacts"; else bad "expected code, got ${phase}"; fi +if grep -q '^gate_canvas_exists=passed' "${T}/.sdlc/workflows/${work_id}.state"; then + ok "sync marks canvas gate passed" +else + bad "canvas gate not passed" +fi + +# --------------------------------------------------------------------------- +echo "== Test 6: status output is human-readable ==" +out="$(wf "${T}" status "${work_id}")" +if grep -q 'Quality gates:' <<< "${out}" && grep -q 'Phase track:' <<< "${out}"; then + ok "status shows gates and phase track" +else + bad "status output incomplete" +fi + +# --------------------------------------------------------------------------- +echo "== Test 7: session scripts update workflow timestamps ==" +T="${WORK}/integrate" +work_id="FEAT-004-delta" +setup_feature "${T}" "${work_id}" +"${START}" --target "${T}" --work-id "${work_id}" --phase plan >/dev/null +if grep -q '^last_session_at=' "${T}/.sdlc/workflows/${work_id}.state"; then + ok "start-agent-session touches workflow" +else + bad "missing last_session_at" +fi +"${CAPTURE}" --target "${T}" --work-id "${work_id}" --phase plan --summary "planned" >/dev/null +if grep -q '^last_capture_at=' "${T}/.sdlc/workflows/${work_id}.state"; then + ok "capture-session-memory records workflow capture" +else + bad "missing last_capture_at" +fi + +# --------------------------------------------------------------------------- +echo +echo "Results: ${pass} passed, ${fail} failed" +if [[ "${fail}" -gt 0 ]]; then + exit 1 +fi From 82ae84bdf7a7eccba125d127a6c276cb5fdbd5ab Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 27 Jun 2026 12:37:18 +0000 Subject: [PATCH 2/7] Improve SDLC workflow usability for humans and agents - Add scripts/sdlc.sh short entry point (default: next) - Add next, start, help, status --json to sdlc-workflow.sh - Auto-sync on status; pass gates when advancing phases - Embed workflow state table in session briefs - Add /sdlc-spdd-whereami across Cursor, Copilot, Claude - Update grounding files and adapter validation Co-authored-by: John Menke --- .github/workflows/test-sdlc-workflow.yml | 18 + agent-context/README.md | 14 + agent-context/sdlc-workflow.sh | 364 +++++++++++++++++- scripts/init-project.sh | 3 +- scripts/sdlc.sh | 28 ++ scripts/start-agent-session.sh | 9 + scripts/upgrade-project.sh | 3 +- scripts/validate-command-adapters.sh | 3 + scripts/verify-project-install.sh | 3 +- templates/claude/CLAUDE.md | 3 +- .../claude/commands/sdlc-spdd-whereami.md | 26 ++ templates/copilot/copilot-instructions.md | 3 +- .../prompts/sdlc-spdd-whereami.prompt.md | 24 ++ templates/cursor/rules/sdlc-spdd.mdc | 3 +- templates/cursor/sdlc-spdd-whereami.md | 21 + tests/test-adapter-install.sh | 2 +- tests/test-sdlc-workflow.sh | 48 +++ 17 files changed, 557 insertions(+), 18 deletions(-) create mode 100755 scripts/sdlc.sh create mode 100644 templates/claude/commands/sdlc-spdd-whereami.md create mode 100644 templates/copilot/prompts/sdlc-spdd-whereami.prompt.md create mode 100644 templates/cursor/sdlc-spdd-whereami.md diff --git a/.github/workflows/test-sdlc-workflow.yml b/.github/workflows/test-sdlc-workflow.yml index 88fb880..8ad1872 100644 --- a/.github/workflows/test-sdlc-workflow.yml +++ b/.github/workflows/test-sdlc-workflow.yml @@ -11,6 +11,14 @@ on: - 'scripts/init-project.sh' - 'scripts/upgrade-project.sh' - 'scripts/verify-project-install.sh' + - 'scripts/sdlc.sh' + - 'scripts/validate-command-adapters.sh' + - 'templates/cursor/sdlc-spdd-whereami.md' + - 'templates/copilot/prompts/sdlc-spdd-whereami.prompt.md' + - 'templates/claude/commands/sdlc-spdd-whereami.md' + - 'templates/cursor/rules/sdlc-spdd.mdc' + - 'templates/copilot/copilot-instructions.md' + - 'templates/claude/CLAUDE.md' - 'tests/test-sdlc-workflow.sh' - 'tests/test-sdlc-pointer.sh' - '.github/workflows/test-sdlc-workflow.yml' @@ -25,6 +33,14 @@ on: - 'scripts/init-project.sh' - 'scripts/upgrade-project.sh' - 'scripts/verify-project-install.sh' + - 'scripts/sdlc.sh' + - 'scripts/validate-command-adapters.sh' + - 'templates/cursor/sdlc-spdd-whereami.md' + - 'templates/copilot/prompts/sdlc-spdd-whereami.prompt.md' + - 'templates/claude/commands/sdlc-spdd-whereami.md' + - 'templates/cursor/rules/sdlc-spdd.mdc' + - 'templates/copilot/copilot-instructions.md' + - 'templates/claude/CLAUDE.md' - 'tests/test-sdlc-workflow.sh' - 'tests/test-sdlc-pointer.sh' - '.github/workflows/test-sdlc-workflow.yml' @@ -39,9 +55,11 @@ jobs: run: | bash -n agent-context/sdlc-pointer.sh bash -n agent-context/sdlc-workflow.sh + bash -n scripts/sdlc.sh bash -n tests/test-sdlc-workflow.sh bash -n scripts/start-agent-session.sh bash -n scripts/capture-session-memory.sh + bash -n scripts/validate-command-adapters.sh - name: Run SDLC pointer regression tests run: ./tests/test-sdlc-pointer.sh diff --git a/agent-context/README.md b/agent-context/README.md index 2b2da4e..a8f3914 100644 --- a/agent-context/README.md +++ b/agent-context/README.md @@ -54,6 +54,9 @@ Each work item also has a canonical canvas under `spdd/canvas/`. Keep both copie ## SDLC Pointer (current chore/task) +**Quick start:** `./scripts/sdlc.sh` (or `./scripts/sdlc.sh next`) shows what to do now. +In chat: `/sdlc-spdd-whereami`. + Agents can drift onto the wrong Work ID when several chores are open. The pointer manager keeps a single active chore in `.sdlc/pointer` (local state; not committed) and provides guarded wrappers so commands refuse to run against a stale pointer. @@ -79,6 +82,17 @@ sdlc_init ## SDLC Workflow (phase + gate tracking) +**Short commands** (installed at `scripts/sdlc-spdd/sdlc.sh`; orchestrator repo: `scripts/sdlc.sh`): + +```bash +./scripts/sdlc.sh # what to do now (default) +./scripts/sdlc.sh status # full dashboard (auto-syncs) +./scripts/sdlc.sh start # open session brief at current phase +./scripts/sdlc.sh resume FEAT-001-order-status-api +./scripts/sdlc.sh advance +./scripts/sdlc.sh shelf --reason "blocked" +``` + The workflow manager builds on the pointer to answer **where am I?**, **what is next?**, and **how do I shelf or resume work?** State lives under `.sdlc/workflows/` (local, gitignored). Committed artifacts (`progress-log.md`, canvas, reviews) remain the audit trail; diff --git a/agent-context/sdlc-workflow.sh b/agent-context/sdlc-workflow.sh index 9645b6c..b09e341 100755 --- a/agent-context/sdlc-workflow.sh +++ b/agent-context/sdlc-workflow.sh @@ -220,6 +220,311 @@ sdlc_workflow_recommended_command() { esac } +sdlc_workflow_shell_start() { + local work_id="${1:-}" + local phase="${2:-}" + local root="${SDLC_ROOT}" + if [[ -z "${work_id}" ]]; then + work_id="$(sdlc_get_pointer)" + fi + if [[ -z "${work_id}" ]]; then + echo "" + return 0 + fi + if [[ -z "${phase}" ]]; then + phase="$(_wf_read_state_var "$(_wf_state_file "${work_id}")" phase init)" + fi + if [[ -x "${root}/scripts/sdlc-spdd/start-agent-session.sh" ]]; then + echo "./scripts/sdlc-spdd/start-agent-session.sh --target . --work-id ${work_id} --phase ${phase}" + else + echo "./scripts/start-agent-session.sh --target . --work-id ${work_id} --phase ${phase}" + fi +} + +sdlc_workflow_shell_capture() { + local work_id="${1:-}" + local phase="${2:-}" + if [[ -z "${work_id}" ]]; then + work_id="$(sdlc_get_pointer)" + fi + if [[ -z "${work_id}" ]]; then + echo "" + return 0 + fi + if [[ -z "${phase}" ]]; then + phase="$(_wf_read_state_var "$(_wf_state_file "${work_id}")" phase resume)" + fi + if [[ -x "${SDLC_ROOT}/scripts/sdlc-spdd/capture-session-memory.sh" ]]; then + echo "./scripts/sdlc-spdd/capture-session-memory.sh --target . --work-id ${work_id} --phase ${phase} --summary \"\"" + else + echo "./scripts/capture-session-memory.sh --target . --work-id ${work_id} --phase ${phase} --summary \"\"" + fi +} + +_wf_gates_for_phase() { + case "${1:-}" in + analysis) printf '%s\n' requirement_documented ;; + plan) printf '%s\n' canvas_exists ;; + architect) printf '%s\n' architect_review operations_task_sized ;; + code) printf '%s\n' code_maps_to_ops tests_updated ;; + api-test) printf '%s\n' tests_updated ;; + review) printf '%s\n' review_completed safeguards_checked ;; + retro) printf '%s\n' retro_completed ;; + sync) printf '%s\n' canvas_synced ;; + *) ;; + esac +} + +_wf_pass_gates_for_phase() { + local work_id="$1" + local phase="$2" + local gate + while IFS= read -r gate; do + [[ -z "${gate}" ]] && continue + local current + current="$(_wf_read_state_var "$(_wf_state_file "${work_id}")" "gate_${gate}" pending)" + if [[ "${current}" != "skipped" ]]; then + _wf_set_state_var "${work_id}" "gate_${gate}" passed + fi + done < <(_wf_gates_for_phase "${phase}") +} + +_wf_json_escape() { + local s="${1:-}" + s="${s//\\/\\\\}" + s="${s//\"/\\\"}" + s="${s//$'\n'/\\n}" + s="${s//$'\r'/}" + printf '%s' "${s}" +} + +_wf_pending_gates() { + local file="$1" + local gi gate state label pending="" + for ((gi = 0; gi < ${#SDLC_GATE_NAMES[@]}; gi++)); do + gate="${SDLC_GATE_NAMES[$gi]}" + label="${SDLC_GATE_LABELS[$gi]}" + state="$(_wf_read_state_var "${file}" "gate_${gate}" pending)" + if [[ "${state}" == "pending" ]]; then + pending+="${label}"$'\n' + fi + done + printf '%s' "${pending%$'\n'}" +} + +sdlc_workflow_brief_markdown() { + local work_id="${1:-}" + if [[ -z "${work_id}" ]]; then + work_id="$(sdlc_get_pointer)" + fi + if [[ -z "${work_id}" ]]; then + echo "No active Work ID. Run \`./scripts/sdlc.sh resume \`." + return 0 + fi + + _wf_ensure_state "${work_id}" + local file phase operation active pending + file="$(_wf_state_file "${work_id}")" + phase="$(_wf_read_state_var "${file}" phase init)" + operation="$(_wf_read_state_var "${file}" operation)" + active="$(_wf_read_state_var "${file}" active 1)" + pending="$(_wf_pending_gates "${file}")" + + cat < # pick up or switch task" + echo " ./scripts/sdlc.sh list-shelved # see parked work" + sdlc_workflow_list_shelved | while IFS=$'\t' read -r wid ph at rs; do + echo " ./scripts/sdlc.sh resume ${wid} # shelved at ${ph}" + done + return 0 + fi + + sdlc_workflow_sync "${work_id}" >/dev/null + _wf_ensure_state "${work_id}" + + local file phase operation pending next_phase + file="$(_wf_state_file "${work_id}")" + phase="$(_wf_read_state_var "${file}" phase init)" + operation="$(_wf_read_state_var "${file}" operation)" + pending="$(_wf_pending_gates "${file}")" + next_phase="$(_wf_next_phase "${phase}")" + + echo "== SDLC: what to do now ==" + echo "Work ID: ${work_id}" + echo "Phase: ${phase} ($(( $(_wf_phase_index "${phase}") + 1 ))/${#SDLC_PHASE_ORDER[@]})" + [[ -n "${operation}" ]] && echo "Operation: ${operation}" + echo + echo "Do now (assistant):" + echo " $(sdlc_workflow_recommended_command "${phase}" "${work_id}")" + echo + echo "Or run in terminal:" + echo " $(sdlc_workflow_shell_start "${work_id}" "${phase}")" + echo + if [[ -n "${pending}" ]]; then + echo "Gates still open:" + while IFS= read -r line; do + [[ -z "${line}" ]] && continue + echo " [ ] ${line}" + done <<< "${pending}" + echo + fi + echo "When this phase is done:" + echo " ./scripts/sdlc.sh advance" + if [[ -n "${next_phase}" ]]; then + echo " (moves to: ${next_phase})" + fi + echo " $(sdlc_workflow_shell_capture "${work_id}" "${phase}")" + echo + if sdlc_workflow_list_shelved | grep -q .; then + echo "Shelved (switch with resume):" + sdlc_workflow_list_shelved | while IFS=$'\t' read -r wid ph at rs; do + echo " ${wid} @ ${ph}" + done + fi +} + +sdlc_workflow_start() { + local work_id + work_id="$(sdlc_get_pointer)" + if [[ -z "${work_id}" ]]; then + echo "sdlc_workflow_start: no active pointer — run: ./scripts/sdlc.sh resume " >&2 + return 2 + fi + sdlc_workflow_sync "${work_id}" >/dev/null + local phase start_script + phase="$(_wf_read_state_var "$(_wf_state_file "${work_id}")" phase init)" + start_script="${SDLC_ROOT}/scripts/sdlc-spdd/start-agent-session.sh" + if [[ ! -x "${start_script}" ]]; then + start_script="${SDLC_ROOT}/scripts/start-agent-session.sh" + fi + if [[ ! -x "${start_script}" ]]; then + echo "sdlc_workflow_start: start-agent-session.sh not found" >&2 + return 1 + fi + "${start_script}" --target "${SDLC_ROOT}" --work-id "${work_id}" --phase "${phase}" +} + +sdlc_workflow_status_json() { + local work_id="${1:-}" + if [[ -z "${work_id}" ]]; then + work_id="$(sdlc_get_pointer)" + fi + + local pointer phases_json gates_json shelved_json="" + pointer="$(sdlc_get_pointer)" + + phases_json="" + local p first=1 + for p in "${SDLC_PHASE_ORDER[@]}"; do + [[ "${first}" -eq 1 ]] || phases_json+="," + phases_json+="\"${p}\"" + first=0 + done + + gates_json="" + if [[ -n "${work_id}" ]]; then + _wf_ensure_state "${work_id}" + local file phase operation active gi gate state + file="$(_wf_state_file "${work_id}")" + phase="$(_wf_read_state_var "${file}" phase init)" + operation="$(_wf_read_state_var "${file}" operation)" + active="$(_wf_read_state_var "${file}" active 1)" + first=1 + gates_json="{" + for ((gi = 0; gi < ${#SDLC_GATE_NAMES[@]}; gi++)); do + gate="${SDLC_GATE_NAMES[$gi]}" + state="$(_wf_read_state_var "${file}" "gate_${gate}" pending)" + [[ "${first}" -eq 1 ]] || gates_json+="," + gates_json+="\"${gate}\":\"${state}\"" + first=0 + done + gates_json+="}" + + printf '{' + printf '"pointer":"%s",' "$(_wf_json_escape "${pointer}")" + printf '"work_id":"%s",' "$(_wf_json_escape "${work_id}")" + printf '"active":%s,' "$([[ "${active}" == "1" ]] && echo true || echo false)" + printf '"phase":"%s",' "$(_wf_json_escape "${phase}")" + printf '"phase_index":%s,' "$(_wf_phase_index "${phase}")" + printf '"phase_total":%s,' "${#SDLC_PHASE_ORDER[@]}" + printf '"operation":"%s",' "$(_wf_json_escape "${operation}")" + printf '"recommended_command":"%s",' "$(_wf_json_escape "$(sdlc_workflow_recommended_command "${phase}" "${work_id}")")" + printf '"shell_start":"%s",' "$(_wf_json_escape "$(sdlc_workflow_shell_start "${work_id}" "${phase}")")" + printf '"shell_capture":"%s",' "$(_wf_json_escape "$(sdlc_workflow_shell_capture "${work_id}" "${phase}")")" + printf '"phases":[%s],' "${phases_json}" + printf '"gates":%s' "${gates_json}" + printf '}\n' + return 0 + fi + + printf '{"pointer":"%s","work_id":null,"phases":[%s],"shelved":[' "$(_wf_json_escape "${pointer}")" "${phases_json}" + first=1 + while IFS=$'\t' read -r wid ph at rs; do + [[ -z "${wid}" ]] && continue + [[ "${first}" -eq 1 ]] || printf ',' + first=0 + printf '{"work_id":"%s","phase":"%s","shelved_at":"%s","reason":"%s"}' \ + "$(_wf_json_escape "${wid}")" "$(_wf_json_escape "${ph}")" \ + "$(_wf_json_escape "${at}")" "$(_wf_json_escape "${rs}")" + done < <(sdlc_workflow_list_shelved) + printf ']}\n' +} + +sdlc_workflow_help() { + cat <<'EOF' +SDLC workflow helper — short paths for humans and agents + + ./scripts/sdlc.sh # full status (auto-syncs from artifacts) + ./scripts/sdlc.sh next # concise "what do I do now?" + ./scripts/sdlc.sh start # open session brief at current phase + ./scripts/sdlc.sh status --json + + ./scripts/sdlc.sh resume [--phase PHASE] + ./scripts/sdlc.sh advance [--to PHASE] + ./scripts/sdlc.sh skip --reason "why" + ./scripts/sdlc.sh shelf --reason "why" + ./scripts/sdlc.sh sync [--work-id ID] + ./scripts/sdlc.sh list-shelved + +In chat: /sdlc-spdd-whereami + +Typical loop: + 1. ./scripts/sdlc.sh next + 2. run the assistant command (or ./scripts/sdlc.sh start) + 3. ./scripts/sdlc.sh advance + 4. capture-session-memory.sh +EOF +} + _wf_infer_phase_from_artifacts() { local work_id="$1" local root="${SDLC_ROOT}" @@ -446,8 +751,8 @@ sdlc_workflow_resume() { resolved_phase="$(_wf_read_state_var "$(_wf_state_file "${work_id}")" phase init)" echo "Resumed ${work_id} at phase: ${resolved_phase}" echo "Recommended command: $(sdlc_workflow_recommended_command "${resolved_phase}" "${work_id}")" - echo "Start session:" - echo " ./scripts/sdlc-spdd/start-agent-session.sh --target . --work-id ${work_id} --phase ${resolved_phase}" + echo "Quick check: ./scripts/sdlc.sh next" + echo "Start session: $(sdlc_workflow_shell_start "${work_id}" "${resolved_phase}")" } sdlc_workflow_advance() { @@ -485,10 +790,12 @@ sdlc_workflow_advance() { fi fi + _wf_pass_gates_for_phase "${work_id}" "${current}" _wf_set_state_var "${work_id}" phase "${next}" _wf_log_history "${work_id}" advance "${current}->${next}" echo "Advanced ${work_id}: ${current} -> ${next}" echo "Recommended command: $(sdlc_workflow_recommended_command "${next}" "${work_id}")" + echo "Quick check: ./scripts/sdlc.sh next" } sdlc_workflow_skip() { @@ -553,7 +860,7 @@ sdlc_workflow_shelf() { sdlc_reset_pointer >/dev/null echo "Shelved ${work_id}" echo "Reason: ${reason}" - echo "Resume later: ./agent-context/sdlc-workflow.sh resume ${work_id}" + echo "Resume later: ./scripts/sdlc.sh resume ${work_id}" } sdlc_workflow_list_shelved() { @@ -679,12 +986,14 @@ sdlc_workflow_status() { echo "Next step:" echo " $(sdlc_workflow_recommended_command "${phase}" "${work_id}")" echo - echo "Commands:" - echo " ./agent-context/sdlc-workflow.sh advance # move to next phase" - echo " ./agent-context/sdlc-workflow.sh skip api-test --reason \"no HTTP surface\"" - echo " ./agent-context/sdlc-workflow.sh shelf --reason \"blocked on review\"" - echo " ./agent-context/sdlc-workflow.sh sync # re-read artifacts" - echo " ./agent-context/sdlc-workflow.sh resume # pick up shelved work" + echo "Quick commands:" + echo " ./scripts/sdlc.sh next # concise what-to-do-now" + echo " ./scripts/sdlc.sh start # open session brief" + echo " ./scripts/sdlc.sh advance # move to next phase" + echo " ./scripts/sdlc.sh skip api-test --reason \"...\"" + echo " ./scripts/sdlc.sh shelf --reason \"...\"" + echo " ./scripts/sdlc.sh sync # re-read artifacts" + echo " ./scripts/sdlc.sh resume # pick up shelved work" } # CLI when script executed directly @@ -694,13 +1003,45 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then case "${cmd}" in status|/sdlc-workflow-status) work_id="" + format="text" + do_sync=1 while [[ $# -gt 0 ]]; do case "$1" in --work-id) work_id="${2:-}"; shift 2 ;; + --json) format="json"; shift ;; + --no-sync) do_sync=0; shift ;; + --sync) do_sync=1; shift ;; + -h|--help) sdlc_workflow_help; exit 0 ;; *) work_id="${1}"; shift ;; esac done - sdlc_workflow_status "${work_id}" + if [[ -z "${work_id}" ]]; then + work_id="$(sdlc_get_pointer)" + fi + if [[ "${do_sync}" -eq 1 && -n "${work_id}" ]]; then + sdlc_workflow_sync "${work_id}" >/dev/null + fi + if [[ "${format}" == "json" ]]; then + sdlc_workflow_status_json "${work_id}" + else + sdlc_workflow_status "${work_id}" + fi + ;; + next|/sdlc-workflow-next) + work_id="" + while [[ $# -gt 0 ]]; do + case "$1" in + --work-id) work_id="${2:-}"; shift 2 ;; + *) work_id="${1}"; shift ;; + esac + done + sdlc_workflow_next "${work_id}" + ;; + start|/sdlc-workflow-start) + sdlc_workflow_start + ;; + help|-h|--help) + sdlc_workflow_help ;; resume|/sdlc-workflow-resume) work_id="${1:-}"; shift || true @@ -760,7 +1101,8 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then sdlc_workflow_list_shelved ;; *) - echo "Usage: $0 {status|resume|advance|skip|shelf|sync|list-shelved} ..." >&2 + echo "Usage: $0 {status|next|start|resume|advance|skip|shelf|sync|list-shelved|help} ..." >&2 + echo "Try: $0 help" >&2 exit 2 ;; esac diff --git a/scripts/init-project.sh b/scripts/init-project.sh index 77f6b19..ed324bd 100755 --- a/scripts/init-project.sh +++ b/scripts/init-project.sh @@ -268,7 +268,8 @@ for file in \ validate-command-adapters.sh \ verify-agent-command-effects.sh \ validate-reasons-canvas.sh \ - verify-project-install.sh; do + verify-project-install.sh \ + sdlc.sh; do copy_if_missing \ "${REPO_ROOT}/scripts/${file}" \ "${TARGET}/scripts/sdlc-spdd/${file}" diff --git a/scripts/sdlc.sh b/scripts/sdlc.sh new file mode 100755 index 0000000..0e0ddc4 --- /dev/null +++ b/scripts/sdlc.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# Short entry point for SDLC pointer + workflow helpers. +# Installed to scripts/sdlc-spdd/sdlc.sh in target projects; lives at scripts/sdlc.sh in the orchestrator repo. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +if [[ -f "${SCRIPT_DIR}/../agent-context/sdlc-workflow.sh" ]]; then + ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +elif [[ -f "${SCRIPT_DIR}/../../agent-context/sdlc-workflow.sh" ]]; then + ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" +else + ROOT="$(git -C "${PWD}" rev-parse --show-toplevel 2>/dev/null || pwd)" +fi + +WORKFLOW="${ROOT}/agent-context/sdlc-workflow.sh" +if [[ ! -x "${WORKFLOW}" ]]; then + echo "sdlc: workflow not installed (${WORKFLOW})" >&2 + echo "Run setup-agent-prompts.sh or upgrade-project.sh from the orchestrator repo." >&2 + exit 1 +fi + +export SDLC_ROOT="${ROOT}" +cmd="${1:-next}" +if [[ $# -gt 0 ]]; then + shift +fi +exec "${WORKFLOW}" "${cmd}" "$@" diff --git a/scripts/start-agent-session.sh b/scripts/start-agent-session.sh index 468f600..2751bd3 100755 --- a/scripts/start-agent-session.sh +++ b/scripts/start-agent-session.sh @@ -78,11 +78,14 @@ if [[ -f "${pointer_script}" && -n "${WORK_ID}" ]]; then fi workflow_script="${TARGET}/agent-context/sdlc-workflow.sh" +workflow_brief_md="Workflow tools not installed." if [[ -f "${workflow_script}" && -n "${WORK_ID}" ]]; then SDLC_ROOT="${TARGET}" # shellcheck source=/dev/null source "${workflow_script}" sdlc_workflow_touch_session "${WORK_ID}" "${PHASE}" "${MILESTONE}" + sdlc_workflow_sync "${WORK_ID}" >/dev/null 2>&1 || true + workflow_brief_md="$(sdlc_workflow_brief_markdown "${WORK_ID}")" fi timestamp="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" @@ -310,6 +313,12 @@ cat > "${session_file}" </progress-log.md` and `spdd/canvas/.md` — read those, not the global history. Sessions for unrelated work are interleaved in time, so never read history top-to-bottom. 3. Discover related work by code area or domain keyword, not by scanning. Filter `agent-context/memory/domain-index.md` by Domain Keywords from the analysis artifact, then `context-index.md` and `session-index.md` by Area (Kinds: analysis, session, decision, pitfall, pattern). Read matches newest-first. Full per-session detail is in `agent-context/memory/sessions/`. For static playbooks and harness files, use `agent-context/memory/phase-index.md` by phase or `./scripts/sdlc-spdd/resolve-agent-context.sh --phase `. `session-history.md` is only a recent chronological overview (older entries archived under `agent-context/memory/archive/`). Do not read whole directories. When capturing: read `agent-context/memory/code-areas.md`; `capture-session-memory.sh` parses session documents/content (summary, session-notes, current-session.md, latest timestamped session brief, analysis, canvas, progress log, capture flags) for path/package tokens, matches known categories, and registers new ones. After `/sdlc-spdd-analysis`, run `index-spdd-analysis.sh` to index domain keywords. Use `--areas` only to override or supplement parsed categories. diff --git a/templates/claude/commands/sdlc-spdd-whereami.md b/templates/claude/commands/sdlc-spdd-whereami.md new file mode 100644 index 0000000..66216c1 --- /dev/null +++ b/templates/claude/commands/sdlc-spdd-whereami.md @@ -0,0 +1,26 @@ +--- +description: Show current SDLC phase, gates, and the single best next action. +argument-hint: +--- + +# /sdlc-spdd-whereami + +You are the SDLC-SPDD Workflow Orientation Agent. + +Your job is to show the user exactly where they are in the SDLC-SPDD workflow and what to do next. + +Do not implement code. + +## Required Behavior + +1. Run `./scripts/sdlc-spdd/sdlc.sh next` (or `./scripts/sdlc.sh next` in the orchestrator repo). +2. Read the output: active Work ID, phase, open gates, and recommended command. +3. If no active Work ID, list shelved work and suggest `./scripts/sdlc-spdd/sdlc.sh resume `. +4. Summarize status in plain language and offer the single best next action. +5. Do not start unrelated work; stay on the pointer Work ID unless the user asks to switch. + +## Output + +- Short orientation summary (Work ID, phase, progress) +- The recommended assistant command or shell command to run next +- Optional: remind user to run `./scripts/sdlc-spdd/sdlc.sh advance` after completing the phase diff --git a/templates/copilot/copilot-instructions.md b/templates/copilot/copilot-instructions.md index b19193e..9844d43 100644 --- a/templates/copilot/copilot-instructions.md +++ b/templates/copilot/copilot-instructions.md @@ -22,6 +22,7 @@ The matching slash commands live in `.github/prompts/` (invoke in Copilot Chat): /sdlc-spdd-prompt-update /sdlc-spdd-retro /sdlc-spdd-sync + /sdlc-spdd-whereami If slash commands are not listed, reference a prompt file: `#prompt:sdlc-spdd-analysis` @@ -47,7 +48,7 @@ Use progressive disclosure ([SDLC Agents](https://github.com/dsilahcilar/sdlc-ag Load context by index, not by scanning. Keep working context small and relevant regardless of project size. See `docs/sdlc-spdd/context-loading-and-scaling.md`. -1. Start at `agent-context/sessions/current-session.md` to resume the active Work ID and phase. If it is missing, read the most recent brief in `agent-context/sessions/` or the indexes in `agent-context/memory/`. +1. Start at `agent-context/sessions/current-session.md` to resume the active Work ID and phase. For a quick orientation, run `./scripts/sdlc-spdd/sdlc.sh next` or invoke `/sdlc-spdd-whereami`. If it is missing, read the most recent brief in `agent-context/sessions/` or the indexes in `agent-context/memory/`. 2. Retrieve by relevance, not recency. A Work ID's own history is its `agent-context/features//progress-log.md` and `spdd/canvas/.md` — read those, not the global history. Sessions for unrelated work are interleaved in time, so never read history top-to-bottom. 3. Discover related work by code area or domain keyword, not by scanning. Filter `agent-context/memory/domain-index.md` by Domain Keywords from the analysis artifact, then `context-index.md` and `session-index.md` by Area (Kinds: analysis, session, decision, pitfall, pattern). Read matches newest-first. Full per-session detail is in `agent-context/memory/sessions/`. For static playbooks and harness files, use `agent-context/memory/phase-index.md` by phase or `./scripts/sdlc-spdd/resolve-agent-context.sh --phase `. `session-history.md` is only a recent chronological overview (older entries archived under `agent-context/memory/archive/`). Do not read whole directories. When capturing: read `agent-context/memory/code-areas.md`; `capture-session-memory.sh` parses session documents/content (summary, session-notes, current-session.md, latest timestamped session brief, analysis, canvas, progress log, capture flags) for path/package tokens, matches known categories, and registers new ones. After `/sdlc-spdd-analysis`, run `index-spdd-analysis.sh` to index domain keywords. Use `--areas` only to override or supplement parsed categories. diff --git a/templates/copilot/prompts/sdlc-spdd-whereami.prompt.md b/templates/copilot/prompts/sdlc-spdd-whereami.prompt.md new file mode 100644 index 0000000..bcf9c7a --- /dev/null +++ b/templates/copilot/prompts/sdlc-spdd-whereami.prompt.md @@ -0,0 +1,24 @@ +--- +description: Show current SDLC phase, gates, and the single best next action. +mode: agent +--- + +# SDLC-SPDD Where Am I + +You are the SDLC-SPDD Workflow Orientation Agent. + +Show the user exactly where they are in the workflow and what to do next. Do not implement code. + +## Required Behavior + +1. Run `./scripts/sdlc-spdd/sdlc.sh next` (or `./scripts/sdlc.sh next` in the orchestrator repo). +2. Read the output: active Work ID, phase, open gates, and recommended command. +3. If no active Work ID, list shelved work and suggest `./scripts/sdlc-spdd/sdlc.sh resume `. +4. Summarize status in plain language and offer the single best next action. +5. Do not start unrelated work; stay on the pointer Work ID unless the user asks to switch. + +## Output + +- Short orientation summary (Work ID, phase, progress) +- The recommended assistant command or shell command to run next +- Optional: remind user to run `./scripts/sdlc-spdd/sdlc.sh advance` after completing the phase diff --git a/templates/cursor/rules/sdlc-spdd.mdc b/templates/cursor/rules/sdlc-spdd.mdc index f2afb21..5d75548 100644 --- a/templates/cursor/rules/sdlc-spdd.mdc +++ b/templates/cursor/rules/sdlc-spdd.mdc @@ -28,6 +28,7 @@ The matching slash commands live in `.cursor/commands/`: /sdlc-spdd-prompt-update /sdlc-spdd-retro /sdlc-spdd-sync + /sdlc-spdd-whereami Preserve context by reading relevant artifacts before answering: @@ -51,7 +52,7 @@ Use progressive disclosure ([SDLC Agents](https://github.com/dsilahcilar/sdlc-ag Load context by index, not by scanning. Keep working context small and relevant regardless of project size. See `docs/sdlc-spdd/context-loading-and-scaling.md`. -1. Start at `agent-context/sessions/current-session.md` to resume the active Work ID and phase. If it is missing, read the most recent brief in `agent-context/sessions/` or the indexes in `agent-context/memory/`. +1. Start at `agent-context/sessions/current-session.md` to resume the active Work ID and phase. For a quick orientation, run `./scripts/sdlc-spdd/sdlc.sh next` or invoke `/sdlc-spdd-whereami`. If it is missing, read the most recent brief in `agent-context/sessions/` or the indexes in `agent-context/memory/`. 2. Retrieve by relevance, not recency. A Work ID's own history is its `agent-context/features//progress-log.md` and `spdd/canvas/.md` — read those, not the global history. Sessions for unrelated work are interleaved in time, so never read history top-to-bottom. 3. Discover related work by code area or domain keyword, not by scanning. Filter `agent-context/memory/domain-index.md` by Domain Keywords from the analysis artifact, then `context-index.md` and `session-index.md` by Area (Kinds: analysis, session, decision, pitfall, pattern). Read matches newest-first. Full per-session detail is in `agent-context/memory/sessions/`. For static playbooks and harness files, use `agent-context/memory/phase-index.md` by phase or `./scripts/sdlc-spdd/resolve-agent-context.sh --phase `. `session-history.md` is only a recent chronological overview (older entries archived under `agent-context/memory/archive/`). Do not read whole directories. When capturing: read `agent-context/memory/code-areas.md`; `capture-session-memory.sh` parses session documents/content (summary, session-notes, current-session.md, latest timestamped session brief, analysis, canvas, progress log, capture flags) for path/package tokens, matches known categories, and registers new ones. After `/sdlc-spdd-analysis`, run `index-spdd-analysis.sh` to index domain keywords. Use `--areas` only to override or supplement parsed categories. diff --git a/templates/cursor/sdlc-spdd-whereami.md b/templates/cursor/sdlc-spdd-whereami.md new file mode 100644 index 0000000..233c16c --- /dev/null +++ b/templates/cursor/sdlc-spdd-whereami.md @@ -0,0 +1,21 @@ +# /sdlc-spdd-whereami + +You are the SDLC-SPDD Workflow Orientation Agent. + +Your job is to show the user exactly where they are in the SDLC-SPDD workflow and what to do next. + +Do not implement code. + +## Required Behavior + +1. Run `./scripts/sdlc-spdd/sdlc.sh next` (or `./scripts/sdlc.sh next` in the orchestrator repo). +2. Read the output: active Work ID, phase, open gates, and recommended command. +3. If no active Work ID, list shelved work and suggest `./scripts/sdlc-spdd/sdlc.sh resume `. +4. Summarize status in plain language and offer the single best next action. +5. Do not start unrelated work; stay on the pointer Work ID unless the user asks to switch. + +## Output + +- Short orientation summary (Work ID, phase, progress) +- The recommended assistant command or shell command to run next +- Optional: remind user to run `./scripts/sdlc-spdd/sdlc.sh advance` after completing the phase diff --git a/tests/test-adapter-install.sh b/tests/test-adapter-install.sh index ee35fb4..5bd6de8 100755 --- a/tests/test-adapter-install.sh +++ b/tests/test-adapter-install.sh @@ -85,7 +85,7 @@ expect_fail() { if "$@" >/dev/null 2>&1; then bad "expected FAIL but passed: ${label}"; else ok "correctly fails: ${label}"; fi } -commands=(init analysis plan architect code api-test review prompt-update retro sync) +commands=(init analysis plan architect code api-test review prompt-update retro sync whereami) assert_cursor_pack() { local t="$1" diff --git a/tests/test-sdlc-workflow.sh b/tests/test-sdlc-workflow.sh index 81d9c69..303642a 100755 --- a/tests/test-sdlc-workflow.sh +++ b/tests/test-sdlc-workflow.sh @@ -118,6 +118,54 @@ else bad "missing last_capture_at" fi +# --------------------------------------------------------------------------- +echo "== Test 8: next command gives actionable output ==" +T="${WORK}/next" +work_id="FEAT-005-next" +setup_feature "${T}" "${work_id}" +printf '# canvas\nReady For Coding\n' > "${T}/spdd/canvas/${work_id}.md" +wf "${T}" resume "${work_id}" >/dev/null +out="$(wf "${T}" next)" +if grep -q 'Do now (assistant):' <<< "${out}" && grep -q 'When this phase is done:' <<< "${out}"; then + ok "next output is actionable" +else + bad "next output missing sections" +fi + +# --------------------------------------------------------------------------- +echo "== Test 9: status --json for agents ==" +json="$(wf "${T}" status --json)" +if grep -q '"phase":"code"' <<< "${json}" && grep -q '"recommended_command"' <<< "${json}"; then + ok "json status includes phase and command" +else + bad "json status incomplete: ${json}" +fi + +# --------------------------------------------------------------------------- +echo "== Test 10: sdlc.sh wrapper delegates ==" +T="${WORK}/wrapper" +work_id="FEAT-005-wrap" +setup_feature "${T}" "${work_id}" +mkdir -p "${T}/scripts/sdlc-spdd" +cp "${REPO_ROOT}/scripts/sdlc.sh" "${T}/scripts/sdlc-spdd/sdlc.sh" +chmod +x "${T}/scripts/sdlc-spdd/sdlc.sh" +SDLC_ROOT="${T}" "${T}/scripts/sdlc-spdd/sdlc.sh" resume "${work_id}" >/dev/null +out="$(SDLC_ROOT="${T}" "${T}/scripts/sdlc-spdd/sdlc.sh" next)" +if grep -q "${work_id}" <<< "${out}"; then ok "sdlc.sh wrapper works"; else bad "sdlc.sh wrapper failed"; fi + +# --------------------------------------------------------------------------- +echo "== Test 11: session brief includes workflow state ==" +T="${WORK}/brief" +work_id="FEAT-006-brief" +setup_feature "${T}" "${work_id}" +"${START}" --target "${T}" --work-id "${work_id}" --phase plan >/dev/null +if grep -q '## Workflow State' "${T}/agent-context/sessions/current-session.md" \ + && grep -q 'Assistant command' "${T}/agent-context/sessions/current-session.md"; then + ok "session brief embeds workflow state" +else + bad "session brief missing workflow state" +fi + # --------------------------------------------------------------------------- echo echo "Results: ${pass} passed, ${fail} failed" From 9558dc55bb60f7c05b4849603d1b6207f650dd02 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 27 Jun 2026 12:53:28 +0000 Subject: [PATCH 3/7] Infer next canvas operation and add guarded capture wrapper - Parse REASONS Canvas Operations for first incomplete T## (e.g. T03) - Surface operation in next/status/json/session brief recommendations - Add sdlc.sh capture with run_against_pointer guard - Auto-sync operation field during workflow sync in code phase Co-authored-by: John Menke --- agent-context/README.md | 3 + agent-context/sdlc-workflow.sh | 217 ++++++++++++++++++++++++++++++--- tests/test-sdlc-workflow.sh | 42 +++++++ 3 files changed, 245 insertions(+), 17 deletions(-) diff --git a/agent-context/README.md b/agent-context/README.md index a8f3914..9207cff 100644 --- a/agent-context/README.md +++ b/agent-context/README.md @@ -91,8 +91,11 @@ sdlc_init ./scripts/sdlc.sh resume FEAT-001-order-status-api ./scripts/sdlc.sh advance ./scripts/sdlc.sh shelf --reason "blocked" +./scripts/sdlc.sh capture --summary "finished T02" # pointer-guarded ``` +In **code** phase, the next canvas operation (`T01`, `T02`, …) is inferred automatically from the REASONS Canvas. + The workflow manager builds on the pointer to answer **where am I?**, **what is next?**, and **how do I shelf or resume work?** State lives under `.sdlc/workflows/` (local, gitignored). Committed artifacts (`progress-log.md`, canvas, reviews) remain the audit trail; diff --git a/agent-context/sdlc-workflow.sh b/agent-context/sdlc-workflow.sh index b09e341..6e1d84e 100755 --- a/agent-context/sdlc-workflow.sh +++ b/agent-context/sdlc-workflow.sh @@ -204,12 +204,16 @@ _wf_ensure_state() { sdlc_workflow_recommended_command() { local phase="${1:-init}" local work_id="${2:-}" + local operation="${3:-}" + if [[ -z "${operation}" && -n "${work_id}" ]]; then + operation="$(_wf_resolve_operation "${work_id}" "${phase}")" + fi case "${phase}" in init) echo "/sdlc-spdd-init" ;; analysis) echo "/sdlc-spdd-analysis @requirements/.md" ;; plan) echo "/sdlc-spdd-plan @spdd/analysis/${work_id:-}-analysis.md" ;; architect) echo "/sdlc-spdd-architect @spdd/canvas/${work_id:-}.md" ;; - code) echo "/sdlc-spdd-code @spdd/canvas/${work_id:-}.md operation " ;; + code) echo "/sdlc-spdd-code @spdd/canvas/${work_id:-}.md operation ${operation:-}" ;; api-test) echo "/sdlc-spdd-api-test @spdd/canvas/${work_id:-}.md" ;; review) echo "/sdlc-spdd-review @spdd/canvas/${work_id:-}.md" ;; prompt-update) echo "/sdlc-spdd-prompt-update @spdd/canvas/${work_id:-}.md" ;; @@ -244,21 +248,19 @@ sdlc_workflow_shell_start() { sdlc_workflow_shell_capture() { local work_id="${1:-}" local phase="${2:-}" + local helper + helper="$(_wf_shell_helper_path)" if [[ -z "${work_id}" ]]; then work_id="$(sdlc_get_pointer)" fi if [[ -z "${work_id}" ]]; then - echo "" + echo "${helper} capture --summary \"\"" return 0 fi if [[ -z "${phase}" ]]; then phase="$(_wf_read_state_var "$(_wf_state_file "${work_id}")" phase resume)" fi - if [[ -x "${SDLC_ROOT}/scripts/sdlc-spdd/capture-session-memory.sh" ]]; then - echo "./scripts/sdlc-spdd/capture-session-memory.sh --target . --work-id ${work_id} --phase ${phase} --summary \"\"" - else - echo "./scripts/capture-session-memory.sh --target . --work-id ${work_id} --phase ${phase} --summary \"\"" - fi + echo "${helper} capture --phase ${phase} --summary \"\"" } _wf_gates_for_phase() { @@ -323,12 +325,16 @@ sdlc_workflow_brief_markdown() { fi _wf_ensure_state "${work_id}" - local file phase operation active pending + local file phase operation active pending op_title file="$(_wf_state_file "${work_id}")" phase="$(_wf_read_state_var "${file}" phase init)" operation="$(_wf_read_state_var "${file}" operation)" active="$(_wf_read_state_var "${file}" active 1)" pending="$(_wf_pending_gates "${file}")" + op_title="" + if [[ -n "${operation}" ]]; then + op_title="$(_wf_operation_title "${work_id}" "${operation}")" + fi cat <"\` | | Orient / status | \`./scripts/sdlc.sh next\` or \`/sdlc-spdd-whereami\` | $(if [[ -n "${pending}" ]]; then @@ -371,20 +378,28 @@ sdlc_workflow_next() { sdlc_workflow_sync "${work_id}" >/dev/null _wf_ensure_state "${work_id}" - local file phase operation pending next_phase + local file phase operation pending next_phase op_title file="$(_wf_state_file "${work_id}")" phase="$(_wf_read_state_var "${file}" phase init)" operation="$(_wf_read_state_var "${file}" operation)" pending="$(_wf_pending_gates "${file}")" next_phase="$(_wf_next_phase "${phase}")" + op_title="" + if [[ -n "${operation}" ]]; then + op_title="$(_wf_operation_title "${work_id}" "${operation}")" + fi echo "== SDLC: what to do now ==" echo "Work ID: ${work_id}" echo "Phase: ${phase} ($(( $(_wf_phase_index "${phase}") + 1 ))/${#SDLC_PHASE_ORDER[@]})" - [[ -n "${operation}" ]] && echo "Operation: ${operation}" + if [[ -n "${operation}" ]]; then + echo "Next operation: ${op_title}" + elif [[ "${phase}" == "code" ]]; then + echo "Next operation: (all canvas operations complete — advance or review)" + fi echo echo "Do now (assistant):" - echo " $(sdlc_workflow_recommended_command "${phase}" "${work_id}")" + echo " $(sdlc_workflow_recommended_command "${phase}" "${work_id}" "${operation}")" echo echo "Or run in terminal:" echo " $(sdlc_workflow_shell_start "${work_id}" "${phase}")" @@ -469,6 +484,11 @@ sdlc_workflow_status_json() { done gates_json+="}" + local op_title="" + if [[ -n "${operation}" ]]; then + op_title="$(_wf_operation_title "${work_id}" "${operation}")" + fi + printf '{' printf '"pointer":"%s",' "$(_wf_json_escape "${pointer}")" printf '"work_id":"%s",' "$(_wf_json_escape "${work_id}")" @@ -477,7 +497,8 @@ sdlc_workflow_status_json() { printf '"phase_index":%s,' "$(_wf_phase_index "${phase}")" printf '"phase_total":%s,' "${#SDLC_PHASE_ORDER[@]}" printf '"operation":"%s",' "$(_wf_json_escape "${operation}")" - printf '"recommended_command":"%s",' "$(_wf_json_escape "$(sdlc_workflow_recommended_command "${phase}" "${work_id}")")" + printf '"operation_title":"%s",' "$(_wf_json_escape "${op_title}")" + printf '"recommended_command":"%s",' "$(_wf_json_escape "$(sdlc_workflow_recommended_command "${phase}" "${work_id}" "${operation}")")" printf '"shell_start":"%s",' "$(_wf_json_escape "$(sdlc_workflow_shell_start "${work_id}" "${phase}")")" printf '"shell_capture":"%s",' "$(_wf_json_escape "$(sdlc_workflow_shell_capture "${work_id}" "${phase}")")" printf '"phases":[%s],' "${phases_json}" @@ -506,6 +527,7 @@ SDLC workflow helper — short paths for humans and agents ./scripts/sdlc.sh # full status (auto-syncs from artifacts) ./scripts/sdlc.sh next # concise "what do I do now?" ./scripts/sdlc.sh start # open session brief at current phase + ./scripts/sdlc.sh capture --summary "..." # guarded capture (pointer must match) ./scripts/sdlc.sh status --json ./scripts/sdlc.sh resume [--phase PHASE] @@ -521,7 +543,9 @@ Typical loop: 1. ./scripts/sdlc.sh next 2. run the assistant command (or ./scripts/sdlc.sh start) 3. ./scripts/sdlc.sh advance - 4. capture-session-memory.sh + 4. ./scripts/sdlc.sh capture --summary "..." + +Code phase: next operation (T01, T02, ...) is read from the REASONS Canvas automatically. EOF } @@ -639,6 +663,104 @@ _wf_resolve_phase() { fi } +_wf_canvas_path() { + local work_id="$1" + local root="${SDLC_ROOT}" + local canvas="${root}/spdd/canvas/${work_id}.md" + if [[ ! -f "${canvas}" ]]; then + canvas="${root}/agent-context/features/${work_id}/reasons-canvas.md" + fi + if [[ -f "${canvas}" ]]; then + printf '%s' "${canvas}" + fi +} + +_wf_infer_next_operation() { + local work_id="$1" + local canvas + canvas="$(_wf_canvas_path "${work_id}")" + [[ -n "${canvas}" ]] || return 0 + + awk ' + BEGIN { op = ""; have_status = 0; complete = 0 } + /^### T[0-9]{2}/ { + if (op != "" && (!have_status || !complete)) { + print op + exit + } + op = $2 + sub(/-.*/, "", op) + have_status = 0 + complete = 0 + next + } + op != "" && /^- Status:/ { + have_status = 1 + line = tolower($0) + if (line ~ /complete|: done/) { + complete = 1 + } else { + complete = 0 + } + next + } + END { + if (op != "" && (!have_status || !complete)) { + print op + } + } + ' "${canvas}" +} + +_wf_operation_title() { + local work_id="$1" + local operation="$2" + local canvas line + canvas="$(_wf_canvas_path "${work_id}")" + [[ -n "${canvas}" && -n "${operation}" ]] || return 0 + line="$(grep -m1 "^### ${operation} " "${canvas}" 2>/dev/null || true)" + if [[ -n "${line}" ]]; then + printf '%s' "${line#### }" + else + printf '%s' "${operation}" + fi +} + +_wf_sync_operation() { + local work_id="$1" + local phase="$2" + local next_op="" + case "${phase}" in + code|review|architect) + next_op="$(_wf_infer_next_operation "${work_id}")" + ;; + esac + if [[ -n "${next_op}" ]]; then + _wf_set_state_var "${work_id}" operation "${next_op}" + else + _wf_set_state_var "${work_id}" operation "" + fi +} + +_wf_resolve_operation() { + local work_id="$1" + local phase="$2" + local operation + operation="$(_wf_read_state_var "$(_wf_state_file "${work_id}")" operation)" + if [[ -z "${operation}" && "${phase}" == "code" ]]; then + operation="$(_wf_infer_next_operation "${work_id}")" + fi + printf '%s' "${operation}" +} + +_wf_shell_helper_path() { + if [[ -x "${SDLC_ROOT}/scripts/sdlc-spdd/sdlc.sh" ]]; then + echo "./scripts/sdlc-spdd/sdlc.sh" + else + echo "./scripts/sdlc.sh" + fi +} + _wf_sync_impl() { local work_id="$1" local file stored inferred resolved gate_line gate value current @@ -659,7 +781,8 @@ _wf_sync_impl() { fi done < <(_wf_infer_gates_from_artifacts "${work_id}") - _wf_log_history "${work_id}" sync "phase=${resolved}" + _wf_sync_operation "${work_id}" "${resolved}" + _wf_log_history "${work_id}" sync "phase=${resolved} operation=$(_wf_read_state_var "${file}" operation)" } sdlc_workflow_sync() { @@ -712,6 +835,63 @@ sdlc_workflow_record_capture() { _wf_with_workflow_lock _wf_record_capture_impl "${work_id}" "${phase}" } +sdlc_workflow_capture() { + local work_id="" + local phase="" + local -a passthrough=() + + while [[ $# -gt 0 ]]; do + case "$1" in + --work-id) + work_id="${2:-}" + shift 2 + ;; + --phase) + phase="${2:-}" + shift 2 + ;; + *) + passthrough+=("$1") + shift + ;; + esac + done + + local pointer + pointer="$(sdlc_get_pointer)" + if [[ -z "${work_id}" ]]; then + work_id="${pointer}" + elif [[ -n "${pointer}" && "${pointer}" != "${work_id}" ]]; then + echo "sdlc_workflow_capture: --work-id '${work_id}' does not match pointer '${pointer}'" >&2 + echo "Run: $(_wf_shell_helper_path) resume ${work_id}" >&2 + return 3 + fi + + if [[ -z "${work_id}" ]]; then + echo "sdlc_workflow_capture: no active pointer — run: $(_wf_shell_helper_path) resume " >&2 + return 2 + fi + + if [[ -z "${phase}" ]]; then + phase="$(_wf_read_state_var "$(_wf_state_file "${work_id}")" phase resume)" + fi + + local capture_script="${SDLC_ROOT}/scripts/sdlc-spdd/capture-session-memory.sh" + if [[ ! -x "${capture_script}" ]]; then + capture_script="${SDLC_ROOT}/scripts/capture-session-memory.sh" + fi + if [[ ! -x "${capture_script}" ]]; then + echo "sdlc_workflow_capture: capture-session-memory.sh not found" >&2 + return 1 + fi + + run_against_pointer "${work_id}" -- "${capture_script}" \ + --target "${SDLC_ROOT}" \ + --work-id "${work_id}" \ + --phase "${phase}" \ + "${passthrough[@]}" +} + sdlc_workflow_resume() { local work_id="$1" local phase="${2:-}" @@ -1100,8 +1280,11 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then list-shelved|/sdlc-workflow-list-shelved) sdlc_workflow_list_shelved ;; + capture|/sdlc-workflow-capture) + sdlc_workflow_capture "$@" + ;; *) - echo "Usage: $0 {status|next|start|resume|advance|skip|shelf|sync|list-shelved|help} ..." >&2 + echo "Usage: $0 {status|next|start|capture|resume|advance|skip|shelf|sync|list-shelved|help} ..." >&2 echo "Try: $0 help" >&2 exit 2 ;; diff --git a/tests/test-sdlc-workflow.sh b/tests/test-sdlc-workflow.sh index 303642a..ceb2c6d 100755 --- a/tests/test-sdlc-workflow.sh +++ b/tests/test-sdlc-workflow.sh @@ -166,6 +166,48 @@ else bad "session brief missing workflow state" fi +# --------------------------------------------------------------------------- +echo "== Test 12: infers next canvas operation from REASONS Canvas ==" +T="${WORK}/ops" +work_id="FEAT-007-ops" +setup_feature "${T}" "${work_id}" +cp "${REPO_ROOT}/examples/spring-boot-order-api/spdd/canvas/FEAT-001-order-status-api.md" \ + "${T}/spdd/canvas/${work_id}.md" +wf "${T}" resume "${work_id}" >/dev/null +wf "${T}" sync "${work_id}" >/dev/null +op="$(grep '^operation=' "${T}/.sdlc/workflows/${work_id}.state" | cut -d= -f2)" +if [[ "${op}" == "T03" ]]; then ok "sync infers next operation T03"; else bad "expected T03, got ${op}"; fi +out="$(wf "${T}" next)" +if grep -q 'operation T03' <<< "${out}"; then ok "next recommends T03 in code command"; else bad "next missing T03 command"; fi +json="$(wf "${T}" status --json)" +if grep -q '"operation":"T03"' <<< "${json}" && grep -q '"operation_title"' <<< "${json}"; then + ok "json includes operation and title" +else + bad "json missing operation fields" +fi + +# --------------------------------------------------------------------------- +echo "== Test 13: capture wrapper guards pointer ==" +T="${WORK}/capture-guard" +work_id="FEAT-008-cap" +setup_feature "${T}" "${work_id}" +mkdir -p "${T}/scripts/sdlc-spdd" +cp "${REPO_ROOT}/scripts/sdlc.sh" "${T}/scripts/sdlc-spdd/sdlc.sh" +cp "${CAPTURE}" "${T}/scripts/sdlc-spdd/capture-session-memory.sh" +chmod +x "${T}/scripts/sdlc-spdd/sdlc.sh" "${T}/scripts/sdlc-spdd/capture-session-memory.sh" +SDLC_ROOT="${T}" "${T}/scripts/sdlc-spdd/sdlc.sh" resume "${work_id}" >/dev/null +if SDLC_ROOT="${T}" "${T}/scripts/sdlc-spdd/sdlc.sh" capture --summary "ok" >/dev/null 2>&1; then + ok "capture succeeds when pointer matches" +else + bad "capture should succeed for active pointer" +fi +SDLC_ROOT="${T}" "${T}/scripts/sdlc-spdd/sdlc.sh" resume FEAT-999-other >/dev/null 2>&1 || true +if SDLC_ROOT="${T}" "${T}/scripts/sdlc-spdd/sdlc.sh" capture --work-id "${work_id}" --summary "bad" >/dev/null 2>&1; then + bad "capture should refuse mismatched work-id" +else + ok "capture refuses stale work-id" +fi + # --------------------------------------------------------------------------- echo echo "Results: ${pass} passed, ${fail} failed" From 5b6e5dd6446c5acb401e14fc1353c0bc429c2968 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 27 Jun 2026 12:57:50 +0000 Subject: [PATCH 4/7] Add team Work ID registry for shared coordination - agent-context/work-registry.tsv (committed) tracks owner, phase, operation - agent-context/sdlc-team-registry.sh: team, list-work, claim, release - Conflict detection on resume; --force to take over stale claims - Auto-update registry on resume, shelf, advance - SDLC_USER and SDLC_NO_TEAM_REGISTRY env overrides Co-authored-by: John Menke --- agent-context/README.md | 16 ++ agent-context/sdlc-team-registry.sh | 332 ++++++++++++++++++++++ agent-context/sdlc-workflow.sh | 83 +++++- agent-context/work-registry.tsv | 8 + scripts/init-project.sh | 11 + scripts/upgrade-project.sh | 8 + scripts/verify-project-install.sh | 4 +- templates/agent-context/work-registry.tsv | 8 + tests/test-sdlc-workflow.sh | 31 +- 9 files changed, 496 insertions(+), 5 deletions(-) create mode 100755 agent-context/sdlc-team-registry.sh create mode 100644 agent-context/work-registry.tsv create mode 100644 templates/agent-context/work-registry.tsv diff --git a/agent-context/README.md b/agent-context/README.md index 9207cff..e6764b1 100644 --- a/agent-context/README.md +++ b/agent-context/README.md @@ -134,6 +134,22 @@ run `sync` to reconcile workflow state from those files. automatically. After shelving, run `resume ` then `start-agent-session.sh` with the suggested phase to sync back into the chat workflow. +## Team Work ID sharing + +Local pointer (`.sdlc/`) is private to your machine. **Team coordination** uses the +committed file `agent-context/work-registry.tsv` — commit it after claim/release so +teammates see who is on which Work ID, phase, and operation. + +```bash +./scripts/sdlc.sh list-work # discover Work IDs in the repo +./scripts/sdlc.sh team # team registry + your pointer +./scripts/sdlc.sh claim FEAT-001-alpha # resume + register as active owner +./scripts/sdlc.sh release --reason "handoff to QA" +./scripts/sdlc.sh resume OTHER-ID --force # take over if teammate left stale claim +``` + +Set `SDLC_USER="Jane"` to label registry rows. Set `SDLC_NO_TEAM_REGISTRY=1` to opt out. + ## Session Persistence Use scripts to keep agent sessions durable across chat boundaries: diff --git a/agent-context/sdlc-team-registry.sh b/agent-context/sdlc-team-registry.sh new file mode 100755 index 0000000..a4790d0 --- /dev/null +++ b/agent-context/sdlc-team-registry.sh @@ -0,0 +1,332 @@ +#!/usr/bin/env bash +# Team-visible Work ID registry — committed coordination layer on top of local .sdlc/ state. +# +# Local pointer (.sdlc/pointer) stays machine-private. +# agent-context/work-registry.tsv is committed so teammates see claims, phase, and shelf notes. +# +# Usage (via sdlc-workflow.sh / scripts/sdlc.sh): +# team | list-work | claim WORK-ID | release [--reason TEXT] + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + set -euo pipefail +fi + +_TEAM_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=/dev/null +source "${_TEAM_SCRIPT_DIR}/sdlc-pointer.sh" + +SDLC_TEAM_REGISTRY="${SDLC_ROOT}/agent-context/work-registry.tsv" +SDLC_TEAM_REGISTRY_LOCK="${SDLC_ROOT}/agent-context/.work-registry.lock" + +_team_owner() { + if [[ -n "${SDLC_USER:-}" ]]; then + printf '%s' "${SDLC_USER}" + return 0 + fi + local name + name="$(git -C "${SDLC_ROOT}" config user.name 2>/dev/null || true)" + if [[ -n "${name}" ]]; then + printf '%s' "${name}" + return 0 + fi + name="$(git -C "${SDLC_ROOT}" config user.email 2>/dev/null || true)" + if [[ -n "${name}" ]]; then + printf '%s' "${name}" + return 0 + fi + printf '%s' "$(whoami 2>/dev/null || echo unknown)" +} + +_team_registry_init() { + mkdir -p "${SDLC_ROOT}/agent-context" + if [[ ! -f "${SDLC_TEAM_REGISTRY}" ]]; then + cat > "${SDLC_TEAM_REGISTRY}" <<'EOF' +# Team Work Registry — tab-separated. Commit updates so teammates see who is on which Work ID. +# Columns: work_id status phase operation owner updated note +# status: active | shelved | done | available +work_id status phase operation owner updated note +EOF + fi +} + +_team_with_registry_lock() { + if command -v flock >/dev/null 2>&1; then + ( + flock -x 200 || exit 1 + "$@" + ) 200>"${SDLC_TEAM_REGISTRY_LOCK}" + else + "$@" + fi +} + +_team_registry_rows() { + _team_registry_init + grep -v '^#' "${SDLC_TEAM_REGISTRY}" | grep -v '^work_id' | grep -v '^[[:space:]]*$' || true +} + +_team_registry_lookup() { + local work_id="$1" + _team_registry_rows | awk -F '\t' -v id="${work_id}" '$1 == id { print; exit }' +} + +_team_registry_upsert_impl() { + local work_id="$1" + local status="$2" + local phase="${3:-}" + local operation="${4:-}" + local note="${5:-}" + local owner updated header tmp + owner="$(_team_owner)" + updated="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" + _team_registry_init + header="$(grep -m1 '^work_id' "${SDLC_TEAM_REGISTRY}" || echo 'work_id status phase operation owner updated note')" + tmp="${SDLC_TEAM_REGISTRY}.tmp.$$" + { + grep '^#' "${SDLC_TEAM_REGISTRY}" || true + printf '%s\n' "${header}" + local found=0 row wid + while IFS= read -r row; do + wid="${row%%$'\t'*}" + if [[ "${wid}" == "${work_id}" ]]; then + found=1 + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "${work_id}" "${status}" "${phase}" "${operation}" "${owner}" "${updated}" "${note}" + else + printf '%s\n' "${row}" + fi + done < <(_team_registry_rows) + if [[ "${found}" -eq 0 ]]; then + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "${work_id}" "${status}" "${phase}" "${operation}" "${owner}" "${updated}" "${note}" + fi + } > "${tmp}" + mv -f "${tmp}" "${SDLC_TEAM_REGISTRY}" +} + +sdlc_team_register() { + local work_id="$1" + local status="$2" + local phase="${3:-}" + local operation="${4:-}" + local note="${5:-}" + if [[ -z "${work_id}" ]]; then + return 0 + fi + if [[ "${SDLC_NO_TEAM_REGISTRY:-0}" == "1" ]]; then + return 0 + fi + _team_with_registry_lock _team_registry_upsert_impl \ + "${work_id}" "${status}" "${phase}" "${operation}" "${note}" +} + +sdlc_team_check_claim() { + local work_id="$1" + local force="${2:-0}" + local owner status updated me + _team_registry_init + owner="$(awk -F '\t' -v id="${work_id}" '$1 == id { print $5; exit }' "${SDLC_TEAM_REGISTRY}")" + status="$(awk -F '\t' -v id="${work_id}" '$1 == id { print $2; exit }' "${SDLC_TEAM_REGISTRY}")" + updated="$(awk -F '\t' -v id="${work_id}" '$1 == id { print $6; exit }' "${SDLC_TEAM_REGISTRY}")" + [[ -n "${owner}" ]] || return 0 + me="$(_team_owner)" + if [[ "${status}" == "active" && "${owner}" != "${me}" ]]; then + echo "Team registry: ${work_id} is active for ${owner} (updated ${updated})" >&2 + if [[ "${force}" != "1" ]]; then + echo "Coordinate with your teammate, or re-run with --force to take over." >&2 + return 3 + fi + echo "Taking over ${work_id} from ${owner} (--force)." >&2 + fi + return 0 +} + +sdlc_team_sync_from_workflow() { + local work_id="$1" + local status="$2" + local note="${3:-}" + local phase="" operation="" file + file="${SDLC_ROOT}/.sdlc/workflows/${work_id}.state" + if [[ -f "${file}" ]]; then + phase="$(grep -m1 '^phase=' "${file}" 2>/dev/null | cut -d= -f2- || true)" + operation="$(grep -m1 '^operation=' "${file}" 2>/dev/null | cut -d= -f2- || true)" + fi + sdlc_team_register "${work_id}" "${status}" "${phase}" "${operation}" "${note}" +} + +sdlc_team_discover_work_ids() { + local root="${SDLC_ROOT}" + local -A seen=() + local path base + shopt -s nullglob + for path in \ + "${root}"/agent-context/features/*/ \ + "${root}"/spdd/canvas/*.md \ + "${root}"/requirements/milestones/*.md; do + if [[ -d "${path}" ]]; then + base="$(basename "${path}")" + else + base="$(basename "${path}" .md)" + fi + [[ "${base}" == "README" ]] && continue + [[ -n "${base}" ]] || continue + seen["${base}"]=1 + done + shopt -u nullglob + printf '%s\n' "${!seen[@]}" | sort +} + +sdlc_team_infer_work_summary() { + local work_id="$1" + local root="${SDLC_ROOT}" + local parts=() + [[ -d "${root}/agent-context/features/${work_id}" ]] && parts+=("feature workspace") + [[ -f "${root}/spdd/canvas/${work_id}.md" ]] && parts+=("canvas") + [[ -f "${root}/requirements/milestones/${work_id}.md" ]] && parts+=("milestone") + if ((${#parts[@]} == 0)); then + printf 'artifacts unknown' + else + local IFS=', ' + printf '%s' "${parts[*]}" + fi +} + +sdlc_team_list_work() { + local work_id + echo "Work IDs in this repository:" + echo + printf ' %-40s %-10s %-8s %-10s %s\n' "WORK-ID" "REGISTRY" "PHASE" "OWNER" "ARTIFACTS" + while IFS= read -r work_id; do + [[ -z "${work_id}" ]] && continue + local reg_status phase owner summary + reg_status="available" + phase="-" + owner="-" + reg_status="$(awk -F '\t' -v id="${work_id}" '$1 == id { print $2; exit }' "${SDLC_TEAM_REGISTRY}")" + if [[ -n "${reg_status}" ]]; then + phase="$(awk -F '\t' -v id="${work_id}" '$1 == id { print $3; exit }' "${SDLC_TEAM_REGISTRY}")" + owner="$(awk -F '\t' -v id="${work_id}" '$1 == id { print $5; exit }' "${SDLC_TEAM_REGISTRY}")" + phase="${phase:--}" + owner="${owner:--}" + else + reg_status="available" + fi + summary="$(sdlc_team_infer_work_summary "${work_id}")" + printf ' %-40s %-10s %-8s %-10s %s\n' "${work_id}" "${reg_status}" "${phase}" "${owner}" "${summary}" + done < <(sdlc_team_discover_work_ids) + echo + echo "Claim work: ./scripts/sdlc.sh claim " + echo "Team view: ./scripts/sdlc.sh team" +} + +sdlc_team_status() { + local pointer me + pointer="$(sdlc_get_pointer)" + me="$(_team_owner)" + echo "SDLC Team View" + echo "==============" + echo "You: ${me}" + if [[ -n "${pointer}" ]]; then + echo "Your local pointer: ${pointer}" + else + echo "Your local pointer: (none)" + fi + echo + echo "Team registry (commit agent-context/work-registry.tsv to share):" + if _team_registry_rows | grep -q .; then + printf ' %-36s %-8s %-10s %-6s %-16s %s\n' "WORK-ID" "STATUS" "PHASE" "OP" "OWNER" "UPDATED" + local wid status phase op owner updated note + while IFS= read -r row; do + wid="$(awk -F '\t' '{ print $1 }' <<< "${row}")" + status="$(awk -F '\t' '{ print $2 }' <<< "${row}")" + phase="$(awk -F '\t' '{ print $3 }' <<< "${row}")" + op="$(awk -F '\t' '{ print $4 }' <<< "${row}")" + owner="$(awk -F '\t' '{ print $5 }' <<< "${row}")" + updated="$(awk -F '\t' '{ print $6 }' <<< "${row}")" + note="$(awk -F '\t' '{ print $7 }' <<< "${row}")" + local mark="" + [[ "${owner}" == "${me}" && "${wid}" == "${pointer}" ]] && mark=" (you)" + [[ "${owner}" == "${me}" && "${wid}" != "${pointer}" ]] && mark=" (you, pointer elsewhere)" + printf ' %-36s %-8s %-10s %-6s %-16s %s%s\n' \ + "${wid}" "${status}" "${phase:--}" "${op:--}" "${owner}" "${updated}" "${note}" "${mark}" + done < <(_team_registry_rows) + else + echo " (empty — claim work with ./scripts/sdlc.sh claim )" + fi + echo + echo "Discover all Work IDs: ./scripts/sdlc.sh list-work" +} + +sdlc_team_claim() { + local work_id="$1" + local force="${2:-0}" + local phase="${3:-}" + if [[ -z "${work_id}" ]]; then + echo "sdlc_team_claim: work id required" >&2 + return 2 + fi + sdlc_team_check_claim "${work_id}" "${force}" || return $? + if [[ ! -f "${SDLC_ROOT}/agent-context/sdlc-workflow.sh" ]]; then + echo "sdlc_team_claim: sdlc-workflow.sh not installed" >&2 + return 1 + fi + # shellcheck source=/dev/null + source "${SDLC_ROOT}/agent-context/sdlc-workflow.sh" + sdlc_workflow_resume "${work_id}" "${phase}" 1 0 + echo "Team registry updated — commit agent-context/work-registry.tsv to share with teammates." +} + +sdlc_team_release() { + local reason="${1:-released}" + local work_id + work_id="$(sdlc_get_pointer)" + if [[ -z "${work_id}" ]]; then + echo "sdlc_team_release: no active pointer" >&2 + return 2 + fi + if [[ ! -f "${SDLC_ROOT}/agent-context/sdlc-workflow.sh" ]]; then + echo "sdlc_team_release: sdlc-workflow.sh not installed" >&2 + return 1 + fi + # shellcheck source=/dev/null + source "${SDLC_ROOT}/agent-context/sdlc-workflow.sh" + sdlc_workflow_shelf "${reason}" + sdlc_team_register "${work_id}" "shelved" "" "" "${reason}" + echo "Team registry updated — commit agent-context/work-registry.tsv to share with teammates." +} + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + cmd="${1:-team}" + shift || true + case "${cmd}" in + team) sdlc_team_status ;; + list-work) sdlc_team_list_work ;; + claim) + work_id="${1:-}"; shift || true + force=0 + phase="" + while [[ $# -gt 0 ]]; do + case "$1" in + --force) force=1; shift ;; + --phase) phase="${2:-}"; shift 2 ;; + *) shift ;; + esac + done + sdlc_team_claim "${work_id}" "${force}" "${phase}" + ;; + release) + reason="released" + while [[ $# -gt 0 ]]; do + case "$1" in + --reason) reason="${2:-}"; shift 2 ;; + *) shift ;; + esac + done + sdlc_team_release "${reason}" + ;; + *) + echo "Usage: $0 {team|list-work|claim|release} ..." >&2 + exit 2 + ;; + esac +fi diff --git a/agent-context/sdlc-workflow.sh b/agent-context/sdlc-workflow.sh index 6e1d84e..f322b5a 100755 --- a/agent-context/sdlc-workflow.sh +++ b/agent-context/sdlc-workflow.sh @@ -19,6 +19,10 @@ fi _SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=/dev/null source "${_SCRIPT_DIR}/sdlc-pointer.sh" +if [[ -f "${_SCRIPT_DIR}/sdlc-team-registry.sh" ]]; then + # shellcheck source=/dev/null + source "${_SCRIPT_DIR}/sdlc-team-registry.sh" +fi SDLC_WORKFLOW_DIR="${SDLC_DIR}/workflows" SDLC_WORKFLOW_LOCK="${SDLC_DIR}/workflow.lock" @@ -536,9 +540,16 @@ SDLC workflow helper — short paths for humans and agents ./scripts/sdlc.sh shelf --reason "why" ./scripts/sdlc.sh sync [--work-id ID] ./scripts/sdlc.sh list-shelved + ./scripts/sdlc.sh team # team registry + your pointer + ./scripts/sdlc.sh list-work # all Work IDs in the repo + ./scripts/sdlc.sh claim # resume + register for team + ./scripts/sdlc.sh release --reason "why" In chat: /sdlc-spdd-whereami +Team sharing: commit agent-context/work-registry.tsv after claim/release/shelf. +Set SDLC_USER to override the owner name. SDLC_NO_TEAM_REGISTRY=1 opts out. + Typical loop: 1. ./scripts/sdlc.sh next 2. run the assistant command (or ./scripts/sdlc.sh start) @@ -896,12 +907,17 @@ sdlc_workflow_resume() { local work_id="$1" local phase="${2:-}" local auto_shelf="${3:-1}" + local force_claim="${4:-0}" if [[ -z "${work_id}" ]]; then echo "sdlc_workflow_resume: work id required" >&2 return 2 fi + if declare -F sdlc_team_check_claim >/dev/null 2>&1; then + sdlc_team_check_claim "${work_id}" "${force_claim}" || return $? + fi + local current current="$(sdlc_get_pointer)" if [[ -n "${current}" && "${current}" != "${work_id}" && "${auto_shelf}" == "1" ]]; then @@ -933,6 +949,10 @@ sdlc_workflow_resume() { echo "Recommended command: $(sdlc_workflow_recommended_command "${resolved_phase}" "${work_id}")" echo "Quick check: ./scripts/sdlc.sh next" echo "Start session: $(sdlc_workflow_shell_start "${work_id}" "${resolved_phase}")" + if declare -F sdlc_team_sync_from_workflow >/dev/null 2>&1; then + sdlc_team_sync_from_workflow "${work_id}" "active" "" + echo "Team: commit agent-context/work-registry.tsv to share this claim." + fi } sdlc_workflow_advance() { @@ -973,6 +993,9 @@ sdlc_workflow_advance() { _wf_pass_gates_for_phase "${work_id}" "${current}" _wf_set_state_var "${work_id}" phase "${next}" _wf_log_history "${work_id}" advance "${current}->${next}" + if declare -F sdlc_team_sync_from_workflow >/dev/null 2>&1; then + sdlc_team_sync_from_workflow "${work_id}" "active" "" + fi echo "Advanced ${work_id}: ${current} -> ${next}" echo "Recommended command: $(sdlc_workflow_recommended_command "${next}" "${work_id}")" echo "Quick check: ./scripts/sdlc.sh next" @@ -1038,6 +1061,10 @@ sdlc_workflow_shelf() { _wf_set_state_var "${work_id}" shelved_reason "${reason}" _wf_log_history "${work_id}" shelf "reason=${reason}" sdlc_reset_pointer >/dev/null + if declare -F sdlc_team_sync_from_workflow >/dev/null 2>&1; then + sdlc_team_sync_from_workflow "${work_id}" "shelved" "${reason}" + echo "Team: commit agent-context/work-registry.tsv to share shelf status." + fi echo "Shelved ${work_id}" echo "Reason: ${reason}" echo "Resume later: ./scripts/sdlc.sh resume ${work_id}" @@ -1226,15 +1253,16 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then resume|/sdlc-workflow-resume) work_id="${1:-}"; shift || true phase="" - reason="" + force_claim=0 while [[ $# -gt 0 ]]; do case "$1" in --phase) phase="${2:-}"; shift 2 ;; + --force) force_claim=1; shift ;; --no-auto-shelf) AUTO_SHELF=0; shift ;; *) shift ;; esac done - sdlc_workflow_resume "${work_id}" "${phase}" "${AUTO_SHELF:-1}" + sdlc_workflow_resume "${work_id}" "${phase}" "${AUTO_SHELF:-1}" "${force_claim}" ;; advance|/sdlc-workflow-advance) to="" @@ -1283,8 +1311,57 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then capture|/sdlc-workflow-capture) sdlc_workflow_capture "$@" ;; + team|/sdlc-team-status) + if declare -F sdlc_team_status >/dev/null 2>&1; then + sdlc_team_status + else + echo "team registry not installed" >&2 + exit 1 + fi + ;; + list-work|/sdlc-list-work) + if declare -F sdlc_team_list_work >/dev/null 2>&1; then + sdlc_team_list_work + else + echo "team registry not installed" >&2 + exit 1 + fi + ;; + claim|/sdlc-team-claim) + work_id="${1:-}"; shift || true + force=0 + phase="" + while [[ $# -gt 0 ]]; do + case "$1" in + --force) force=1; shift ;; + --phase) phase="${2:-}"; shift 2 ;; + *) shift ;; + esac + done + if declare -F sdlc_team_claim >/dev/null 2>&1; then + sdlc_team_claim "${work_id}" "${force}" "${phase}" + else + echo "team registry not installed" >&2 + exit 1 + fi + ;; + release|/sdlc-team-release) + reason="released" + while [[ $# -gt 0 ]]; do + case "$1" in + --reason) reason="${2:-}"; shift 2 ;; + *) shift ;; + esac + done + if declare -F sdlc_team_release >/dev/null 2>&1; then + sdlc_team_release "${reason}" + else + echo "team registry not installed" >&2 + exit 1 + fi + ;; *) - echo "Usage: $0 {status|next|start|capture|resume|advance|skip|shelf|sync|list-shelved|help} ..." >&2 + echo "Usage: $0 {status|next|start|capture|resume|advance|skip|shelf|sync|team|list-work|claim|release|list-shelved|help} ..." >&2 echo "Try: $0 help" >&2 exit 2 ;; diff --git a/agent-context/work-registry.tsv b/agent-context/work-registry.tsv new file mode 100644 index 0000000..213f310 --- /dev/null +++ b/agent-context/work-registry.tsv @@ -0,0 +1,8 @@ +# Team Work Registry — tab-separated. Commit updates so teammates see who is on which Work ID. +# Columns: work_id status phase operation owner updated note +# status: active | shelved | done | available +# +# Updated by: ./scripts/sdlc.sh claim|release|resume|shelf +# Override owner label: export SDLC_USER="your-name" +# Opt out of registry writes: export SDLC_NO_TEAM_REGISTRY=1 +work_id status phase operation owner updated note diff --git a/scripts/init-project.sh b/scripts/init-project.sh index ed324bd..5e31def 100755 --- a/scripts/init-project.sh +++ b/scripts/init-project.sh @@ -229,6 +229,17 @@ if [[ "${DRY_RUN}" -eq 0 && -f "${TARGET}/agent-context/sdlc-workflow.sh" ]]; th chmod +x "${TARGET}/agent-context/sdlc-workflow.sh" fi +copy_if_missing \ + "${REPO_ROOT}/agent-context/sdlc-team-registry.sh" \ + "${TARGET}/agent-context/sdlc-team-registry.sh" +if [[ "${DRY_RUN}" -eq 0 && -f "${TARGET}/agent-context/sdlc-team-registry.sh" ]]; then + chmod +x "${TARGET}/agent-context/sdlc-team-registry.sh" +fi + +copy_if_missing \ + "${REPO_ROOT}/templates/agent-context/work-registry.tsv" \ + "${TARGET}/agent-context/work-registry.tsv" + copy_if_missing \ "${REPO_ROOT}/agent-context/README.md" \ "${TARGET}/agent-context/README.md" diff --git a/scripts/upgrade-project.sh b/scripts/upgrade-project.sh index eb7bf72..f57ee17 100755 --- a/scripts/upgrade-project.sh +++ b/scripts/upgrade-project.sh @@ -399,6 +399,14 @@ copy_executable_framework_file \ "${REPO_ROOT}/agent-context/sdlc-workflow.sh" \ "${TARGET}/agent-context/sdlc-workflow.sh" +copy_executable_framework_file \ + "${REPO_ROOT}/agent-context/sdlc-team-registry.sh" \ + "${TARGET}/agent-context/sdlc-team-registry.sh" + +copy_framework_file \ + "${REPO_ROOT}/templates/agent-context/work-registry.tsv" \ + "${TARGET}/agent-context/work-registry.tsv" + for file in \ quality-gates.md \ validation-rules.md; do diff --git a/scripts/verify-project-install.sh b/scripts/verify-project-install.sh index a4ee0b3..9f8e351 100755 --- a/scripts/verify-project-install.sh +++ b/scripts/verify-project-install.sh @@ -155,7 +155,9 @@ run_part "SDLC (sessions, memory, playbooks)" \ SDLC "session handoff playbook" "agent-context/playbooks/session-handoff-playbook.md" file \ SDLC "quality gates" "agent-context/harness/quality-gates.md" file \ SDLC "pointer manager script" "agent-context/sdlc-pointer.sh" executable \ - SDLC "workflow manager script" "agent-context/sdlc-workflow.sh" executable + SDLC "workflow manager script" "agent-context/sdlc-workflow.sh" executable \ + SDLC "team registry script" "agent-context/sdlc-team-registry.sh" executable \ + SDLC "team work registry" "agent-context/work-registry.tsv" file run_part "Runtime scripts and docs" \ Runtime "runtime scripts directory" "scripts/sdlc-spdd" dir \ diff --git a/templates/agent-context/work-registry.tsv b/templates/agent-context/work-registry.tsv new file mode 100644 index 0000000..213f310 --- /dev/null +++ b/templates/agent-context/work-registry.tsv @@ -0,0 +1,8 @@ +# Team Work Registry — tab-separated. Commit updates so teammates see who is on which Work ID. +# Columns: work_id status phase operation owner updated note +# status: active | shelved | done | available +# +# Updated by: ./scripts/sdlc.sh claim|release|resume|shelf +# Override owner label: export SDLC_USER="your-name" +# Opt out of registry writes: export SDLC_NO_TEAM_REGISTRY=1 +work_id status phase operation owner updated note diff --git a/tests/test-sdlc-workflow.sh b/tests/test-sdlc-workflow.sh index ceb2c6d..51644ab 100755 --- a/tests/test-sdlc-workflow.sh +++ b/tests/test-sdlc-workflow.sh @@ -31,7 +31,9 @@ setup_feature() { "${t}/spdd/analysis" cp "${POINTER}" "${t}/agent-context/sdlc-pointer.sh" cp "${WORKFLOW}" "${t}/agent-context/sdlc-workflow.sh" - chmod +x "${t}/agent-context/sdlc-pointer.sh" "${t}/agent-context/sdlc-workflow.sh" + cp "${REPO_ROOT}/agent-context/sdlc-team-registry.sh" "${t}/agent-context/sdlc-team-registry.sh" + cp "${REPO_ROOT}/templates/agent-context/work-registry.tsv" "${t}/agent-context/work-registry.tsv" + chmod +x "${t}/agent-context/sdlc-pointer.sh" "${t}/agent-context/sdlc-workflow.sh" "${t}/agent-context/sdlc-team-registry.sh" } # --------------------------------------------------------------------------- @@ -208,6 +210,33 @@ else ok "capture refuses stale work-id" fi +# --------------------------------------------------------------------------- +echo "== Test 14: team registry claim and conflict ==" +T="${WORK}/team" +work_id="FEAT-009-team" +setup_feature "${T}" "${work_id}" +SDLC_USER="alice" SDLC_ROOT="${T}" wf "${T}" claim "${work_id}" >/dev/null +if grep -q $'FEAT-009-team\tactive\t' "${T}/agent-context/work-registry.tsv"; then + ok "claim writes team registry" +else + bad "team registry missing active row" +fi +if SDLC_USER="bob" SDLC_ROOT="${T}" wf "${T}" resume "${work_id}" >/dev/null 2>&1; then + bad "resume should refuse another owner claim" +else + ok "resume refuses conflicting team claim" +fi +if SDLC_USER="bob" SDLC_ROOT="${T}" wf "${T}" resume "${work_id}" --force >/dev/null; then + ok "resume --force allows takeover" +else + bad "resume --force should succeed" +fi + +# --------------------------------------------------------------------------- +echo "== Test 15: list-work discovers repo Work IDs ==" +out="$(SDLC_ROOT="${T}" wf "${T}" list-work)" +if grep -q 'FEAT-009-team' <<< "${out}"; then ok "list-work shows work id"; else bad "list-work missing id"; fi + # --------------------------------------------------------------------------- echo echo "Results: ${pass} passed, ${fail} failed" From 79a8af3b4774ec637274956b2ee68df399d6e74f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 27 Jun 2026 13:03:06 +0000 Subject: [PATCH 5/7] Add team registry follow-ups: stale TTL, done sync, hooks, notes - Flag stale active claims (SDLC_TEAM_STALE_DAYS, default 7) in team/list-work - Auto-mark done from canvas Final Status (sync-team, team, list-work) - branch:/pr:/jira: note tokens on claim; auto git branch by default - SDLC_TEAM_REGISTRY_HOOK with Slack/Jira example hook script - Expand /sdlc-spdd-whereami to check team registry before coding (8 steps) - 30 regression tests Co-authored-by: John Menke --- agent-context/README.md | 20 ++ agent-context/sdlc-team-registry.sh | 197 ++++++++++++++++-- agent-context/sdlc-workflow.sh | 29 ++- scripts/init-project.sh | 4 + scripts/upgrade-project.sh | 4 + .../hooks/notify-team-registry.example.sh | 37 ++++ templates/agent-context/work-registry.tsv | 1 + .../claude/commands/sdlc-spdd-whereami.md | 20 +- .../prompts/sdlc-spdd-whereami.prompt.md | 20 +- templates/cursor/sdlc-spdd-whereami.md | 18 +- tests/test-sdlc-workflow.sh | 56 +++++ 11 files changed, 366 insertions(+), 40 deletions(-) create mode 100644 templates/agent-context/hooks/notify-team-registry.example.sh diff --git a/agent-context/README.md b/agent-context/README.md index e6764b1..1d1549f 100644 --- a/agent-context/README.md +++ b/agent-context/README.md @@ -150,6 +150,26 @@ teammates see who is on which Work ID, phase, and operation. Set `SDLC_USER="Jane"` to label registry rows. Set `SDLC_NO_TEAM_REGISTRY=1` to opt out. +**Stale claims:** active rows older than `SDLC_TEAM_STALE_DAYS` (default 7) show `[STALE>Nd]` in +`team` / `list-work`. Stale claims warn but do not block; non-stale claims block until `--force`. + +**Done status:** canvases with `## Final Status` → `Status: Complete` are marked `done` when you run +`team`, `list-work`, or `sync-team`. + +**Branch / PR / Jira linking** (stored in the `note` column): + +```bash +./scripts/sdlc.sh claim FEAT-001 --branch cursor/feat-001 --pr "#21" --jira "PROJ-123" +# auto-detects current git branch on claim (disable: SDLC_TEAM_AUTO_BRANCH=0) +``` + +**Notifications:** copy `agent-context/hooks/notify-team-registry.example.sh` and set: + +```bash +export SDLC_TEAM_REGISTRY_HOOK=./agent-context/hooks/notify-team-registry.sh +export SDLC_TEAM_SLACK_WEBHOOK=https://hooks.slack.com/services/... +``` + ## Session Persistence Use scripts to keep agent sessions durable across chat boundaries: diff --git a/agent-context/sdlc-team-registry.sh b/agent-context/sdlc-team-registry.sh index a4790d0..8fe584d 100755 --- a/agent-context/sdlc-team-registry.sh +++ b/agent-context/sdlc-team-registry.sh @@ -18,6 +18,129 @@ source "${_TEAM_SCRIPT_DIR}/sdlc-pointer.sh" SDLC_TEAM_REGISTRY="${SDLC_ROOT}/agent-context/work-registry.tsv" SDLC_TEAM_REGISTRY_LOCK="${SDLC_ROOT}/agent-context/.work-registry.lock" +_team_stale_days() { + printf '%s' "${SDLC_TEAM_STALE_DAYS:-7}" +} + +_team_is_stale_claim() { + local updated="${1:-}" + local status="${2:-}" + [[ "${status}" == "active" ]] || return 1 + [[ -n "${updated}" ]] || return 1 + local now updated_secs age limit + now="$(date -u +%s)" + updated_secs="$(date -u -d "${updated}" +%s 2>/dev/null || echo 0)" + (( updated_secs > 0 )) || return 1 + age=$((now - updated_secs)) + limit=$(( $(_team_stale_days) * 86400 )) + (( age > limit )) +} + +_team_stale_label() { + local updated="$1" + local status="$2" + if _team_is_stale_claim "${updated}" "${status}"; then + printf ' [STALE>%sd]' "$(_team_stale_days)" + fi +} + +_team_canvas_path() { + local work_id="$1" + local root="${SDLC_ROOT}" + local canvas="${root}/spdd/canvas/${work_id}.md" + if [[ ! -f "${canvas}" ]]; then + canvas="${root}/agent-context/features/${work_id}/reasons-canvas.md" + fi + [[ -f "${canvas}" ]] && printf '%s' "${canvas}" +} + +_team_canvas_is_complete() { + local work_id="$1" + local canvas line + canvas="$(_team_canvas_path "${work_id}")" + [[ -n "${canvas}" ]] || return 1 + line="$(awk ' + /^## Final Status/ { in_final=1; next } + /^## / { if (in_final) in_final=0 } + in_final && /^- Status:/ { + sub(/^- Status:[[:space:]]*/, "") + print + exit + } + ' "${canvas}")" + [[ -z "${line}" ]] && return 1 + line="$(printf '%s' "${line}" | tr '[:upper:]' '[:lower:]')" + [[ "${line}" == *complete* ]] && [[ "${line}" != *in\ progress* ]] +} + +_team_registry_note_for() { + local work_id="$1" + awk -F '\t' -v id="${work_id}" '$1 == id { print $7; exit }' "${SDLC_TEAM_REGISTRY}" +} + +_team_compose_note() { + local existing="${1:-}" + local branch="${2:-}" + local pr="${3:-}" + local jira="${4:-}" + local extra="${5:-}" + local out="" token + for token in ${existing}; do + [[ "${token}" == branch:* || "${token}" == pr:* || "${token}" == jira:* ]] && continue + out+="${token} " + done + [[ -n "${branch}" ]] && out+="branch:${branch} " + [[ -n "${pr}" ]] && out+="pr:${pr} " + [[ -n "${jira}" ]] && out+="jira:${jira} " + [[ -n "${extra}" ]] && out+="${extra} " + printf '%s' "${out%" "}" +} + +_team_auto_branch() { + local branch="${1:-}" + if [[ -n "${branch}" && "${branch}" != "auto" ]]; then + printf '%s' "${branch}" + return 0 + fi + if [[ -z "${branch}" && "${SDLC_TEAM_AUTO_BRANCH:-1}" != "1" ]]; then + return 0 + fi + git -C "${SDLC_ROOT}" branch --show-current 2>/dev/null || true +} + +_team_run_hook() { + local work_id="$1" + local status="$2" + local phase="$3" + local operation="$4" + local owner="$5" + local updated="$6" + local note="$7" + local hook="${SDLC_TEAM_REGISTRY_HOOK:-}" + [[ -n "${hook}" && -x "${hook}" ]] || return 0 + "${hook}" "${work_id}" "${status}" "${phase}" "${operation}" "${owner}" "${updated}" "${note}" || true +} + +sdlc_team_refresh_done_status() { + local work_id + _team_registry_init + while IFS= read -r work_id; do + [[ -z "${work_id}" ]] && continue + _team_canvas_is_complete "${work_id}" || continue + local cur_status cur_phase cur_op cur_note + cur_status="$(awk -F '\t' -v id="${work_id}" '$1 == id { print $2; exit }' "${SDLC_TEAM_REGISTRY}")" + if [[ -z "${cur_status}" ]]; then + sdlc_team_register "${work_id}" "done" "sync" "" "canvas complete" + continue + fi + [[ "${cur_status}" == "done" ]] && continue + cur_phase="$(awk -F '\t' -v id="${work_id}" '$1 == id { print $3; exit }' "${SDLC_TEAM_REGISTRY}")" + cur_op="$(awk -F '\t' -v id="${work_id}" '$1 == id { print $4; exit }' "${SDLC_TEAM_REGISTRY}")" + cur_note="$(_team_compose_note "$(_team_registry_note_for "${work_id}")" "" "" "" "canvas Final Status: Complete")" + sdlc_team_register "${work_id}" "done" "${cur_phase}" "${cur_op}" "${cur_note}" + done < <(sdlc_team_discover_work_ids) +} + _team_owner() { if [[ -n "${SDLC_USER:-}" ]]; then printf '%s' "${SDLC_USER}" @@ -44,6 +167,7 @@ _team_registry_init() { # Team Work Registry — tab-separated. Commit updates so teammates see who is on which Work ID. # Columns: work_id status phase operation owner updated note # status: active | shelved | done | available +# note tokens: branch: pr: jira: work_id status phase operation owner updated note EOF fi @@ -90,6 +214,9 @@ _team_registry_upsert_impl() { wid="${row%%$'\t'*}" if [[ "${wid}" == "${work_id}" ]]; then found=1 + if [[ -z "${note}" ]]; then + note="$(awk -F '\t' '{ print $7 }' <<< "${row}")" + fi printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ "${work_id}" "${status}" "${phase}" "${operation}" "${owner}" "${updated}" "${note}" else @@ -118,6 +245,12 @@ sdlc_team_register() { fi _team_with_registry_lock _team_registry_upsert_impl \ "${work_id}" "${status}" "${phase}" "${operation}" "${note}" + local hook_owner hook_updated hook_note + hook_owner="$(awk -F '\t' -v id="${work_id}" '$1 == id { print $5; exit }' "${SDLC_TEAM_REGISTRY}")" + hook_updated="$(awk -F '\t' -v id="${work_id}" '$1 == id { print $6; exit }' "${SDLC_TEAM_REGISTRY}")" + hook_note="$(awk -F '\t' -v id="${work_id}" '$1 == id { print $7; exit }' "${SDLC_TEAM_REGISTRY}")" + _team_run_hook "${work_id}" "${status}" "${phase}" "${operation}" \ + "${hook_owner}" "${hook_updated}" "${hook_note}" } sdlc_team_check_claim() { @@ -131,6 +264,11 @@ sdlc_team_check_claim() { [[ -n "${owner}" ]] || return 0 me="$(_team_owner)" if [[ "${status}" == "active" && "${owner}" != "${me}" ]]; then + if _team_is_stale_claim "${updated}" "${status}"; then + echo "Team registry: ${work_id} is active for ${owner} but stale (>${_team_stale_days}d since ${updated})." >&2 + echo "You may proceed, or use --force to take over explicitly." >&2 + return 0 + fi echo "Team registry: ${work_id} is active for ${owner} (updated ${updated})" >&2 if [[ "${force}" != "1" ]]; then echo "Coordinate with your teammate, or re-run with --force to take over." >&2 @@ -193,39 +331,51 @@ sdlc_team_infer_work_summary() { sdlc_team_list_work() { local work_id + sdlc_team_refresh_done_status echo "Work IDs in this repository:" echo - printf ' %-40s %-10s %-8s %-10s %s\n' "WORK-ID" "REGISTRY" "PHASE" "OWNER" "ARTIFACTS" + printf ' %-40s %-12s %-8s %-10s %s\n' "WORK-ID" "REGISTRY" "PHASE" "OWNER" "ARTIFACTS" while IFS= read -r work_id; do [[ -z "${work_id}" ]] && continue - local reg_status phase owner summary + local reg_status phase owner summary updated stale done_hint reg_status="available" phase="-" owner="-" + updated="" reg_status="$(awk -F '\t' -v id="${work_id}" '$1 == id { print $2; exit }' "${SDLC_TEAM_REGISTRY}")" if [[ -n "${reg_status}" ]]; then phase="$(awk -F '\t' -v id="${work_id}" '$1 == id { print $3; exit }' "${SDLC_TEAM_REGISTRY}")" owner="$(awk -F '\t' -v id="${work_id}" '$1 == id { print $5; exit }' "${SDLC_TEAM_REGISTRY}")" + updated="$(awk -F '\t' -v id="${work_id}" '$1 == id { print $6; exit }' "${SDLC_TEAM_REGISTRY}")" phase="${phase:--}" owner="${owner:--}" + stale="$(_team_stale_label "${updated}" "${reg_status}")" + reg_status="${reg_status}${stale}" else reg_status="available" fi - summary="$(sdlc_team_infer_work_summary "${work_id}")" - printf ' %-40s %-10s %-8s %-10s %s\n' "${work_id}" "${reg_status}" "${phase}" "${owner}" "${summary}" + if _team_canvas_is_complete "${work_id}" && [[ "${reg_status}" != done* ]]; then + done_hint=" (canvas complete)" + else + done_hint="" + fi + summary="$(sdlc_team_infer_work_summary "${work_id}")${done_hint}" + printf ' %-40s %-12s %-8s %-10s %s\n' "${work_id}" "${reg_status}" "${phase}" "${owner}" "${summary}" done < <(sdlc_team_discover_work_ids) echo - echo "Claim work: ./scripts/sdlc.sh claim " - echo "Team view: ./scripts/sdlc.sh team" + echo "Claim: ./scripts/sdlc.sh claim [--branch NAME] [--pr #N] [--jira KEY]" + echo "Team: ./scripts/sdlc.sh team" } sdlc_team_status() { local pointer me + sdlc_team_refresh_done_status pointer="$(sdlc_get_pointer)" me="$(_team_owner)" echo "SDLC Team View" echo "==============" echo "You: ${me}" + echo "Stale claim TTL: $(_team_stale_days) days (override: SDLC_TEAM_STALE_DAYS)" if [[ -n "${pointer}" ]]; then echo "Your local pointer: ${pointer}" else @@ -234,8 +384,8 @@ sdlc_team_status() { echo echo "Team registry (commit agent-context/work-registry.tsv to share):" if _team_registry_rows | grep -q .; then - printf ' %-36s %-8s %-10s %-6s %-16s %s\n' "WORK-ID" "STATUS" "PHASE" "OP" "OWNER" "UPDATED" - local wid status phase op owner updated note + printf ' %-36s %-14s %-10s %-6s %-16s %s\n' "WORK-ID" "STATUS" "PHASE" "OP" "OWNER" "NOTE" + local wid status phase op owner updated note status_disp while IFS= read -r row; do wid="$(awk -F '\t' '{ print $1 }' <<< "${row}")" status="$(awk -F '\t' '{ print $2 }' <<< "${row}")" @@ -244,16 +394,18 @@ sdlc_team_status() { owner="$(awk -F '\t' '{ print $5 }' <<< "${row}")" updated="$(awk -F '\t' '{ print $6 }' <<< "${row}")" note="$(awk -F '\t' '{ print $7 }' <<< "${row}")" + status_disp="${status}$(_team_stale_label "${updated}" "${status}")" local mark="" [[ "${owner}" == "${me}" && "${wid}" == "${pointer}" ]] && mark=" (you)" [[ "${owner}" == "${me}" && "${wid}" != "${pointer}" ]] && mark=" (you, pointer elsewhere)" - printf ' %-36s %-8s %-10s %-6s %-16s %s%s\n' \ - "${wid}" "${status}" "${phase:--}" "${op:--}" "${owner}" "${updated}" "${note}" "${mark}" + printf ' %-36s %-14s %-10s %-6s %-16s %s%s\n' \ + "${wid}" "${status_disp}" "${phase:--}" "${op:--}" "${owner}" "${note:-${updated}}" "${mark}" done < <(_team_registry_rows) else echo " (empty — claim work with ./scripts/sdlc.sh claim )" fi echo + echo "Hooks: set SDLC_TEAM_REGISTRY_HOOK to agent-context/hooks/notify-team-registry.sh" echo "Discover all Work IDs: ./scripts/sdlc.sh list-work" } @@ -261,6 +413,10 @@ sdlc_team_claim() { local work_id="$1" local force="${2:-0}" local phase="${3:-}" + local branch="${4:-}" + local pr="${5:-}" + local jira="${6:-}" + local note_extra="${7:-}" if [[ -z "${work_id}" ]]; then echo "sdlc_team_claim: work id required" >&2 return 2 @@ -270,9 +426,13 @@ sdlc_team_claim() { echo "sdlc_team_claim: sdlc-workflow.sh not installed" >&2 return 1 fi + branch="$(_team_auto_branch "${branch}")" + local existing_note note + existing_note="$(_team_registry_note_for "${work_id}")" + note="$(_team_compose_note "${existing_note}" "${branch}" "${pr}" "${jira}" "${note_extra}")" # shellcheck source=/dev/null source "${SDLC_ROOT}/agent-context/sdlc-workflow.sh" - sdlc_workflow_resume "${work_id}" "${phase}" 1 0 + sdlc_workflow_resume "${work_id}" "${phase}" 1 0 "${note}" echo "Team registry updated — commit agent-context/work-registry.tsv to share with teammates." } @@ -291,7 +451,6 @@ sdlc_team_release() { # shellcheck source=/dev/null source "${SDLC_ROOT}/agent-context/sdlc-workflow.sh" sdlc_workflow_shelf "${reason}" - sdlc_team_register "${work_id}" "shelved" "" "" "${reason}" echo "Team registry updated — commit agent-context/work-registry.tsv to share with teammates." } @@ -305,14 +464,26 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then work_id="${1:-}"; shift || true force=0 phase="" + branch="" + pr="" + jira="" + note_extra="" while [[ $# -gt 0 ]]; do case "$1" in --force) force=1; shift ;; --phase) phase="${2:-}"; shift 2 ;; + --branch) branch="${2:-}"; shift 2 ;; + --pr) pr="${2:-}"; shift 2 ;; + --jira) jira="${2:-}"; shift 2 ;; + --note) note_extra="${2:-}"; shift 2 ;; *) shift ;; esac done - sdlc_team_claim "${work_id}" "${force}" "${phase}" + sdlc_team_claim "${work_id}" "${force}" "${phase}" "${branch}" "${pr}" "${jira}" "${note_extra}" + ;; + sync-team|/sdlc-team-sync) + sdlc_team_refresh_done_status + echo "Team registry refreshed from canvas Final Status." ;; release) reason="released" diff --git a/agent-context/sdlc-workflow.sh b/agent-context/sdlc-workflow.sh index f322b5a..a741690 100755 --- a/agent-context/sdlc-workflow.sh +++ b/agent-context/sdlc-workflow.sh @@ -542,13 +542,16 @@ SDLC workflow helper — short paths for humans and agents ./scripts/sdlc.sh list-shelved ./scripts/sdlc.sh team # team registry + your pointer ./scripts/sdlc.sh list-work # all Work IDs in the repo - ./scripts/sdlc.sh claim # resume + register for team + ./scripts/sdlc.sh claim [--branch NAME] [--pr #N] [--jira KEY] ./scripts/sdlc.sh release --reason "why" + ./scripts/sdlc.sh sync-team # mark done from canvas Final Status In chat: /sdlc-spdd-whereami Team sharing: commit agent-context/work-registry.tsv after claim/release/shelf. Set SDLC_USER to override the owner name. SDLC_NO_TEAM_REGISTRY=1 opts out. +SDLC_TEAM_STALE_DAYS=7 flags stale active claims. SDLC_TEAM_REGISTRY_HOOK for Slack/Jira. +Note tokens: branch:... pr:... jira:... (auto branch from git on claim by default). Typical loop: 1. ./scripts/sdlc.sh next @@ -908,6 +911,7 @@ sdlc_workflow_resume() { local phase="${2:-}" local auto_shelf="${3:-1}" local force_claim="${4:-0}" + local team_note="${5:-}" if [[ -z "${work_id}" ]]; then echo "sdlc_workflow_resume: work id required" >&2 @@ -950,7 +954,7 @@ sdlc_workflow_resume() { echo "Quick check: ./scripts/sdlc.sh next" echo "Start session: $(sdlc_workflow_shell_start "${work_id}" "${resolved_phase}")" if declare -F sdlc_team_sync_from_workflow >/dev/null 2>&1; then - sdlc_team_sync_from_workflow "${work_id}" "active" "" + sdlc_team_sync_from_workflow "${work_id}" "active" "${team_note}" echo "Team: commit agent-context/work-registry.tsv to share this claim." fi } @@ -1331,15 +1335,32 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then work_id="${1:-}"; shift || true force=0 phase="" + branch="" + pr="" + jira="" + note_extra="" while [[ $# -gt 0 ]]; do case "$1" in --force) force=1; shift ;; --phase) phase="${2:-}"; shift 2 ;; + --branch) branch="${2:-}"; shift 2 ;; + --pr) pr="${2:-}"; shift 2 ;; + --jira) jira="${2:-}"; shift 2 ;; + --note) note_extra="${2:-}"; shift 2 ;; *) shift ;; esac done if declare -F sdlc_team_claim >/dev/null 2>&1; then - sdlc_team_claim "${work_id}" "${force}" "${phase}" + sdlc_team_claim "${work_id}" "${force}" "${phase}" "${branch}" "${pr}" "${jira}" "${note_extra}" + else + echo "team registry not installed" >&2 + exit 1 + fi + ;; + sync-team|/sdlc-team-sync) + if declare -F sdlc_team_refresh_done_status >/dev/null 2>&1; then + sdlc_team_refresh_done_status + echo "Team registry refreshed from canvas Final Status." else echo "team registry not installed" >&2 exit 1 @@ -1361,7 +1382,7 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then fi ;; *) - echo "Usage: $0 {status|next|start|capture|resume|advance|skip|shelf|sync|team|list-work|claim|release|list-shelved|help} ..." >&2 + echo "Usage: $0 {status|next|start|capture|resume|advance|skip|shelf|sync|sync-team|team|list-work|claim|release|list-shelved|help} ..." >&2 echo "Try: $0 help" >&2 exit 2 ;; diff --git a/scripts/init-project.sh b/scripts/init-project.sh index 5e31def..87c3b11 100755 --- a/scripts/init-project.sh +++ b/scripts/init-project.sh @@ -240,6 +240,10 @@ copy_if_missing \ "${REPO_ROOT}/templates/agent-context/work-registry.tsv" \ "${TARGET}/agent-context/work-registry.tsv" +copy_if_missing \ + "${REPO_ROOT}/templates/agent-context/hooks/notify-team-registry.example.sh" \ + "${TARGET}/agent-context/hooks/notify-team-registry.example.sh" + copy_if_missing \ "${REPO_ROOT}/agent-context/README.md" \ "${TARGET}/agent-context/README.md" diff --git a/scripts/upgrade-project.sh b/scripts/upgrade-project.sh index f57ee17..574c8b4 100755 --- a/scripts/upgrade-project.sh +++ b/scripts/upgrade-project.sh @@ -407,6 +407,10 @@ copy_framework_file \ "${REPO_ROOT}/templates/agent-context/work-registry.tsv" \ "${TARGET}/agent-context/work-registry.tsv" +copy_framework_file \ + "${REPO_ROOT}/templates/agent-context/hooks/notify-team-registry.example.sh" \ + "${TARGET}/agent-context/hooks/notify-team-registry.example.sh" + for file in \ quality-gates.md \ validation-rules.md; do diff --git a/templates/agent-context/hooks/notify-team-registry.example.sh b/templates/agent-context/hooks/notify-team-registry.example.sh new file mode 100644 index 0000000..28040ae --- /dev/null +++ b/templates/agent-context/hooks/notify-team-registry.example.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# Example SDLC_TEAM_REGISTRY_HOOK — copy to agent-context/hooks/notify-team-registry.sh +# and set: export SDLC_TEAM_REGISTRY_HOOK=./agent-context/hooks/notify-team-registry.sh +# +# Args: work_id status phase operation owner updated note +# +# Slack (set SDLC_TEAM_SLACK_WEBHOOK): +# https://api.slack.com/messaging/webhooks + +set -euo pipefail + +work_id="${1:-}" +status="${2:-}" +phase="${3:-}" +operation="${4:-}" +owner="${5:-}" +updated="${6:-}" +note="${7:-}" + +message="SDLC registry: *${work_id}* → ${status} (phase: ${phase}, op: ${operation:-none}, by: ${owner})" +[[ -n "${note}" ]] && message+=" — ${note}" + +if [[ -n "${SDLC_TEAM_SLACK_WEBHOOK:-}" ]]; then + payload="$(printf '{"text":"%s"}' "${message//\"/\\\"}")" + curl -fsS -X POST -H 'Content-type: application/json' \ + --data "${payload}" \ + "${SDLC_TEAM_SLACK_WEBHOOK}" >/dev/null 2>&1 || true +fi + +# Jira (optional — set SDLC_TEAM_JIRA_WEBHOOK to a script or URL your team approves): +if [[ -n "${SDLC_TEAM_JIRA_WEBHOOK:-}" ]]; then + curl -fsS -X POST -H 'Content-type: application/json' \ + --data "{\"work_id\":\"${work_id}\",\"status\":\"${status}\",\"owner\":\"${owner}\",\"note\":\"${note}\"}" \ + "${SDLC_TEAM_JIRA_WEBHOOK}" >/dev/null 2>&1 || true +fi + +echo "${message}" diff --git a/templates/agent-context/work-registry.tsv b/templates/agent-context/work-registry.tsv index 213f310..b37c6cb 100644 --- a/templates/agent-context/work-registry.tsv +++ b/templates/agent-context/work-registry.tsv @@ -1,6 +1,7 @@ # Team Work Registry — tab-separated. Commit updates so teammates see who is on which Work ID. # Columns: work_id status phase operation owner updated note # status: active | shelved | done | available +# note tokens: branch: pr: jira: # # Updated by: ./scripts/sdlc.sh claim|release|resume|shelf # Override owner label: export SDLC_USER="your-name" diff --git a/templates/claude/commands/sdlc-spdd-whereami.md b/templates/claude/commands/sdlc-spdd-whereami.md index 66216c1..23b1591 100644 --- a/templates/claude/commands/sdlc-spdd-whereami.md +++ b/templates/claude/commands/sdlc-spdd-whereami.md @@ -1,5 +1,5 @@ --- -description: Show current SDLC phase, gates, and the single best next action. +description: Show current SDLC phase, gates, team registry, and the single best next action. argument-hint: --- @@ -13,14 +13,18 @@ Do not implement code. ## Required Behavior -1. Run `./scripts/sdlc-spdd/sdlc.sh next` (or `./scripts/sdlc.sh next` in the orchestrator repo). -2. Read the output: active Work ID, phase, open gates, and recommended command. -3. If no active Work ID, list shelved work and suggest `./scripts/sdlc-spdd/sdlc.sh resume `. -4. Summarize status in plain language and offer the single best next action. -5. Do not start unrelated work; stay on the pointer Work ID unless the user asks to switch. +1. Run `./scripts/sdlc-spdd/sdlc.sh team` (or `./scripts/sdlc.sh team` in the orchestrator repo) to read the committed team registry. +2. Run `./scripts/sdlc-spdd/sdlc.sh list-work` when no active pointer or the user asks what Work IDs exist. +3. Run `./scripts/sdlc-spdd/sdlc.sh next` (or `./scripts/sdlc.sh next`) for local phase, gates, and the recommended command. +4. Check the team registry for conflicts: another owner with a non-stale `active` claim blocks coding unless the user confirms or uses `--force`. +5. Treat `[STALE>Nd]` registry rows as safe to take over with coordination; `done` rows mean pick a different Work ID. +6. If no active Work ID, suggest `./scripts/sdlc-spdd/sdlc.sh claim ` or `resume `. +7. Summarize status in plain language and offer the single best next action (include branch:/pr:/jira: note tokens when present). +8. Do not start unrelated work or implement code on a Work ID claimed by another teammate (non-stale). ## Output -- Short orientation summary (Work ID, phase, progress) +- Team registry summary (owner, phase, stale/done flags, note tokens) +- Local pointer summary (Work ID, phase, next operation if in code phase) - The recommended assistant command or shell command to run next -- Optional: remind user to run `./scripts/sdlc-spdd/sdlc.sh advance` after completing the phase +- Remind user to commit `agent-context/work-registry.tsv` after claim/release diff --git a/templates/copilot/prompts/sdlc-spdd-whereami.prompt.md b/templates/copilot/prompts/sdlc-spdd-whereami.prompt.md index bcf9c7a..be8c9c6 100644 --- a/templates/copilot/prompts/sdlc-spdd-whereami.prompt.md +++ b/templates/copilot/prompts/sdlc-spdd-whereami.prompt.md @@ -1,5 +1,5 @@ --- -description: Show current SDLC phase, gates, and the single best next action. +description: Show current SDLC phase, gates, team registry, and the single best next action. mode: agent --- @@ -11,14 +11,18 @@ Show the user exactly where they are in the workflow and what to do next. Do not ## Required Behavior -1. Run `./scripts/sdlc-spdd/sdlc.sh next` (or `./scripts/sdlc.sh next` in the orchestrator repo). -2. Read the output: active Work ID, phase, open gates, and recommended command. -3. If no active Work ID, list shelved work and suggest `./scripts/sdlc-spdd/sdlc.sh resume `. -4. Summarize status in plain language and offer the single best next action. -5. Do not start unrelated work; stay on the pointer Work ID unless the user asks to switch. +1. Run `./scripts/sdlc-spdd/sdlc.sh team` (or `./scripts/sdlc.sh team` in the orchestrator repo) to read the committed team registry. +2. Run `./scripts/sdlc-spdd/sdlc.sh list-work` when no active pointer or the user asks what Work IDs exist. +3. Run `./scripts/sdlc-spdd/sdlc.sh next` (or `./scripts/sdlc.sh next`) for local phase, gates, and the recommended command. +4. Check the team registry for conflicts: another owner with a non-stale `active` claim blocks coding unless the user confirms or uses `--force`. +5. Treat `[STALE>Nd]` registry rows as safe to take over with coordination; `done` rows mean pick a different Work ID. +6. If no active Work ID, suggest `./scripts/sdlc-spdd/sdlc.sh claim ` or `resume `. +7. Summarize status in plain language and offer the single best next action (include branch:/pr:/jira: note tokens when present). +8. Do not start unrelated work or implement code on a Work ID claimed by another teammate (non-stale). ## Output -- Short orientation summary (Work ID, phase, progress) +- Team registry summary (owner, phase, stale/done flags, note tokens) +- Local pointer summary (Work ID, phase, next operation if in code phase) - The recommended assistant command or shell command to run next -- Optional: remind user to run `./scripts/sdlc-spdd/sdlc.sh advance` after completing the phase +- Remind user to commit `agent-context/work-registry.tsv` after claim/release diff --git a/templates/cursor/sdlc-spdd-whereami.md b/templates/cursor/sdlc-spdd-whereami.md index 233c16c..f4b0841 100644 --- a/templates/cursor/sdlc-spdd-whereami.md +++ b/templates/cursor/sdlc-spdd-whereami.md @@ -8,14 +8,18 @@ Do not implement code. ## Required Behavior -1. Run `./scripts/sdlc-spdd/sdlc.sh next` (or `./scripts/sdlc.sh next` in the orchestrator repo). -2. Read the output: active Work ID, phase, open gates, and recommended command. -3. If no active Work ID, list shelved work and suggest `./scripts/sdlc-spdd/sdlc.sh resume `. -4. Summarize status in plain language and offer the single best next action. -5. Do not start unrelated work; stay on the pointer Work ID unless the user asks to switch. +1. Run `./scripts/sdlc-spdd/sdlc.sh team` (or `./scripts/sdlc.sh team` in the orchestrator repo) to read the committed team registry. +2. Run `./scripts/sdlc-spdd/sdlc.sh list-work` when no active pointer or the user asks what Work IDs exist. +3. Run `./scripts/sdlc-spdd/sdlc.sh next` (or `./scripts/sdlc.sh next`) for local phase, gates, and the recommended command. +4. Check the team registry for conflicts: another owner with a non-stale `active` claim blocks coding unless the user confirms or uses `--force`. +5. Treat `[STALE>Nd]` registry rows as safe to take over with coordination; `done` rows mean pick a different Work ID. +6. If no active Work ID, suggest `./scripts/sdlc-spdd/sdlc.sh claim ` or `resume `. +7. Summarize status in plain language and offer the single best next action (include branch:/pr:/jira: note tokens when present). +8. Do not start unrelated work or implement code on a Work ID claimed by another teammate (non-stale). ## Output -- Short orientation summary (Work ID, phase, progress) +- Team registry summary (owner, phase, stale/done flags, note tokens) +- Local pointer summary (Work ID, phase, next operation if in code phase) - The recommended assistant command or shell command to run next -- Optional: remind user to run `./scripts/sdlc-spdd/sdlc.sh advance` after completing the phase +- Remind user to commit `agent-context/work-registry.tsv` after claim/release diff --git a/tests/test-sdlc-workflow.sh b/tests/test-sdlc-workflow.sh index 51644ab..54b48e9 100755 --- a/tests/test-sdlc-workflow.sh +++ b/tests/test-sdlc-workflow.sh @@ -237,6 +237,62 @@ echo "== Test 15: list-work discovers repo Work IDs ==" out="$(SDLC_ROOT="${T}" wf "${T}" list-work)" if grep -q 'FEAT-009-team' <<< "${out}"; then ok "list-work shows work id"; else bad "list-work missing id"; fi +# --------------------------------------------------------------------------- +echo "== Test 16: stale claim flagged in team output ==" +T="${WORK}/stale" +work_id="FEAT-010-stale" +setup_feature "${T}" "${work_id}" +printf 'work_id\tstatus\tphase\toperation\towner\tupdated\tnote\n' > "${T}/agent-context/work-registry.tsv" +printf 'FEAT-010-stale\tactive\tcode\t\talice\t2020-01-01T00:00:00Z\t\n' >> "${T}/agent-context/work-registry.tsv" +out="$(SDLC_TEAM_STALE_DAYS=0 SDLC_ROOT="${T}" wf "${T}" team)" +if grep -q 'STALE' <<< "${out}"; then ok "stale claim flagged"; else bad "stale flag missing"; fi + +# --------------------------------------------------------------------------- +echo "== Test 17: done status from canvas Final Status ==" +T="${WORK}/done" +work_id="CHORE-001-done" +setup_feature "${T}" "${work_id}" +cp "${REPO_ROOT}/spdd/canvas/CHORE-001-docgen-initial-documentation.md" "${T}/spdd/canvas/${work_id}.md" +SDLC_ROOT="${T}" wf "${T}" sync-team >/dev/null +if grep -q $'CHORE-001-done\tdone\t' "${T}/agent-context/work-registry.tsv"; then + ok "sync-team marks canvas complete as done" +else + bad "done status not written" +fi + +# --------------------------------------------------------------------------- +echo "== Test 18: claim records branch and pr note tokens ==" +T="${WORK}/notes" +work_id="FEAT-011-notes" +setup_feature "${T}" "${work_id}" +SDLC_USER="dev1" SDLC_ROOT="${T}" wf "${T}" claim "${work_id}" --branch "cursor/feat-011" --pr "#99" >/dev/null +if grep -q 'branch:cursor/feat-011' "${T}/agent-context/work-registry.tsv" \ + && grep -q 'pr:#99' "${T}/agent-context/work-registry.tsv"; then + ok "claim stores branch and pr note tokens" +else + bad "branch/pr tokens missing from registry" +fi + +# --------------------------------------------------------------------------- +echo "== Test 19: registry hook fires on claim ==" +T="${WORK}/hook" +work_id="FEAT-012-hook" +setup_feature "${T}" "${work_id}" +hook_log="${T}/hook.log" +mkdir -p "${T}/agent-context/hooks" +cat > "${T}/agent-context/hooks/notify.sh" <> "${hook_log}" +EOF +chmod +x "${T}/agent-context/hooks/notify.sh" +SDLC_TEAM_REGISTRY_HOOK="${T}/agent-context/hooks/notify.sh" \ + SDLC_USER="hooker" SDLC_ROOT="${T}" wf "${T}" claim "${work_id}" >/dev/null +if [[ -f "${hook_log}" ]] && grep -q 'FEAT-012-hook' "${hook_log}"; then + ok "registry hook invoked" +else + bad "registry hook not invoked" +fi + # --------------------------------------------------------------------------- echo echo "Results: ${pass} passed, ${fail} failed" From ed4205f4bb6fd9ccab3b9c614c851dafb27a32db Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 27 Jun 2026 13:07:18 +0000 Subject: [PATCH 6/7] Formalize milestone requirements as Jira draft source - Scaffold ## Jira section in create-work-from-milestone output - Auto-read Key from requirements/milestones on claim (SDLC_TEAM_AUTO_JIRA) - Show jira:KEY or jira draft in list-work artifacts - Document convention in milestone README, jira-runbook, integration-linking Co-authored-by: John Menke --- agent-context/README.md | 4 ++ agent-context/sdlc-team-registry.sh | 55 +++++++++++++++++++++ agent-context/sdlc-workflow.sh | 2 +- docs/integration-linking.md | 3 +- docs/jira-runbook.md | 11 +++++ scripts/create-work-from-milestone.sh | 21 ++++++++ templates/requirements/milestones/README.md | 15 +++++- tests/test-sdlc-workflow.sh | 27 ++++++++++ 8 files changed, 135 insertions(+), 3 deletions(-) diff --git a/agent-context/README.md b/agent-context/README.md index 1d1549f..2dbd14c 100644 --- a/agent-context/README.md +++ b/agent-context/README.md @@ -161,8 +161,12 @@ Set `SDLC_USER="Jane"` to label registry rows. Set `SDLC_NO_TEAM_REGISTRY=1` to ```bash ./scripts/sdlc.sh claim FEAT-001 --branch cursor/feat-001 --pr "#21" --jira "PROJ-123" # auto-detects current git branch on claim (disable: SDLC_TEAM_AUTO_BRANCH=0) +# auto-reads Jira Key from requirements/milestones/.md ## Jira (disable: SDLC_TEAM_AUTO_JIRA=0) ``` +Jira **draft syntax** for issue creation lives in `requirements/milestones/.md` under +`## Jira`. Set `- Key: ABC-123` after create; `list-work` shows `jira:ABC-123` or `jira draft`. + **Notifications:** copy `agent-context/hooks/notify-team-registry.example.sh` and set: ```bash diff --git a/agent-context/sdlc-team-registry.sh b/agent-context/sdlc-team-registry.sh index 8fe584d..aadabb9 100755 --- a/agent-context/sdlc-team-registry.sh +++ b/agent-context/sdlc-team-registry.sh @@ -108,6 +108,53 @@ _team_auto_branch() { git -C "${SDLC_ROOT}" branch --show-current 2>/dev/null || true } +_team_milestone_path() { + local work_id="$1" + local path="${SDLC_ROOT}/requirements/milestones/${work_id}.md" + [[ -f "${path}" ]] && printf '%s' "${path}" +} + +# Reads Jira key from requirements/milestones/.md ## Jira section (- Key: ABC-123). +_team_jira_from_milestone() { + local work_id="$1" + local path + path="$(_team_milestone_path "${work_id}")" + [[ -n "${path}" ]] || return 0 + awk ' + /^## Jira/ { in_jira=1; next } + /^## / { if (in_jira) exit } + in_jira && /^[[:space:]]*(-[[:space:]]+)?[Kk]ey:[[:space:]]*/ { + sub(/^[[:space:]]*(-[[:space:]]+)?[Kk]ey:[[:space:]]*/, "") + gsub(/^[[:space:]]+|[[:space:]]+$/, "") + if ($0 ~ /^[A-Z][A-Z0-9]+-[0-9]+$/ && $0 !~ /^(TBD|TODO|NONE)$/i) { + print $0 + exit + } + } + ' "${path}" +} + +_team_milestone_has_jira_draft() { + local work_id="$1" + local path + path="$(_team_milestone_path "${work_id}")" + [[ -n "${path}" ]] || return 1 + grep -q '^## Jira' "${path}" +} + +_team_auto_jira() { + local jira="${1:-}" + local work_id="${2:-}" + if [[ -n "${jira}" ]]; then + printf '%s' "${jira}" + return 0 + fi + if [[ "${SDLC_TEAM_AUTO_JIRA:-1}" != "1" ]]; then + return 0 + fi + _team_jira_from_milestone "${work_id}" +} + _team_run_hook() { local work_id="$1" local status="$2" @@ -321,6 +368,13 @@ sdlc_team_infer_work_summary() { [[ -d "${root}/agent-context/features/${work_id}" ]] && parts+=("feature workspace") [[ -f "${root}/spdd/canvas/${work_id}.md" ]] && parts+=("canvas") [[ -f "${root}/requirements/milestones/${work_id}.md" ]] && parts+=("milestone") + local jira_key + jira_key="$(_team_jira_from_milestone "${work_id}")" + if [[ -n "${jira_key}" ]]; then + parts+=("jira:${jira_key}") + elif _team_milestone_has_jira_draft "${work_id}"; then + parts+=("jira draft") + fi if ((${#parts[@]} == 0)); then printf 'artifacts unknown' else @@ -427,6 +481,7 @@ sdlc_team_claim() { return 1 fi branch="$(_team_auto_branch "${branch}")" + jira="$(_team_auto_jira "${jira}" "${work_id}")" local existing_note note existing_note="$(_team_registry_note_for "${work_id}")" note="$(_team_compose_note "${existing_note}" "${branch}" "${pr}" "${jira}" "${note_extra}")" diff --git a/agent-context/sdlc-workflow.sh b/agent-context/sdlc-workflow.sh index a741690..9de4e82 100755 --- a/agent-context/sdlc-workflow.sh +++ b/agent-context/sdlc-workflow.sh @@ -551,7 +551,7 @@ In chat: /sdlc-spdd-whereami Team sharing: commit agent-context/work-registry.tsv after claim/release/shelf. Set SDLC_USER to override the owner name. SDLC_NO_TEAM_REGISTRY=1 opts out. SDLC_TEAM_STALE_DAYS=7 flags stale active claims. SDLC_TEAM_REGISTRY_HOOK for Slack/Jira. -Note tokens: branch:... pr:... jira:... (auto branch from git on claim by default). +Note tokens: branch:... pr:... jira:... (auto branch from git; auto jira Key from requirements/milestones/.md on claim). Typical loop: 1. ./scripts/sdlc.sh next diff --git a/docs/integration-linking.md b/docs/integration-linking.md index 1c7d580..ceb958b 100644 --- a/docs/integration-linking.md +++ b/docs/integration-linking.md @@ -51,7 +51,8 @@ For detailed issue creation and synchronization steps, see [jira-runbook.md](jir ### Create a new Jira issue -When the request starts outside Jira, first draft the issue from the requirement: +When the request starts outside Jira, draft field syntax in `requirements/milestones/.md` +under `## Jira` first (see [jira-runbook.md](jira-runbook.md)). Then create from that draft: Draft a Jira issue for this request. Include issue type, summary, business value, scope in, scope out, Given/When/Then acceptance criteria, labels, components, and links. diff --git a/docs/jira-runbook.md b/docs/jira-runbook.md index 063047e..d25b77a 100644 --- a/docs/jira-runbook.md +++ b/docs/jira-runbook.md @@ -18,6 +18,17 @@ Jira should not replace the canvas. The canvas should not replace Jira workflow Use this flow when a request starts outside Jira. +### 0. Draft in the milestone requirement (recommended) + +For Work IDs created from milestones, store Jira field syntax in: + + requirements/milestones/.md + +under `## Jira` (scaffolded by `create-work-from-milestone.sh`). Fill Summary, Description, +and Given/When/Then acceptance criteria there first — it is the copy-paste source for Jira UI, +MCP, or API creation. After Jira returns a key, set `- Key: ABC-123` in that section and commit. +`./scripts/sdlc.sh claim ` then auto-links `jira:ABC-123` in `work-registry.tsv`. + ### 1. Triage the request Prompt: diff --git a/scripts/create-work-from-milestone.sh b/scripts/create-work-from-milestone.sh index 652f890..f0c5430 100755 --- a/scripts/create-work-from-milestone.sh +++ b/scripts/create-work-from-milestone.sh @@ -382,6 +382,27 @@ ${title} - [ ] Define acceptance criteria before coding. +## Jira + +Draft for issue creation — paste the fields below into Jira UI, MCP, or your approved API. +After the issue exists, set **Key** and commit; \`claim\` auto-links \`jira:\` in the team registry. + +- Key: TBD +- Issue type: Story +- Summary: ${title} +- Labels: sdlc-spdd +- Components: + +### Description + +${title} + +Derived from ${milestone_rel}. + +### Acceptance criteria (Given/When/Then) + +- Given ... When ... Then ... + ## Next Step Run: diff --git a/templates/requirements/milestones/README.md b/templates/requirements/milestones/README.md index a737f1d..9403aea 100644 --- a/templates/requirements/milestones/README.md +++ b/templates/requirements/milestones/README.md @@ -15,13 +15,26 @@ Use these files in plan prompts: /sdlc-spdd-plan @requirements/milestones/.md @ROADMAP.md @milestone-1.md +## Jira issue drafts + +Each milestone requirement file is the **natural place to store Jira syntax** before and after +issue creation. Keep copy-paste-ready fields under `## Jira`: + +- **Before create** — fill Summary, Description, acceptance criteria, labels, components +- **After create** — set `- Key: ABC-123` and commit +- **On claim** — `./scripts/sdlc.sh claim ` auto-reads the Key into the team registry + `jira:` note token (disable with `SDLC_TEAM_AUTO_JIRA=0`) + +See [jira-runbook.md](../../docs/jira-runbook.md) for the full create-and-sync flow. + ## Relationship to other planning artifacts | Artifact | Role | |----------|------| | `milestone-*.md` | Goal, scope checklist, linked Work IDs | -| `requirements/milestones/` | Per-item requirement stubs derived from milestones | +| `requirements/milestones/` | Per-item requirement stubs + Jira draft syntax | | `session-notes/` | Daily agent-session narrative | | `ROADMAP.md` | Milestone progress and current focus | Ad-hoc requirements (not from a milestone) live directly under `requirements/` instead. +Use the same `## Jira` section there when the work will be tracked in Jira. diff --git a/tests/test-sdlc-workflow.sh b/tests/test-sdlc-workflow.sh index 54b48e9..97805f1 100755 --- a/tests/test-sdlc-workflow.sh +++ b/tests/test-sdlc-workflow.sh @@ -293,6 +293,33 @@ else bad "registry hook not invoked" fi +# --------------------------------------------------------------------------- +echo "== Test 20: claim auto-reads jira Key from milestone requirement ==" +T="${WORK}/milestone-jira" +work_id="FEAT-013-jira" +setup_feature "${T}" "${work_id}" +mkdir -p "${T}/requirements/milestones" +cat > "${T}/requirements/milestones/${work_id}.md" <<'EOF' +# Requirement: FEAT-013-jira + +## Jira + +- Key: ORCH-42 +- Summary: test issue +EOF +SDLC_USER="dev2" SDLC_ROOT="${T}" wf "${T}" claim "${work_id}" >/dev/null +if grep -q 'jira:ORCH-42' "${T}/agent-context/work-registry.tsv"; then + ok "claim auto-reads jira key from milestone" +else + bad "milestone jira key not in registry" +fi +out="$(SDLC_ROOT="${T}" wf "${T}" list-work)" +if grep -q 'jira:ORCH-42' <<< "${out}"; then + ok "list-work shows milestone jira key" +else + bad "list-work missing jira key" +fi + # --------------------------------------------------------------------------- echo echo "Results: ${pass} passed, ${fail} failed" From 4471388c92574ae275cbad5f3db16e89a40a910a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 27 Jun 2026 13:10:45 +0000 Subject: [PATCH 7/7] Add milestones README to orchestrator requirements folder for dogfooding Co-authored-by: John Menke --- requirements/milestones/README.md | 40 +++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 requirements/milestones/README.md diff --git a/requirements/milestones/README.md b/requirements/milestones/README.md new file mode 100644 index 0000000..9403aea --- /dev/null +++ b/requirements/milestones/README.md @@ -0,0 +1,40 @@ +# Milestone-Derived Requirements + +This folder holds requirement stubs created from milestone checklist items. + +## Purpose + +When you run `create-work-from-milestone.sh`, each unchecked milestone item becomes: + +- a Work ID +- a requirement file here: `requirements/milestones/.md` +- a draft REASONS Canvas under `spdd/canvas/.md` +- a **Linked Work** row in the source `milestone-*.md` file + +Use these files in plan prompts: + + /sdlc-spdd-plan @requirements/milestones/.md @ROADMAP.md @milestone-1.md + +## Jira issue drafts + +Each milestone requirement file is the **natural place to store Jira syntax** before and after +issue creation. Keep copy-paste-ready fields under `## Jira`: + +- **Before create** — fill Summary, Description, acceptance criteria, labels, components +- **After create** — set `- Key: ABC-123` and commit +- **On claim** — `./scripts/sdlc.sh claim ` auto-reads the Key into the team registry + `jira:` note token (disable with `SDLC_TEAM_AUTO_JIRA=0`) + +See [jira-runbook.md](../../docs/jira-runbook.md) for the full create-and-sync flow. + +## Relationship to other planning artifacts + +| Artifact | Role | +|----------|------| +| `milestone-*.md` | Goal, scope checklist, linked Work IDs | +| `requirements/milestones/` | Per-item requirement stubs + Jira draft syntax | +| `session-notes/` | Daily agent-session narrative | +| `ROADMAP.md` | Milestone progress and current focus | + +Ad-hoc requirements (not from a milestone) live directly under `requirements/` instead. +Use the same `## Jira` section there when the work will be tracked in Jira.