Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/workflows/test-sdlc-pointer.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Test SDLC Pointer

on:
pull_request:
paths:
- 'agent-context/sdlc-pointer.sh'
- 'agent-context/README.md'
- 'scripts/init-project.sh'
- 'scripts/upgrade-project.sh'
- 'scripts/start-agent-session.sh'
- 'scripts/verify-project-install.sh'
- 'tests/test-sdlc-pointer.sh'
- '.github/workflows/test-sdlc-pointer.yml'
push:
branches: [main]
paths:
- 'agent-context/sdlc-pointer.sh'
- 'agent-context/README.md'
- 'scripts/init-project.sh'
- 'scripts/upgrade-project.sh'
- 'scripts/start-agent-session.sh'
- 'scripts/verify-project-install.sh'
- 'tests/test-sdlc-pointer.sh'
- '.github/workflows/test-sdlc-pointer.yml'

jobs:
test-sdlc-pointer:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Check shell syntax
run: bash -n agent-context/sdlc-pointer.sh tests/test-sdlc-pointer.sh scripts/start-agent-session.sh

- name: Run SDLC pointer regression tests
run: ./tests/test-sdlc-pointer.sh
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Local SDLC pointer state (see agent-context/sdlc-pointer.sh)
.sdlc/

# Python venv for docgen (orchestrator dev only — see scripts/setup-docgen-venv.sh)
.venv/

Expand Down
25 changes: 25 additions & 0 deletions agent-context/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,31 @@ Full detail: [Bootstrap and index-based loading](../docs/context-loading-and-sca

Each work item also has a canonical canvas under `spdd/canvas/`. Keep both copies aligned using `/sdlc-spdd-sync` or `./scripts/sync-agent-context.sh`.

## SDLC Pointer (current chore/task)

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.

```bash
# Source once per shell (or let start-agent-session.sh set the pointer for you)
source agent-context/sdlc-pointer.sh

# Set / inspect / clear
./agent-context/sdlc-pointer.sh set CHORE-123
./agent-context/sdlc-pointer.sh get
./agent-context/sdlc-pointer.sh reset

# Guarded execution — exits 3 when the pointer does not match
run_against_pointer "CHORE-123" -- ./scripts/sdlc-spdd/capture-session-memory.sh --work-id CHORE-123 ...

# Optional bootstrap override on agent start
export SDLC_POINTER_OVERRIDE=CHORE-123
sdlc_init
```

`start-agent-session.sh` sets the pointer automatically when `--work-id` is provided.

## Session Persistence

Use scripts to keep agent sessions durable across chat boundaries:
Expand Down
120 changes: 120 additions & 0 deletions agent-context/sdlc-pointer.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
#!/usr/bin/env bash
# Simple pointer manager for SDLC chores/tasks
# Usage:
# source agent-context/sdlc-pointer.sh
# sdlc_init # call on initialization
# sdlc_set_pointer ID # sets pointer
# sdlc_get_pointer # prints pointer or empty
# sdlc_reset_pointer # clears pointer
# run_against_pointer "<expected>" -- <command...>
#
# Default storage: .sdlc/pointer in the repository root.

if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
set -euo pipefail
fi

SDLC_ROOT="${SDLC_ROOT:-$(git -C "${PWD}" rev-parse --show-toplevel 2>/dev/null || pwd)}"
SDLC_DIR="${SDLC_DIR:-${SDLC_ROOT}/.sdlc}"
SDLC_POINTER="${SDLC_DIR}/pointer"
SDLC_LOCK="${SDLC_DIR}/pointer.lock"

mkdir -p "${SDLC_DIR}"

_have_flock() {
command -v flock >/dev/null 2>&1
}

sdlc_get_pointer() {
if [[ ! -f "${SDLC_POINTER}" ]]; then
printf ""
return 0
fi
cat "${SDLC_POINTER}"
}

sdlc_set_pointer() {
local new="${1:-}"
if [[ -z "${new}" ]]; then
echo "sdlc_set_pointer: missing pointer id" >&2
return 2
fi
if _have_flock; then
flock --exclusive --timeout 5 "${SDLC_LOCK}" \
bash -c 'printf "%s" "$1" > "$2"' _ "${new}" "${SDLC_POINTER}"
else
local tmpfile="${SDLC_POINTER}.$(date +%s).tmp"
printf '%s' "${new}" > "${tmpfile}"
mv -f "${tmpfile}" "${SDLC_POINTER}"
fi
echo "pointer set to: ${new}"
}

sdlc_reset_pointer() {
if _have_flock; then
flock --exclusive --timeout 5 "${SDLC_LOCK}" \
bash -c 'rm -f "$1"' _ "${SDLC_POINTER}"
else
rm -f "${SDLC_POINTER}" 2>/dev/null || true
fi
echo "pointer cleared"
}

sdlc_init() {
if [[ -n "${SDLC_POINTER_OVERRIDE:-}" ]]; then
sdlc_set_pointer "${SDLC_POINTER_OVERRIDE}"
fi
}

run_against_pointer() {
local expected="${1:-}"
shift || true
if [[ "${1:-}" == "--" ]]; then
shift
fi
if [[ -z "${expected}" ]]; then
echo "run_against_pointer: expected pointer id required" >&2
return 2
fi
local current=""
if _have_flock; then
current="$(flock --shared --timeout 5 "${SDLC_LOCK}" \
bash -c 'cat "$1" 2>/dev/null || true' _ "${SDLC_POINTER}")"
else
current="$(cat "${SDLC_POINTER}" 2>/dev/null || true)"
fi
if [[ "${current}" != "${expected}" ]]; then
echo "Pointer mismatch: current='${current}' expected='${expected}' — refusing to run" >&2
return 3
fi
"$@"
}

# CLI when script executed directly
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
cmd="${1:-}"
shift || true
case "${cmd}" in
set|/sdlc-set-pointer)
sdlc_set_pointer "$@"
;;
get|/sdlc-get-pointer)
sdlc_get_pointer
;;
reset|/sdlc-reset-pointer)
sdlc_reset_pointer
;;
init|/sdlc-init)
sdlc_init
;;
run|/sdlc-run)
expected="${1:-}"
shift || true
run_against_pointer "${expected}" -- "$@"
;;
*)
echo "Usage: $0 {set|get|reset|init|run} ..." >&2
exit 2
;;
esac
fi
11 changes: 11 additions & 0 deletions scripts/init-project.sh
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,17 @@ copy_if_missing \
"${REPO_ROOT}/agent-context/harness/validation-rules.md" \
"${TARGET}/agent-context/harness/validation-rules.md"

copy_if_missing \
"${REPO_ROOT}/agent-context/sdlc-pointer.sh" \
"${TARGET}/agent-context/sdlc-pointer.sh"
if [[ "${DRY_RUN}" -eq 0 && -f "${TARGET}/agent-context/sdlc-pointer.sh" ]]; then
chmod +x "${TARGET}/agent-context/sdlc-pointer.sh"
fi

copy_if_missing \
"${REPO_ROOT}/agent-context/README.md" \
"${TARGET}/agent-context/README.md"

# Copy user-facing SDLC-SPDD docs into the target project.
# Skip orchestrator-internal docs (see scripts/lib/shipped-docs-boundary.sh).
# shellcheck source=lib/shipped-docs-boundary.sh
Expand Down
9 changes: 9 additions & 0 deletions scripts/start-agent-session.sh
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@ case "${PHASE}" in
esac

TARGET="$(cd "${TARGET}" && pwd)"

pointer_script="${TARGET}/agent-context/sdlc-pointer.sh"
if [[ -f "${pointer_script}" && -n "${WORK_ID}" ]]; then
SDLC_ROOT="${TARGET}"
# shellcheck source=/dev/null
source "${pointer_script}"
sdlc_set_pointer "${WORK_ID}" >/dev/null
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"
Expand Down
4 changes: 4 additions & 0 deletions scripts/upgrade-project.sh
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,10 @@ copy_framework_file \
"${REPO_ROOT}/agent-context/README.md" \
"${TARGET}/agent-context/README.md"

copy_executable_framework_file \
"${REPO_ROOT}/agent-context/sdlc-pointer.sh" \
"${TARGET}/agent-context/sdlc-pointer.sh"

for file in \
quality-gates.md \
validation-rules.md; do
Expand Down
3 changes: 2 additions & 1 deletion scripts/verify-project-install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,8 @@ run_part "SDLC (sessions, memory, playbooks)" \
SDLC "harness directory" "agent-context/harness" dir \
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 "quality gates" "agent-context/harness/quality-gates.md" file \
SDLC "pointer manager script" "agent-context/sdlc-pointer.sh" executable

run_part "Runtime scripts and docs" \
Runtime "runtime scripts directory" "scripts/sdlc-spdd" dir \
Expand Down
97 changes: 97 additions & 0 deletions tests/test-sdlc-pointer.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#!/usr/bin/env bash
set -euo pipefail

# Regression harness for agent-context/sdlc-pointer.sh
#
# Usage: ./tests/test-sdlc-pointer.sh

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
POINTER="${REPO_ROOT}/agent-context/sdlc-pointer.sh"
START="${REPO_ROOT}/scripts/start-agent-session.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)); }

run_pointer() {
SDLC_ROOT="${1}" "${POINTER}" "${@:2}"
}

source_pointer() {
SDLC_ROOT="${1}"
# shellcheck source=/dev/null
source "${POINTER}"
}

# ---------------------------------------------------------------------------
echo "== Test 1: CLI set/get/reset =="
T="${WORK}/cli"
mkdir -p "${T}"
run_pointer "${T}" set FEAT-001 >/dev/null
current="$(run_pointer "${T}" get)"
if [[ "${current}" == "FEAT-001" ]]; then ok "set/get round-trip"; else bad "expected FEAT-001, got '${current}'"; fi
run_pointer "${T}" reset >/dev/null
current="$(run_pointer "${T}" get)"
if [[ -z "${current}" ]]; then ok "reset clears pointer"; else bad "expected empty pointer after reset"; fi

# ---------------------------------------------------------------------------
echo "== Test 2: run_against_pointer guards execution =="
T="${WORK}/guard"
mkdir -p "${T}"
source_pointer "${T}"
sdlc_set_pointer "CHORE-123" >/dev/null
if run_against_pointer "CHORE-123" -- printf ok | grep -q ok; then
ok "guarded run succeeds when pointer matches"
else
bad "guarded run should succeed when pointer matches"
fi
if run_against_pointer "CHORE-999" -- printf fail >/dev/null 2>&1; then
bad "guarded run should fail on mismatch"
else
ok "guarded run refuses mismatch"
fi

# ---------------------------------------------------------------------------
echo "== Test 3: sdlc_init honors SDLC_POINTER_OVERRIDE =="
T="${WORK}/init"
mkdir -p "${T}"
SDLC_ROOT="${T}" SDLC_POINTER_OVERRIDE=SPIKE-001 "${POINTER}" init >/dev/null
current="$(run_pointer "${T}" get)"
if [[ "${current}" == "SPIKE-001" ]]; then ok "sdlc_init sets override"; else bad "sdlc_init override failed"; fi

# ---------------------------------------------------------------------------
echo "== Test 4: start-agent-session.sh sets pointer from --work-id =="
T="${WORK}/session"
mkdir -p "${T}/agent-context/sessions"
cp "${POINTER}" "${T}/agent-context/sdlc-pointer.sh"
chmod +x "${T}/agent-context/sdlc-pointer.sh"
"${START}" --target "${T}" --work-id FEAT-002-session --phase plan >/dev/null
current="$(SDLC_ROOT="${T}" "${T}/agent-context/sdlc-pointer.sh" get)"
if [[ "${current}" == "FEAT-002-session" ]]; then
ok "start-agent-session sets pointer"
else
bad "expected FEAT-002-session from session start, got '${current}'"
fi

# ---------------------------------------------------------------------------
echo "== Test 5: init-project installs pointer script =="
T="${WORK}/install"
mkdir -p "${T}"
"${REPO_ROOT}/scripts/init-project.sh" --target "${T}" >/dev/null
if [[ -x "${T}/agent-context/sdlc-pointer.sh" ]]; then
ok "init-project installs executable pointer script"
else
bad "init-project missing agent-context/sdlc-pointer.sh"
fi

# ---------------------------------------------------------------------------
echo
echo "Results: ${pass} passed, ${fail} failed"
if [[ "${fail}" -gt 0 ]]; then
exit 1
fi
Loading