From d8119d7e8bd12c25d1dc5f4870eb6f32ef2d8ccb Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 14 Sep 2026 04:47:17 +0000 Subject: [PATCH 01/18] docs: define REF-003 Python-only workflow plan Co-authored-by: John Menke --- ...retire-bash-workflow-dual-path-analysis.md | 209 ++++++++++++++ .../REF-003-retire-bash-workflow-dual-path.md | 261 ++++++++++++++++++ sdlc-spdd/spdd/memory/registry.jsonl | 1 + 3 files changed, 471 insertions(+) create mode 100644 sdlc-spdd/spdd/analysis/REF-003-retire-bash-workflow-dual-path-analysis.md create mode 100644 sdlc-spdd/spdd/canvas/REF-003-retire-bash-workflow-dual-path.md diff --git a/sdlc-spdd/spdd/analysis/REF-003-retire-bash-workflow-dual-path-analysis.md b/sdlc-spdd/spdd/analysis/REF-003-retire-bash-workflow-dual-path-analysis.md new file mode 100644 index 00000000..8d97bba7 --- /dev/null +++ b/sdlc-spdd/spdd/analysis/REF-003-retire-bash-workflow-dual-path-analysis.md @@ -0,0 +1,209 @@ +# REF-003 Analysis — Retire the bash workflow twin; Python is the only flow + +## Metadata + +- Work ID: REF-003-retire-bash-workflow-dual-path +- Requirement: `sdlc-spdd/requirements/milestones/milestone-3/REF-003-retire-bash-workflow-dual-path.md` +- Milestone: Milestone 3 — One flow on storage v3 (P0, size L) +- Opened by: `sdlc-spdd/spdd/analysis/SPIKE-005-architecture-review-analysis.md` finding M1 +- Timestamp: 2026-09-14 +- Beck stage: make it right +- Prior related work: `REF-001-engine-single-source`, `REF-002-purge-pre-v3-compat` +- Context retrieval: no work-specific analysis records; optional Guide backend was unavailable, so file/ledger retrieval remained authoritative. + +## Scope Lock + +### In Scope for This Work + +Remove the duplicate bash implementations of workflow, gate, pointer, and team +registry. Python `sdlc_engine` becomes mandatory for all lifecycle behavior. +Install and upgrade remain bash packaging scripts, and session/capture remain +shell utilities until REF-010, but those utilities must call the Python engine +instead of sourcing retired behavior. + +1. **Mandatory dispatcher**: simplify `scripts/sdlc.sh` and its installed + dogfood copy so every lifecycle command executes `python -m sdlc_engine` + through `resolve_engine_python`. Remove workflow-script probing and shell + fallback. `SDLC_ENGINE=shell` and `SDLC_GATE_ENGINE=shell` fail with a clear + Python-only message. Existing `SDLC_ENGINE=python` callers remain harmless; + they no longer select different semantics. +2. **Retained shell utilities**: preserve `start-agent-session.sh`, + `capture-session-memory.sh`, and `accept-lessons.sh`, but remove imports of + `sdlc-pointer.sh`, `sdlc-workflow.sh`, and `sdlc-team-registry.sh`. + Pointer/status/next/team/context-accept behavior is obtained through the + Python CLI. Any Python invocation uses `resolve_engine_python`, never bare + `python3`. +3. **Installed-target shell bridge**: extend `cmd_shell` to resolve scripts + from both orchestrator `scripts/` and installed + `/sdlc-spdd/scripts/`. Preserve argument forwarding and target + working directory. +4. **Delete the twin**: remove + `templates/agent-context/sdlc-workflow.sh` (2,046 LOC), + `sdlc-team-registry.sh` (927 LOC), and `sdlc-pointer.sh` (127 LOC), plus + their dogfood copies under `sdlc-spdd/scripts/`. The shipped template + reduction is exactly 3,100 LOC before replacement code. +5. **Install and upgrade**: stop installing the three retired scripts, + remove them from existing v3 installs during upgrade, and change install + verification to require their absence while requiring executable + `sdlc.sh`. Initialization must not recreate them. +6. **Tests and CI**: rewrite dispatcher, pointer, workflow/gate, archive, and + installed-target tests to exercise Python-only behavior. The repository now + contains 28 `tests/test-*.sh` harnesses (the requirement's count of 30 + predates REF-002 deleting two consolidation harnesses); all 28 must either + remain green against the Python-only dispatcher or be replaced by pytest + coverage in the same operation. Remove CI workflows only when their named + harness is removed. +7. **Documentation**: update `README.md`, `TESTING.md`, `docs/engine-v2.md`, + shipped documentation mirrors, template runtime documentation, and + `CHANGELOG.md` to state that there is one mandatory Python engine and bash + is retained only for packaging or explicitly named utilities. + +### Not in Scope + +| Item | Why deferred | Target | +|------|--------------|--------| +| Reimplementing session/capture in Python | Explicit requirement non-goal; wrappers only lose dependencies on the retired twin | REF-010-pythonize-session-and-capture | +| Removing install/upgrade shell | Bash remains the packaging layer | none | +| Typed records and atomic JSONL appends | Storage hardening, not engine consolidation | REF-006-storage-records-and-atomic-appends | +| Splitting issue synchronization | Unrelated service boundary | REF-011-split-issue-sync-adapters | +| Guide changes or upstream contributions | Explicit non-goal and workspace safeguard | none | +| Rewriting historical milestone/canvas/review records | They describe past dual-engine decisions and remain audit evidence | none | + +## Domain Keywords + +- Python-only engine, mandatory engine, single flow +- workflow, gate, pointer, team registry +- thin dispatcher, shell bridge, installed target +- `SDLC_ENGINE`, `SDLC_GATE_ENGINE`, `SDLC_PY` +- session brief, capture, accept +- init, upgrade, verify install +- shell harness, pytest replacement, LOC reduction + +## Code Areas + +- Dispatcher and Python resolution: `scripts/sdlc.sh`, `scripts/lib/python.sh` +- Python CLI: `engine/src/sdlc_engine/cli_commands.py`, + `engine/src/sdlc_engine/cli_parser.py` +- Retained utilities: `scripts/start-agent-session.sh`, + `scripts/capture-session-memory.sh`, `scripts/accept-lessons.sh` +- Packaging: `scripts/init-project.sh`, `scripts/upgrade-project.sh`, + `scripts/verify-project-install.sh` +- Retired templates: `templates/agent-context/sdlc-{workflow,team-registry,pointer}.sh` +- Dogfood runtime mirror: `sdlc-spdd/scripts/` +- Tests: `engine/tests_unit/`, `engine/tests_integration/`, `tests/test-*.sh`, + `tests/live-consumer/`, `.github/workflows/` +- Docs: `README.md`, `TESTING.md`, `docs/engine-v2.md`, + `templates/agent-context/README.md`, `sdlc-spdd/docs/`, `CHANGELOG.md` + +## Existing Concepts + +- `scripts/sdlc.sh` currently defaults `SDLC_ENGINE=auto`, probes whether the + Python package is importable, routes selected commands to Python, and falls + through to `sdlc-workflow.sh`. `SDLC_ENGINE=shell` bypasses Python except + for a hard-coded list of Python-only commands. +- `sdlc-workflow.sh` sources pointer and registry implementations and contains + a second lifecycle phase table, gate table, state machine, artifact scanner, + capture wrapper, and command parser. +- `start-agent-session.sh` sources all three retired scripts for pointer + updates, workflow synchronization, recommended commands, and Jira/team + snippets. It also invokes bare `python3` in database lookup fallbacks. +- `capture-session-memory.sh` already writes v3 staged lesson records itself. + Its only workflow dependency is a final optional call that records a + shell-workflow gate marker; Python gates use ledger validation receipts and + do not need that marker. +- `accept-lessons.sh` contains Python-engine detection plus a pure-shell + fallback implemented with inline Python. The fallback is a second + persistence behavior and must disappear; Python already implements + `context accept`. +- `cmd_shell` currently searches only `/scripts`, which works in this + orchestrator but not in installed consumers whose helpers live under + `/sdlc-spdd/scripts`. +- Init/upgrade copy all three retired template files. Verification currently + requires them to be executable. + +## New Concepts + +- **Mandatory engine**: importability is a prerequisite, not a routing choice. + Every lifecycle command has one parser, one state machine, one registry, and + one gate implementation. +- **Utility bridge**: commands that intentionally remain shell are invoked + through the Python CLI's `shell` bridge. Their shell implementation is + packaging/session functionality, not a lifecycle fallback. +- **Retired-file invariant**: fresh installs and upgrades contain + `sdlc-spdd/scripts/sdlc.sh` but do not contain the three bash twins. +- **Historical-reference boundary**: active operator docs and tests must not + advertise shell-engine selection; completed canvases/reviews and changelog + history may retain factual references to the former design. + +## Strategic Direction + +Implement in dependency order: + +1. Make the Python shell bridge support installed targets and add focused unit + coverage. +2. Convert the dispatcher and retained utilities to mandatory Python behavior + while the old files still exist, proving there is no runtime dependency. +3. Update packaging and verification, then delete both template and dogfood + copies of the twin. +4. Convert or remove affected shell harnesses and CI entries, preserving + equivalent assertions in Python tests. +5. Synchronize current docs and run the complete validation matrix. + +Each operation must be independently green and committed separately. Deletion +does not begin until tests prove retained utilities and installed-target +bridging no longer source the old scripts. + +## Test Plan + +### Automated + +- Focused pytest for `cmd_shell` source-checkout and installed-target + resolution, missing scripts, argument forwarding, and working directory. +- Dispatcher harness tests for unset/default mode, `SDLC_ENGINE=python`, + explicit rejection of `SDLC_ENGINE=shell` and + `SDLC_GATE_ENGINE=shell`, missing engine install hint, and resolved + Python 3.12 execution. +- Session/capture/accept tests proving no retired file is present and all + persisted state is produced by Python/v3 records. +- Init, upgrade, verification, installer unit/integration, and live-consumer + tests proving fresh and upgraded targets omit the twin. +- All 28 remaining `tests/test-*.sh` harnesses, full engine unit and + integration suites, command-spec validation, ShellCheck, and the full local + CI runner. + +### Terminal smoke + +Initialize a temporary target, verify its installed layout, and exercise +`claim`, `next`, `status`, `gate`, `pointer`, shell-bridged session capture, +and `context accept`. Confirm the retired scripts are absent and unsupported +engine environment values fail non-zero with the documented message. + +## Risks and Gaps + +1. **Installed targets may not contain engine source.** The dispatcher must + emit the existing Python 3.12/setup hint when the package is unavailable; + it must not silently recover to bash. +2. **Session brief content can regress.** Replacing sourced functions with CLI + output may alter formatting or omit Jira/recommended-command context. + Preserve user-visible sections where Python exposes equivalent data and + cover stable contracts rather than exact incidental prose. +3. **Environment variables are used widely.** Active scripts/tests that set + `SDLC_ENGINE=python` should continue to work, but shell/auto selection must + no longer produce alternate behavior. Historical records should not be + mechanically rewritten. +4. **Upgrade deletion is destructive but intentional.** Restrict deletion to + the three exact framework-owned paths under `sdlc-spdd/scripts`; never + remove arbitrary user shell files. +5. **Bare Python calls remain in retained utilities.** Only engine-facing + calls are mandatory scope, but touched utility paths should use + `resolve_engine_python` consistently to avoid interpreter drift. +6. **The requirement's harness count is stale.** REF-002 removed two harnesses + before this work. Validation must use the current inventory of 28 and + document the reconciliation rather than recreating obsolete tests. + +## Recommendation + +Proceed to `/sdlc-spdd-plan` and `/sdlc-spdd-architect`. The behavior contract +is sufficiently specific: one mandatory Python lifecycle engine, exact +retired paths, explicit rejection semantics, retained shell utility boundary, +and a measurable 3,100-line shipped-code reduction. diff --git a/sdlc-spdd/spdd/canvas/REF-003-retire-bash-workflow-dual-path.md b/sdlc-spdd/spdd/canvas/REF-003-retire-bash-workflow-dual-path.md new file mode 100644 index 00000000..b0aff726 --- /dev/null +++ b/sdlc-spdd/spdd/canvas/REF-003-retire-bash-workflow-dual-path.md @@ -0,0 +1,261 @@ +# REASONS Canvas: REF-003-retire-bash-workflow-dual-path — Python-only lifecycle flow + +## Metadata + +- Work ID: REF-003-retire-bash-workflow-dual-path +- Work Type: Refactor +- Status: In Progress +- Readiness: Ready For Coding +- Created: 2026-09-14 +- Updated: 2026-09-14 +- Milestone: milestone-3 +- Depends on: REF-002-purge-pre-v3-compat (Complete) +- Blocks: REF-010-pythonize-session-and-capture +- Related: REF-001-engine-single-source +- Requirement: `sdlc-spdd/requirements/milestones/milestone-3/REF-003-retire-bash-workflow-dual-path.md` +- Analysis: `sdlc-spdd/spdd/analysis/REF-003-retire-bash-workflow-dual-path-analysis.md` +- Beck stage: make it right + +## R - Requirements + +### User Goal + +Operate one lifecycle implementation. Workflow, gate, pointer, and team +registry behavior comes only from Python `sdlc_engine`; bash remains only for +install/upgrade packaging and retained session/capture utilities. + +### Acceptance Criteria + +- [ ] `templates/agent-context/` and `sdlc-spdd/scripts/` contain no + `sdlc-workflow.sh`, `sdlc-team-registry.sh`, or `sdlc-pointer.sh`. +- [ ] `SDLC_ENGINE=shell ./scripts/sdlc.sh next` exits non-zero with a clear + Python-only message. +- [ ] `SDLC_GATE_ENGINE=shell ./scripts/sdlc.sh gate ...` exits non-zero with + the same single-engine contract. +- [ ] Missing/unusable Python 3.12 or `sdlc_engine` exits non-zero with the + install/setup hint; no bash fallback runs. +- [ ] Gate and utility engine calls resolve `SDLC_PY`; no touched path invokes + bare `python3` for engine behavior. +- [ ] `cmd_shell` bridges both orchestrator `scripts/` and installed + `sdlc-spdd/scripts/`. +- [ ] Fresh installs and upgrades omit/remove the three retired files, and + install verification asserts their absence. +- [ ] All current 28 shell harnesses pass against the Python-only dispatcher + or are replaced by equivalent pytest coverage. +- [ ] Shipped duplicate implementation decreases by at least 3,000 LOC + (measured template deletion: 3,100 LOC). +- [ ] Current README, testing, engine, template, and shipped documentation + describe one mandatory Python engine. + +### Non-Goals + +- Pythonizing session/capture scripts (REF-010) +- Removing install/upgrade shell +- Typed/locked storage records (REF-006) +- Splitting issue synchronization (REF-011) +- Embabel Guide work or upstream contributions +- Rewriting historical records that accurately describe the former twin + +## E - Entities + +- `scripts/sdlc.sh`: mandatory-engine dispatcher +- `resolve_engine_python` / `SDLC_PY`: sole interpreter resolution path +- `sdlc_engine` CLI: lifecycle parser and behavior +- `cmd_shell`: retained utility bridge +- `start-agent-session.sh`, `capture-session-memory.sh`, + `accept-lessons.sh`: shell utilities, not lifecycle implementations +- Retired files: `sdlc-workflow.sh`, `sdlc-team-registry.sh`, + `sdlc-pointer.sh` +- Installed home: `/sdlc-spdd/scripts/` +- Current shell harness inventory: 28 `tests/test-*.sh` files + +## A - Approach + +Remove runtime dependencies before deleting files. First teach the Python shell +bridge to find installed utilities. Then make `sdlc.sh` require Python and +rewire retained utilities to use the Python CLI. After focused tests show no +dependency on the twins, update packaging and delete exact framework-owned +paths. Finally migrate affected harnesses, synchronize docs, and run the full +matrix. + +`SDLC_ENGINE=python` remains accepted as a compatibility no-op because it +already means the only supported engine. `SDLC_ENGINE=shell` and any +`SDLC_GATE_ENGINE` override are rejected; neither can select behavior. + +## S - Structure + +### Files to add + +- `sdlc-spdd/spdd/reviews/REF-003-retire-bash-workflow-dual-path-review.md` +- `sdlc-spdd/spdd/sync/REF-003-retire-bash-workflow-dual-path-sync.md` +- Focused pytest coverage in an existing or new + `engine/tests_unit/test_cli_shell.py` + +### Files to delete + +- `templates/agent-context/sdlc-workflow.sh` +- `templates/agent-context/sdlc-team-registry.sh` +- `templates/agent-context/sdlc-pointer.sh` +- `sdlc-spdd/scripts/sdlc-workflow.sh` +- `sdlc-spdd/scripts/sdlc-team-registry.sh` +- `sdlc-spdd/scripts/sdlc-pointer.sh` +- Obsolete shell harnesses/workflows only when equivalent pytest coverage is + committed in the same operation + +### Files to modify + +- Dispatcher/bridge: `scripts/sdlc.sh`, `sdlc-spdd/scripts/sdlc.sh`, + `engine/src/sdlc_engine/cli_commands.py` +- Utilities: `scripts/start-agent-session.sh`, + `scripts/capture-session-memory.sh`, `scripts/accept-lessons.sh` and + installed dogfood copies +- Packaging: `scripts/init-project.sh`, `scripts/upgrade-project.sh`, + `scripts/verify-project-install.sh` and installed dogfood copies +- Tests/workflows: affected `engine/tests_*`, `tests/test-*.sh`, + `tests/live-consumer/`, `.github/workflows/` +- Docs: `README.md`, `TESTING.md`, `docs/engine-v2.md`, relevant + `sdlc-spdd/docs/` mirrors, `templates/agent-context/README.md`, + `CHANGELOG.md` +- Lifecycle artifacts: requirement, milestone, roadmap, review, sync, registry, + and accepted lessons + +## O - Operations + +### T01 - Bridge retained shell utilities in source and installed targets + +- Status: Pending +- Description: Extend Python `cmd_shell` resolution to executable utilities in + orchestrator `scripts/` or installed `/sdlc-spdd/scripts/`, with + stable missing-script, argument-forwarding, and working-directory behavior. +- Files: `engine/src/sdlc_engine/cli_commands.py`, + `engine/tests_unit/test_cli_shell.py` (or nearest existing CLI test module) +- Tests: focused pytest for source, installed, missing, and forwarding cases +- Validation: an installed-target fixture executes a helper through + `sdlc-engine shell` + +### T02 - Make the dispatcher and retained utilities Python-only + +- Status: Pending +- Description: Remove workflow probing/fallback from both `sdlc.sh` copies; + reject shell engine overrides; require resolved Python 3.12; route + start/capture/complete/accept through Python or its shell bridge; stop + retained utilities from sourcing the retired scripts or invoking bare + Python for engine behavior. +- Files: `scripts/sdlc.sh`, `sdlc-spdd/scripts/sdlc.sh`, + `scripts/start-agent-session.sh`, `sdlc-spdd/scripts/start-agent-session.sh`, + `scripts/capture-session-memory.sh`, + `sdlc-spdd/scripts/capture-session-memory.sh`, + `scripts/accept-lessons.sh`, `sdlc-spdd/scripts/accept-lessons.sh`, + dispatcher/session/capture tests +- Tests: `tests/test-sdlc-engine-shim.sh`, focused session/capture/accept pytest +- Validation: default/`python` execute one engine; shell/gate overrides fail; + no retained utility requires a twin + +### T03 - Stop shipping and delete the bash twin + +- Status: Pending +- Description: Remove twin installation from init/upgrade, delete exact + retired paths during upgrade, require their absence in install verification, + and delete template plus dogfood copies. +- Files: `scripts/init-project.sh`, `scripts/upgrade-project.sh`, + `scripts/verify-project-install.sh`, dogfood copies, + `templates/agent-context/sdlc-{workflow,team-registry,pointer}.sh`, + `sdlc-spdd/scripts/sdlc-{workflow,team-registry,pointer}.sh`, + installer tests +- Tests: installer unit/integration, init, upgrade, verify-install, + live-consumer install scenario +- Validation: fresh and upgraded fixtures contain `sdlc.sh`, omit all three + twins, and report at least 3,000 shipped LOC removed + +### T04 - Migrate the shell-harness and CI matrix + +- Status: Pending +- Description: Rewrite affected workflow, pointer, gate, archive, integration, + and live-consumer harnesses to exercise Python-only behavior; remove only + tests/workflows made obsolete by equivalent pytest coverage. Reconcile the + requirement's pre-REF-002 count of 30 with the current inventory of 28. +- Files: affected `tests/test-*.sh`, `tests/live-consumer/`, + `tests/research/test_ref001_sut.py`, `.github/workflows/` +- Tests: all remaining `tests/test-*.sh`, engine unit and integration suites +- Validation: no active test sets `SDLC_ENGINE=shell` or + `SDLC_GATE_ENGINE=shell` except explicit rejection assertions + +### T05 - Synchronize the one-engine documentation contract + +- Status: Pending +- Description: Update active operator, testing, engine, template, and shipped + docs to state the mandatory Python contract; preserve historical audit + records; update changelog and REF-003 lifecycle status. +- Files: `README.md`, `TESTING.md`, `docs/engine-v2.md`, + `templates/agent-context/README.md`, relevant `sdlc-spdd/docs/` mirrors, + `CHANGELOG.md`, this canvas and requirement +- Tests: doc mirror checks, command-spec generation, active-reference grep +- Validation: no current operator instruction offers shell engine selection + +### T06 - Full validation, review, retro, and sync + +- Status: Pending +- Description: Run the complete local unit/integration/shell/live-consumer and + documentation validation matrix, perform scope/review checks, capture and + accept lessons, synchronize milestone/roadmap/dependencies, and release the + Work ID. +- Files: review, sync, canvas, requirement, milestone, roadmap, dependency + requirements, registry, lessons, derived context index +- Tests: full local CI runner plus operation diff-scope and lifecycle gates +- Validation: all checks terminal and green; no pending REF-003 lifecycle + artifact + +## N - Norms + +- One approved operation per commit +- Delete duplicate behavior; do not preserve untested safety fallbacks +- Python 3.12 resolution always uses the shared resolver +- Bash may package or host a retained utility, but may not decide lifecycle + semantics +- Current documentation changes with behavior; historical audit evidence stays +- Never contribute Guide work upstream to `embabel/guide` + +## S - Safeguards + +- Exact-path deletion only under framework-owned template/install locations +- Upgrade never deletes unrelated user scripts +- Missing Python fails loudly with setup guidance +- Shell-engine requests fail; they never silently switch behavior +- Retained utility output and persisted records receive focused regression + coverage before twin deletion +- Installed-target smoke proves source-checkout assumptions did not leak +- Full current harness inventory is measured and run, not inferred from stale + requirement prose + +## Test Plan + +- Automated: focused bridge/dispatcher/unit tests, retained utility tests, + installer/integration/live-consumer tests, all current shell harnesses, + ShellCheck, docs/spec generation, and full engine unit/integration suites. +- Terminal smoke: initialize and verify a temporary target; run claim, next, + status, gate, pointer, capture, and accept; confirm retired files are absent + and unsupported engine overrides fail. +- Evidence: save final full-suite and smoke output under + `/opt/cursor/artifacts/`. + +## Review Checklist + +- [ ] T01 bridge is covered for source and installed targets +- [ ] T02 dispatcher has one mandatory engine +- [ ] T02 retained utilities have no twin dependency +- [ ] T03 twin files are absent from source and installs +- [ ] T03 shipped LOC reduction is at least 3,000 +- [ ] T04 current shell/pytest matrix is green +- [ ] T05 current docs state one engine +- [ ] T06 review, retro, sync, and dependency docs are complete +- [ ] Full validation suite is green + +## Sync Notes + +Begins from PR #320 merge commit `5ea5c8f`; all four post-merge `main` +workflows passed before REF-003 coding began. + +## Final Status + +- Readiness: Ready For Coding +- Status: In Progress diff --git a/sdlc-spdd/spdd/memory/registry.jsonl b/sdlc-spdd/spdd/memory/registry.jsonl index a7993810..53a7d4e9 100644 --- a/sdlc-spdd/spdd/memory/registry.jsonl +++ b/sdlc-spdd/spdd/memory/registry.jsonl @@ -46,3 +46,4 @@ {"event": "claim", "work_id": "CHORE-006-dogfood-adapters-and-quick-spec", "status": "active", "phase": "init", "operation": "", "owner": "Cursor Agent", "note": "", "ts": "2026-09-14T04:32:51Z"} {"event": "release", "work_id": "CHORE-006-dogfood-adapters-and-quick-spec", "status": "shelved", "phase": "retro", "operation": "", "owner": "Cursor Agent", "note": "CHORE-006 complete in PR #320; review, retro, and sync recorded", "ts": "2026-09-14T04:38:32Z"} {"event": "refresh", "work_id": "CHORE-006-dogfood-adapters-and-quick-spec", "status": "done", "phase": "retro", "operation": "", "owner": "Cursor Agent", "note": "canvas Final Status: Complete", "ts": "2026-09-14T04:38:32Z"} +{"event": "claim", "work_id": "REF-003-retire-bash-workflow-dual-path", "status": "active", "phase": "init", "operation": "", "owner": "Cursor Agent", "note": "", "ts": "2026-09-14T04:42:43Z"} From 2d47232c44fff89b630d4906a2a08379d934dda4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 14 Sep 2026 04:48:06 +0000 Subject: [PATCH 02/18] refactor: bridge installed shell utilities Co-authored-by: John Menke --- engine/src/sdlc_engine/cli_commands.py | 22 +++++++--- engine/tests_unit/test_cli_shell.py | 60 ++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 7 deletions(-) create mode 100644 engine/tests_unit/test_cli_shell.py diff --git a/engine/src/sdlc_engine/cli_commands.py b/engine/src/sdlc_engine/cli_commands.py index 913889bc..5e1ed23c 100644 --- a/engine/src/sdlc_engine/cli_commands.py +++ b/engine/src/sdlc_engine/cli_commands.py @@ -477,14 +477,22 @@ def cmd_version(_: argparse.Namespace) -> int: def cmd_shell(args: argparse.Namespace) -> int: - """Bridge to remaining v1 shell scripts under scripts/.""" + """Bridge to retained shell utilities in source or installed layouts.""" root = _project(args).root - script = root / "scripts" / args.script - if not script.is_file(): - # also allow bare names that live in scripts/ - candidate = root / "scripts" / f"{args.script}.sh" - script = candidate if candidate.is_file() else script - if not script.is_file(): + script_names = [args.script] + if not args.script.endswith(".sh"): + script_names.append(f"{args.script}.sh") + script_dirs = [root / "scripts", root / "sdlc-spdd" / "scripts"] + script = next( + ( + directory / name + for directory in script_dirs + for name in script_names + if (directory / name).is_file() + ), + None, + ) + if script is None: print(f"shell bridge: script not found: {args.script}", file=sys.stderr) return 1 return subprocess.call([str(script), *args.script_args], cwd=root) diff --git a/engine/tests_unit/test_cli_shell.py b/engine/tests_unit/test_cli_shell.py new file mode 100644 index 00000000..36634cae --- /dev/null +++ b/engine/tests_unit/test_cli_shell.py @@ -0,0 +1,60 @@ +from pathlib import Path + +import pytest + +from sdlc_engine.cli import main + + +def _write_helper(path: Path, label: str) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text( + "#!/bin/sh\n" + f'printf "{label}|%s|%s\\n" "$PWD" "$*"\n', + encoding="utf-8", + ) + path.chmod(0o755) + + +@pytest.mark.parametrize( + ("relative_dir", "script_arg"), + [ + ("scripts", "bridge-helper"), + ("sdlc-spdd/scripts", "bridge-helper.sh"), + ], +) +def test_shell_bridge_runs_source_and_installed_helpers( + tmp_path: Path, + capfd: pytest.CaptureFixture[str], + relative_dir: str, + script_arg: str, +) -> None: + helper = tmp_path / relative_dir / "bridge-helper.sh" + _write_helper(helper, relative_dir) + + assert main(["--root", str(tmp_path), "shell", script_arg, "alpha", "beta"]) == 0 + + output = capfd.readouterr().out.strip() + assert output == f"{relative_dir}|{tmp_path}|alpha beta" + + +def test_shell_bridge_prefers_orchestrator_scripts( + tmp_path: Path, + capfd: pytest.CaptureFixture[str], +) -> None: + _write_helper(tmp_path / "scripts" / "same-name.sh", "source") + _write_helper(tmp_path / "sdlc-spdd" / "scripts" / "same-name.sh", "installed") + + assert main(["--root", str(tmp_path), "shell", "same-name"]) == 0 + + assert capfd.readouterr().out.startswith("source|") + + +def test_shell_bridge_reports_missing_script( + tmp_path: Path, + capsys: pytest.CaptureFixture[str], +) -> None: + assert main(["--root", str(tmp_path), "shell", "not-installed"]) == 1 + assert ( + capsys.readouterr().err.strip() + == "shell bridge: script not found: not-installed" + ) From 2514ae46f3a1c3a10891985da2edcd05c24640a2 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 14 Sep 2026 04:48:21 +0000 Subject: [PATCH 03/18] docs: record REF-003 T01 completion Co-authored-by: John Menke --- .../spdd/canvas/REF-003-retire-bash-workflow-dual-path.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdlc-spdd/spdd/canvas/REF-003-retire-bash-workflow-dual-path.md b/sdlc-spdd/spdd/canvas/REF-003-retire-bash-workflow-dual-path.md index b0aff726..8b8edd25 100644 --- a/sdlc-spdd/spdd/canvas/REF-003-retire-bash-workflow-dual-path.md +++ b/sdlc-spdd/spdd/canvas/REF-003-retire-bash-workflow-dual-path.md @@ -123,7 +123,7 @@ already means the only supported engine. `SDLC_ENGINE=shell` and any ### T01 - Bridge retained shell utilities in source and installed targets -- Status: Pending +- Status: Complete - Description: Extend Python `cmd_shell` resolution to executable utilities in orchestrator `scripts/` or installed `/sdlc-spdd/scripts/`, with stable missing-script, argument-forwarding, and working-directory behavior. @@ -240,7 +240,7 @@ already means the only supported engine. `SDLC_ENGINE=shell` and any ## Review Checklist -- [ ] T01 bridge is covered for source and installed targets +- [x] T01 bridge is covered for source and installed targets - [ ] T02 dispatcher has one mandatory engine - [ ] T02 retained utilities have no twin dependency - [ ] T03 twin files are absent from source and installs From 9d83531d82a0e7e3440f96d3d5407c4b9eb37ca7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 14 Sep 2026 04:54:26 +0000 Subject: [PATCH 04/18] refactor: require Python lifecycle engine Co-authored-by: John Menke --- scripts/accept-lessons.sh | 159 ++++------------ scripts/capture-session-memory.sh | 55 ++++-- scripts/sdlc.sh | 197 ++++++++------------ scripts/start-agent-session.sh | 96 +++++----- sdlc-spdd/scripts/accept-lessons.sh | 159 ++++------------ sdlc-spdd/scripts/capture-session-memory.sh | 55 ++++-- sdlc-spdd/scripts/sdlc.sh | 197 ++++++++------------ sdlc-spdd/scripts/start-agent-session.sh | 96 +++++----- tests/test-sdlc-engine-shim.sh | 65 +++++-- 9 files changed, 448 insertions(+), 631 deletions(-) diff --git a/scripts/accept-lessons.sh b/scripts/accept-lessons.sh index f129b7d1..0d664a3f 100755 --- a/scripts/accept-lessons.sh +++ b/scripts/accept-lessons.sh @@ -6,6 +6,8 @@ _SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "${_SCRIPT_DIR}/lib/common.sh" # shellcheck source=/dev/null source "${_SCRIPT_DIR}/lib/paths.sh" +# shellcheck source=/dev/null +source "${_SCRIPT_DIR}/lib/python.sh" usage() { cat <<'EOF' @@ -57,31 +59,37 @@ while [[ $# -gt 0 ]]; do done TARGET="$(sdlc_resolve_target "${TARGET}")" -export SDLC_ROOT="${TARGET}" - ledger="$(sdlc_ledger "${TARGET}")" stage="$(sdlc_stage "${TARGET}")" +if [[ "$(basename "$(dirname "${_SCRIPT_DIR}")")" == "sdlc-spdd" ]]; then + ENGINE_ROOT="$(cd "${_SCRIPT_DIR}/../.." && pwd)" +else + ENGINE_ROOT="$(cd "${_SCRIPT_DIR}/.." && pwd)" +fi +SDLC_ROOT="${ENGINE_ROOT}" resolve_engine_python || exit 1 +export SDLC_ROOT="${TARGET}" -_python_engine_available() { - if [[ -d "${TARGET}/engine/src/sdlc_engine" ]]; then - PYTHONPATH="${TARGET}/engine/src${PYTHONPATH:+:${PYTHONPATH}}" \ - python3 -c 'import sdlc_engine' 2>/dev/null - return $? +_engine_cli() { + if [[ -d "${ENGINE_ROOT}/engine/src/sdlc_engine" ]]; then + PYTHONPATH="${ENGINE_ROOT}/engine/src${PYTHONPATH:+:${PYTHONPATH}}" \ + "${SDLC_PY}" -m sdlc_engine --root "${TARGET}" "$@" + else + "${SDLC_PY}" -m sdlc_engine --root "${TARGET}" "$@" fi - python3 -c 'import sdlc_engine' 2>/dev/null } -_run_python_accept() { - local -a args=(context accept) - [[ -n "${WORK_ID}" ]] && args+=(--work-id "${WORK_ID}") - [[ -n "${IDS}" ]] && args+=(--ids "${IDS}") - [[ "${DISCARD_REST}" -eq 1 ]] && args+=(--discard-rest) - if [[ -d "${TARGET}/engine/src/sdlc_engine" ]]; then - PYTHONPATH="${TARGET}/engine/src${PYTHONPATH:+:${PYTHONPATH}}" \ - python3 -m sdlc_engine --root "${TARGET}" "${args[@]}" - else - python3 -m sdlc_engine --root "${TARGET}" "${args[@]}" +_do_git_commit() { + local count="${1:-0}" + local commit_id="${WORK_ID:-all}" + if [[ ! -f "${ledger}" ]] || [[ "${count}" -eq 0 ]]; then + echo "Nothing to commit (no records promoted)." + return 0 fi + local rel_ledger + rel_ledger="${ledger#${TARGET}/}" + git -C "${TARGET}" add "${rel_ledger}" + git -C "${TARGET}" commit -m "memory: accept ${count} lessons for ${commit_id}" + echo "Committed ${rel_ledger}" } if [[ "${LIST_ONLY}" -eq 1 ]]; then @@ -89,7 +97,7 @@ if [[ "${LIST_ONLY}" -eq 1 ]]; then echo "No staged records." exit 0 fi - python3 - <&2; exit 1; } - echo "${result}" - accepted_count="$(printf '%s' "${result}" | python3 -c 'import json,sys; d=json.load(sys.stdin) if sys.stdin.readable() else {}; print(d.get("accepted_count",0))' 2>/dev/null || echo 0)" - if [[ "${DO_COMMIT}" -eq 1 ]]; then - _do_git_commit "${accepted_count}" - fi - exit 0 -fi - -# Pure-shell accept fallback -_accept_shell() { - python3 - <&1)" || { echo "${result}" >&2; exit 1; } echo "${result}" -accepted_count="$(printf '%s' "${result}" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("accepted_count",0))')" +accepted_count="$( + printf '%s' "${result}" | + "${SDLC_PY}" -c 'import json,sys; print(json.load(sys.stdin).get("accepted_count", 0))' +)" if [[ "${DO_COMMIT}" -eq 1 ]]; then _do_git_commit "${accepted_count}" fi diff --git a/scripts/capture-session-memory.sh b/scripts/capture-session-memory.sh index 5b5bf2ae..c47234ba 100755 --- a/scripts/capture-session-memory.sh +++ b/scripts/capture-session-memory.sh @@ -12,6 +12,8 @@ source "${_SCRIPT_DIR}/lib/areas.sh" source "${_SCRIPT_DIR}/lib/milestone.sh" # shellcheck source=/dev/null source "${_SCRIPT_DIR}/lib/readiness.sh" +# shellcheck source=/dev/null +source "${_SCRIPT_DIR}/lib/python.sh" usage() { cat <<'EOF' @@ -112,12 +114,6 @@ while [[ $# -gt 0 ]]; do esac done -if [[ -z "${WORK_ID}" ]]; then - echo "Error: --work-id is required" >&2 - usage >&2 - exit 1 -fi - if [[ -n "${SUMMARY_FILE}" ]]; then if [[ "${SUMMARY_FILE}" == "-" ]]; then SUMMARY="$(cat)" @@ -133,6 +129,42 @@ if [[ -z "${SUMMARY}" ]]; then exit 1 fi +TARGET="$(sdlc_resolve_target "${TARGET}")" +HOME="$(sdlc_home "${TARGET}")" +if [[ "$(basename "$(dirname "${_SCRIPT_DIR}")")" == "sdlc-spdd" ]]; then + ENGINE_ROOT="$(cd "${_SCRIPT_DIR}/../.." && pwd)" +else + ENGINE_ROOT="$(cd "${_SCRIPT_DIR}/.." && pwd)" +fi +SDLC_ROOT="${ENGINE_ROOT}" resolve_engine_python || exit 1 +export SDLC_ROOT="${TARGET}" + +_engine_cli() { + if [[ -d "${ENGINE_ROOT}/engine/src/sdlc_engine" ]]; then + PYTHONPATH="${ENGINE_ROOT}/engine/src${PYTHONPATH:+:${PYTHONPATH}}" \ + "${SDLC_PY}" -m sdlc_engine --root "${TARGET}" "$@" + else + "${SDLC_PY}" -m sdlc_engine --root "${TARGET}" "$@" + fi +} + +if [[ -z "${WORK_ID}" ]]; then + WORK_ID="$(_engine_cli pointer get)" +fi +if [[ -z "${WORK_ID}" ]]; then + echo "Error: --work-id is required (or claim/resume an active pointer)" >&2 + usage >&2 + exit 1 +fi +if [[ "${PHASE}" == "resume" ]]; then + status_json="$(_engine_cli status --work-id "${WORK_ID}" --json)" + PHASE="$( + "${SDLC_PY}" -c \ + 'import json,sys; print(json.load(sys.stdin).get("phase", "resume"))' \ + <<<"${status_json}" + )" +fi + # I1 machine artifact: code-phase capture and --complete need command/exit/result. # Title/body --validation prose is not a verify receipt. if [[ "${COMPLETE}" -eq 1 ]]; then @@ -173,9 +205,6 @@ if [[ "${COMPLETE}" -eq 1 && "${VERIFY_RESULT}" != "pass" ]]; then exit 1 fi -TARGET="$(sdlc_resolve_target "${TARGET}")" -export SDLC_ROOT="${TARGET}" -HOME="$(sdlc_home "${TARGET}")" timestamp="$(sdlc_timestamp_iso)" session_day="$(sdlc_timestamp_day)" @@ -484,12 +513,4 @@ if [[ -n "${ROADMAP_NOTE}" ]]; then } >> "${roadmap_file}" fi -workflow_script="${HOME}/scripts/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 "staged ${#staged_records[@]} records → ${stage_file#${TARGET}/}; run 'sdlc.sh accept --work-id ${WORK_ID}' at retro/sync" diff --git a/scripts/sdlc.sh b/scripts/sdlc.sh index 78ec3894..74e28f3d 100755 --- a/scripts/sdlc.sh +++ b/scripts/sdlc.sh @@ -1,40 +1,20 @@ #!/usr/bin/env bash -# Short entry point for SDLC pointer + workflow helpers. +# Short entry point for the mandatory Python SDLC engine. # Installed to sdlc-spdd/scripts/sdlc.sh in target projects (storage v3); # lives at scripts/sdlc.sh in the orchestrator repo. # -# Engine selection (REF-001): -# SDLC_ENGINE=auto Default. Prefer Python when importable (Milestone 2 SUT) -# SDLC_ENGINE=python Require Python engine -# SDLC_ENGINE=shell Legacy bash workflow CLI; gates still use Python when importable -# unless SDLC_GATE_ENGINE=shell +# Install/upgrade and selected session/capture utilities remain shell scripts, +# but workflow, registry, pointer, and gate behavior always comes from Python. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -if [[ -f "${SCRIPT_DIR}/sdlc-workflow.sh" ]]; then - # Storage v3 install / dogfood home: …/sdlc-spdd/scripts/sdlc.sh - # or orchestrator tooling if workflow scripts were co-located. - if [[ "$(basename "$(dirname "${SCRIPT_DIR}")")" == "sdlc-spdd" ]]; then - ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" - else - ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" - fi - WORKFLOW="${SCRIPT_DIR}/sdlc-workflow.sh" -elif [[ -f "${SCRIPT_DIR}/../sdlc-spdd/scripts/sdlc-workflow.sh" ]]; then - # Orchestrator repo: scripts/sdlc.sh with dogfood home under sdlc-spdd/. - ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" - WORKFLOW="${ROOT}/sdlc-spdd/scripts/sdlc-workflow.sh" -elif [[ -f "${SCRIPT_DIR}/../templates/agent-context/sdlc-workflow.sh" ]]; then - # Orchestrator source checkout before dogfood home exists. - ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" - WORKFLOW="${ROOT}/templates/agent-context/sdlc-workflow.sh" +if [[ "$(basename "$(dirname "${SCRIPT_DIR}")")" == "sdlc-spdd" ]]; then + # Installed or dogfood home: /sdlc-spdd/scripts/sdlc.sh + ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" else - ROOT="$(git -C "${PWD}" rev-parse --show-toplevel 2>/dev/null || pwd)" - WORKFLOW="${ROOT}/sdlc-spdd/scripts/sdlc-workflow.sh" - if [[ ! -f "${WORKFLOW}" ]]; then - WORKFLOW="${ROOT}/templates/agent-context/sdlc-workflow.sh" - fi + # Orchestrator source checkout: /scripts/sdlc.sh + ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" fi # Script-derived ROOT finds the orch .venv + engine source. --target/--root @@ -78,7 +58,6 @@ else fi export SDLC_ROOT="${ROOT}" -ENGINE_MODE="${SDLC_ENGINE:-auto}" if [[ -f "${SCRIPT_DIR}/lib/python.sh" ]]; then # shellcheck source=scripts/lib/python.sh @@ -88,8 +67,33 @@ elif [[ -f "${ROOT}/scripts/lib/python.sh" ]]; then source "${ROOT}/scripts/lib/python.sh" fi +case "${SDLC_ENGINE:-python}" in + auto|python) ;; + shell) + echo "sdlc: SDLC_ENGINE=shell is no longer supported; the Python engine is required" >&2 + echo "Install with: ./scripts/setup-engine-venv.sh # Python 3.12" >&2 + exit 2 + ;; + *) + echo "sdlc: unknown SDLC_ENGINE='${SDLC_ENGINE}' (Python is the only supported engine)" >&2 + exit 2 + ;; +esac + +case "${SDLC_GATE_ENGINE:-python}" in + python) ;; + shell) + echo "sdlc: SDLC_GATE_ENGINE=shell is no longer supported; Python gate semantics are required" >&2 + echo "Install with: ./scripts/setup-engine-venv.sh # Python 3.12" >&2 + exit 2 + ;; + *) + echo "sdlc: unknown SDLC_GATE_ENGINE='${SDLC_GATE_ENGINE}' (Python gates are mandatory)" >&2 + exit 2 + ;; +esac + _python_engine_available() { - # python.sh may be absent in hermetic wrapper copies of this script. declare -F resolve_engine_python >/dev/null 2>&1 || return 1 resolve_engine_python || return 1 if [[ -d "${ROOT}/engine/src/sdlc_engine" ]]; then @@ -100,17 +104,6 @@ _python_engine_available() { "${SDLC_PY}" -c 'import sdlc_engine' 2>/dev/null } -# Shell-only verbs. Python has `local capture` / `context accept`, not these. -# Routing them under SDLC_ENGINE=auto would collide with the staging CLI. -_python_engine_handles() { - case "$1" in - capture|complete|start|accept|help|-h|--help) - return 1 - ;; - esac - return 0 -} - _run_python_engine() { local args=("$@") resolve_engine_python || exit 1 @@ -126,93 +119,63 @@ if [[ $# -gt 0 ]]; then shift fi -# Python-engine-only commands that must work even when SDLC_ENGINE=shell. -# Normalize hyphen aliases: local-* → local , db-* → db . -_py_only_args=() +# Normalize compatibility aliases and bridge only the explicitly retained +# session/capture utilities. All lifecycle behavior remains in sdlc_engine. +_engine_args=() case "${cmd}" in - local) - _py_only_args=("local" "$@") - ;; - local-start) _py_only_args=("local" "start" "$@") ;; - local-list) _py_only_args=("local" "list" "$@") ;; - local-status) _py_only_args=("local" "status" "$@") ;; - local-capture) _py_only_args=("local" "capture" "$@") ;; - local-shelf) _py_only_args=("local" "shelf" "$@") ;; - local-resume) _py_only_args=("local" "resume" "$@") ;; - local-promote) _py_only_args=("local" "promote" "$@") ;; - local-abandon) _py_only_args=("local" "abandon" "$@") ;; - quick) - _py_only_args=("quick" "$@") - ;; - db) - _py_only_args=("db" "$@") - ;; - db-rebuild) _py_only_args=("db" "rebuild" "$@") ;; - db-status) _py_only_args=("db" "status" "$@") ;; - db-path) _py_only_args=("db" "path" "$@") ;; - db-query) _py_only_args=("db" "query" "$@") ;; - db-lookup) _py_only_args=("db" "lookup" "$@") ;; - db-export) _py_only_args=("db" "export" "$@") ;; - commit-message) - _py_only_args=("commit-message" "$@") - ;; - sunset) - _py_only_args=("sunset" "$@") - ;; - viewer) - _py_only_args=("viewer" "$@") - ;; - work) - _py_only_args=("work" "$@") - ;; + local-start) _engine_args=("local" "start" "$@") ;; + local-list) _engine_args=("local" "list" "$@") ;; + local-status) _engine_args=("local" "status" "$@") ;; + local-capture) _engine_args=("local" "capture" "$@") ;; + local-shelf) _engine_args=("local" "shelf" "$@") ;; + local-resume) _engine_args=("local" "resume" "$@") ;; + local-promote) _engine_args=("local" "promote" "$@") ;; + local-abandon) _engine_args=("local" "abandon" "$@") ;; + db-rebuild) _engine_args=("db" "rebuild" "$@") ;; + db-status) _engine_args=("db" "status" "$@") ;; + db-path) _engine_args=("db" "path" "$@") ;; + db-query) _engine_args=("db" "query" "$@") ;; + db-lookup) _engine_args=("db" "lookup" "$@") ;; + db-export) _engine_args=("db" "export" "$@") ;; work-init-from-adf|init-from-adf) - _py_only_args=("work" "init-from-adf" "$@") - ;; - context) - _py_only_args=("context" "$@") + _engine_args=("work" "init-from-adf" "$@") ;; guide-query) - _py_only_args=("context" "guide-query" "$@") + _engine_args=("context" "guide-query" "$@") ;; - installer|console|dashboard) - _py_only_args=("${cmd}" "$@") + start|/sdlc-workflow-start) + _engine_args=("shell" "start-agent-session.sh" "--target" "${PROJECT_ROOT}" "$@") ;; -esac -if ((${#_py_only_args[@]} > 0)); then - if ! _python_engine_available; then - echo "sdlc: '${_py_only_args[0]}' requires the Python engine (engine/sdlc_engine)" >&2 - echo "Install with: ./scripts/setup-engine-venv.sh # Python 3.12" >&2 - exit 1 - fi - _run_python_engine "${_py_only_args[@]}" -fi - -case "${ENGINE_MODE}" in - python) - if ! _python_engine_available; then - echo "sdlc: SDLC_ENGINE=python but sdlc_engine is not importable" >&2 - echo "Install with: ./scripts/setup-engine-venv.sh # Python 3.12" >&2 - exit 1 - fi - _run_python_engine "${cmd}" "$@" - ;; - auto) - if _python_engine_handles "${cmd}" && _python_engine_available; then - _run_python_engine "${cmd}" "$@" - fi + capture|/sdlc-workflow-capture) + _engine_args=("shell" "capture-session-memory.sh" "--target" "${PROJECT_ROOT}" "$@") ;; - shell) + complete|/sdlc-workflow-complete) + _engine_args=("shell" "capture-session-memory.sh" "--target" "${PROJECT_ROOT}" "--complete" "$@") ;; - *) - echo "sdlc: unknown SDLC_ENGINE='${ENGINE_MODE}' (use auto|python|shell)" >&2 - exit 2 + accept|/sdlc-accept-lessons) + _engine_args=("shell" "accept-lessons.sh" "--target" "${PROJECT_ROOT}" "$@") ;; + /sdlc-workflow-next) _engine_args=("next" "$@") ;; + /sdlc-workflow-resume) _engine_args=("resume" "$@") ;; + /sdlc-workflow-advance) _engine_args=("advance" "$@") ;; + /sdlc-workflow-skip) _engine_args=("skip" "$@") ;; + /sdlc-workflow-shelf) _engine_args=("shelf" "$@") ;; + /sdlc-workflow-sync) _engine_args=("sync" "$@") ;; + /sdlc-workflow-gate) _engine_args=("gate" "$@") ;; + /sdlc-workflow-list-shelved) _engine_args=("list-shelved" "$@") ;; + /sdlc-team-status) _engine_args=("team" "$@") ;; + /sdlc-list-work) _engine_args=("list-work" "$@") ;; + /sdlc-team-claim) _engine_args=("claim" "$@") ;; + /sdlc-team-release) _engine_args=("release" "$@") ;; + /sdlc-team-archive) _engine_args=("archive" "$@") ;; + help|-h|--help) _engine_args=("--help" "$@") ;; + *) _engine_args=("${cmd}" "$@") ;; esac -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 +if ! _python_engine_available; then + echo "sdlc: the Python engine (sdlc_engine) is required but not importable" >&2 + echo "Install with: ./scripts/setup-engine-venv.sh # Python 3.12" >&2 exit 1 fi -exec "${WORKFLOW}" "${cmd}" "$@" +_run_python_engine "${_engine_args[@]}" diff --git a/scripts/start-agent-session.sh b/scripts/start-agent-session.sh index 0bf4a291..cb3149fe 100755 --- a/scripts/start-agent-session.sh +++ b/scripts/start-agent-session.sh @@ -8,6 +8,8 @@ source "${_SCRIPT_DIR}/lib/common.sh" source "${_SCRIPT_DIR}/lib/paths.sh" # shellcheck source=/dev/null source "${_SCRIPT_DIR}/lib/milestone.sh" +# shellcheck source=/dev/null +source "${_SCRIPT_DIR}/lib/python.sh" usage() { cat <<'EOF' @@ -101,37 +103,51 @@ esac TARGET="$(sdlc_resolve_target "${TARGET}")" export SDLC_ROOT="${TARGET}" HOME="$(sdlc_home "${TARGET}")" +if [[ "$(basename "$(dirname "${_SCRIPT_DIR}")")" == "sdlc-spdd" ]]; then + ENGINE_ROOT="$(cd "${_SCRIPT_DIR}/../.." && pwd)" +else + ENGINE_ROOT="$(cd "${_SCRIPT_DIR}/.." && pwd)" +fi +SDLC_ROOT="${ENGINE_ROOT}" resolve_engine_python || exit 1 +export SDLC_ROOT="${TARGET}" + +_engine_python() { + if [[ -d "${ENGINE_ROOT}/engine/src/sdlc_engine" ]]; then + PYTHONPATH="${ENGINE_ROOT}/engine/src${PYTHONPATH:+:${PYTHONPATH}}" \ + "${SDLC_PY}" "$@" + else + "${SDLC_PY}" "$@" + fi +} -pointer_script="${HOME}/scripts/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 +SDLC_CLI="${_SCRIPT_DIR}/sdlc.sh" +if [[ ! -x "${SDLC_CLI}" ]]; then + echo "sdlc: mandatory Python dispatcher not installed: ${SDLC_CLI}" >&2 + exit 1 fi -workflow_script="${HOME}/scripts/sdlc-workflow.sh" -team_script="${HOME}/scripts/sdlc-team-registry.sh" -workflow_brief_md="Workflow tools not installed." +_engine_cli() { + "${SDLC_CLI}" --target "${TARGET}" "$@" +} + +if [[ -n "${WORK_ID}" ]]; then + _engine_cli pointer set "${WORK_ID}" >/dev/null +else + WORK_ID="$(_engine_cli pointer get)" +fi + +workflow_brief_md="No active Work ID." jira_status="" jira_ask_prompt="" -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}")" -elif [[ -f "${team_script}" && -n "${WORK_ID}" ]]; then - SDLC_ROOT="${TARGET}" - # shellcheck source=/dev/null - source "${team_script}" -fi -if [[ -n "${WORK_ID}" ]] && declare -F sdlc_team_jira_status >/dev/null 2>&1; then - jira_status="$(sdlc_team_jira_status "${WORK_ID}")" -fi -if [[ -n "${WORK_ID}" ]] && declare -F sdlc_team_jira_ask_prompt >/dev/null 2>&1; then - jira_ask_prompt="$(sdlc_team_jira_ask_prompt "${WORK_ID}")" +if [[ -n "${WORK_ID}" ]]; then + status_json="$(_engine_cli status --work-id "${WORK_ID}" --json)" + if [[ "${PHASE}" == "resume" ]]; then + PHASE="$(awk -F '"' '/"phase":/ { print $4; exit }' <<<"${status_json}")" + PHASE="${PHASE:-resume}" + fi + workflow_brief_md="$( + printf '### Python Engine Status\n\n```json\n%s\n```\n' "${status_json}" + )" fi timestamp="$(sdlc_timestamp_iso)" @@ -225,9 +241,6 @@ export SDLC_ROOT="${TARGET}" if [[ "${QUIET}" -eq 1 ]]; then export SDLC_QUIET=1 fi -if [[ "${QUIET}" -eq 0 ]] && declare -F sdlc_workflow_recommended_command >/dev/null 2>&1 && [[ -n "${WORK_ID}" ]]; then - recommended_command="$(sdlc_workflow_recommended_command "${PHASE}" "${WORK_ID}")" -fi if [[ "${QUIET}" -eq 1 ]]; then recommended_command="Quiet mode: retrieve context via SQLite/Guide/context store; no T## dogfood command." fi @@ -302,23 +315,9 @@ sqlite_section_md="" sqlite_lookup_loaded=0 if [[ -n "${WORK_ID}" ]]; then _run_db_lookup() { - local out="" - if [[ -x "${HOME}/scripts/sdlc.sh" ]]; then - out="$( - SDLC_ENGINE=python SDLC_ROOT="${TARGET}" \ - "${HOME}/scripts/sdlc.sh" db lookup \ - --work-id "${WORK_ID}" \ - --markdown 2>/dev/null || true - )" - fi - if [[ -z "${out}" ]] && python3 -c 'import sdlc_engine' 2>/dev/null; then - out="$( - python3 -m sdlc_engine --root "${TARGET}" db lookup \ - --work-id "${WORK_ID}" \ - --markdown 2>/dev/null || true - )" - fi - printf '%s' "${out}" + _engine_cli db lookup \ + --work-id "${WORK_ID}" \ + --markdown 2>/dev/null || true } sqlite_section_md="$(_run_db_lookup)" if [[ -n "${sqlite_section_md}" ]] && grep -Fq 'Local SQLite Index' <<<"${sqlite_section_md}"; then @@ -547,9 +546,8 @@ cp "${session_file}" "${current_file}" # Index hot session into SQLite when engine is available (#85). if [[ -n "${WORK_ID}" ]]; then - python3 -m sdlc_engine --root "${TARGET}" db query \ - --sql "SELECT 1" >/dev/null 2>&1 || true - python3 - </dev/null || true + _engine_cli db query --sql "SELECT 1" >/dev/null 2>&1 || true + _engine_python - </dev/null || true from sdlc_engine.db import LocalIndex from sdlc_engine.project import Project idx = LocalIndex(Project("${TARGET}")) diff --git a/sdlc-spdd/scripts/accept-lessons.sh b/sdlc-spdd/scripts/accept-lessons.sh index f129b7d1..0d664a3f 100755 --- a/sdlc-spdd/scripts/accept-lessons.sh +++ b/sdlc-spdd/scripts/accept-lessons.sh @@ -6,6 +6,8 @@ _SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "${_SCRIPT_DIR}/lib/common.sh" # shellcheck source=/dev/null source "${_SCRIPT_DIR}/lib/paths.sh" +# shellcheck source=/dev/null +source "${_SCRIPT_DIR}/lib/python.sh" usage() { cat <<'EOF' @@ -57,31 +59,37 @@ while [[ $# -gt 0 ]]; do done TARGET="$(sdlc_resolve_target "${TARGET}")" -export SDLC_ROOT="${TARGET}" - ledger="$(sdlc_ledger "${TARGET}")" stage="$(sdlc_stage "${TARGET}")" +if [[ "$(basename "$(dirname "${_SCRIPT_DIR}")")" == "sdlc-spdd" ]]; then + ENGINE_ROOT="$(cd "${_SCRIPT_DIR}/../.." && pwd)" +else + ENGINE_ROOT="$(cd "${_SCRIPT_DIR}/.." && pwd)" +fi +SDLC_ROOT="${ENGINE_ROOT}" resolve_engine_python || exit 1 +export SDLC_ROOT="${TARGET}" -_python_engine_available() { - if [[ -d "${TARGET}/engine/src/sdlc_engine" ]]; then - PYTHONPATH="${TARGET}/engine/src${PYTHONPATH:+:${PYTHONPATH}}" \ - python3 -c 'import sdlc_engine' 2>/dev/null - return $? +_engine_cli() { + if [[ -d "${ENGINE_ROOT}/engine/src/sdlc_engine" ]]; then + PYTHONPATH="${ENGINE_ROOT}/engine/src${PYTHONPATH:+:${PYTHONPATH}}" \ + "${SDLC_PY}" -m sdlc_engine --root "${TARGET}" "$@" + else + "${SDLC_PY}" -m sdlc_engine --root "${TARGET}" "$@" fi - python3 -c 'import sdlc_engine' 2>/dev/null } -_run_python_accept() { - local -a args=(context accept) - [[ -n "${WORK_ID}" ]] && args+=(--work-id "${WORK_ID}") - [[ -n "${IDS}" ]] && args+=(--ids "${IDS}") - [[ "${DISCARD_REST}" -eq 1 ]] && args+=(--discard-rest) - if [[ -d "${TARGET}/engine/src/sdlc_engine" ]]; then - PYTHONPATH="${TARGET}/engine/src${PYTHONPATH:+:${PYTHONPATH}}" \ - python3 -m sdlc_engine --root "${TARGET}" "${args[@]}" - else - python3 -m sdlc_engine --root "${TARGET}" "${args[@]}" +_do_git_commit() { + local count="${1:-0}" + local commit_id="${WORK_ID:-all}" + if [[ ! -f "${ledger}" ]] || [[ "${count}" -eq 0 ]]; then + echo "Nothing to commit (no records promoted)." + return 0 fi + local rel_ledger + rel_ledger="${ledger#${TARGET}/}" + git -C "${TARGET}" add "${rel_ledger}" + git -C "${TARGET}" commit -m "memory: accept ${count} lessons for ${commit_id}" + echo "Committed ${rel_ledger}" } if [[ "${LIST_ONLY}" -eq 1 ]]; then @@ -89,7 +97,7 @@ if [[ "${LIST_ONLY}" -eq 1 ]]; then echo "No staged records." exit 0 fi - python3 - <&2; exit 1; } - echo "${result}" - accepted_count="$(printf '%s' "${result}" | python3 -c 'import json,sys; d=json.load(sys.stdin) if sys.stdin.readable() else {}; print(d.get("accepted_count",0))' 2>/dev/null || echo 0)" - if [[ "${DO_COMMIT}" -eq 1 ]]; then - _do_git_commit "${accepted_count}" - fi - exit 0 -fi - -# Pure-shell accept fallback -_accept_shell() { - python3 - <&1)" || { echo "${result}" >&2; exit 1; } echo "${result}" -accepted_count="$(printf '%s' "${result}" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("accepted_count",0))')" +accepted_count="$( + printf '%s' "${result}" | + "${SDLC_PY}" -c 'import json,sys; print(json.load(sys.stdin).get("accepted_count", 0))' +)" if [[ "${DO_COMMIT}" -eq 1 ]]; then _do_git_commit "${accepted_count}" fi diff --git a/sdlc-spdd/scripts/capture-session-memory.sh b/sdlc-spdd/scripts/capture-session-memory.sh index 5b5bf2ae..c47234ba 100755 --- a/sdlc-spdd/scripts/capture-session-memory.sh +++ b/sdlc-spdd/scripts/capture-session-memory.sh @@ -12,6 +12,8 @@ source "${_SCRIPT_DIR}/lib/areas.sh" source "${_SCRIPT_DIR}/lib/milestone.sh" # shellcheck source=/dev/null source "${_SCRIPT_DIR}/lib/readiness.sh" +# shellcheck source=/dev/null +source "${_SCRIPT_DIR}/lib/python.sh" usage() { cat <<'EOF' @@ -112,12 +114,6 @@ while [[ $# -gt 0 ]]; do esac done -if [[ -z "${WORK_ID}" ]]; then - echo "Error: --work-id is required" >&2 - usage >&2 - exit 1 -fi - if [[ -n "${SUMMARY_FILE}" ]]; then if [[ "${SUMMARY_FILE}" == "-" ]]; then SUMMARY="$(cat)" @@ -133,6 +129,42 @@ if [[ -z "${SUMMARY}" ]]; then exit 1 fi +TARGET="$(sdlc_resolve_target "${TARGET}")" +HOME="$(sdlc_home "${TARGET}")" +if [[ "$(basename "$(dirname "${_SCRIPT_DIR}")")" == "sdlc-spdd" ]]; then + ENGINE_ROOT="$(cd "${_SCRIPT_DIR}/../.." && pwd)" +else + ENGINE_ROOT="$(cd "${_SCRIPT_DIR}/.." && pwd)" +fi +SDLC_ROOT="${ENGINE_ROOT}" resolve_engine_python || exit 1 +export SDLC_ROOT="${TARGET}" + +_engine_cli() { + if [[ -d "${ENGINE_ROOT}/engine/src/sdlc_engine" ]]; then + PYTHONPATH="${ENGINE_ROOT}/engine/src${PYTHONPATH:+:${PYTHONPATH}}" \ + "${SDLC_PY}" -m sdlc_engine --root "${TARGET}" "$@" + else + "${SDLC_PY}" -m sdlc_engine --root "${TARGET}" "$@" + fi +} + +if [[ -z "${WORK_ID}" ]]; then + WORK_ID="$(_engine_cli pointer get)" +fi +if [[ -z "${WORK_ID}" ]]; then + echo "Error: --work-id is required (or claim/resume an active pointer)" >&2 + usage >&2 + exit 1 +fi +if [[ "${PHASE}" == "resume" ]]; then + status_json="$(_engine_cli status --work-id "${WORK_ID}" --json)" + PHASE="$( + "${SDLC_PY}" -c \ + 'import json,sys; print(json.load(sys.stdin).get("phase", "resume"))' \ + <<<"${status_json}" + )" +fi + # I1 machine artifact: code-phase capture and --complete need command/exit/result. # Title/body --validation prose is not a verify receipt. if [[ "${COMPLETE}" -eq 1 ]]; then @@ -173,9 +205,6 @@ if [[ "${COMPLETE}" -eq 1 && "${VERIFY_RESULT}" != "pass" ]]; then exit 1 fi -TARGET="$(sdlc_resolve_target "${TARGET}")" -export SDLC_ROOT="${TARGET}" -HOME="$(sdlc_home "${TARGET}")" timestamp="$(sdlc_timestamp_iso)" session_day="$(sdlc_timestamp_day)" @@ -484,12 +513,4 @@ if [[ -n "${ROADMAP_NOTE}" ]]; then } >> "${roadmap_file}" fi -workflow_script="${HOME}/scripts/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 "staged ${#staged_records[@]} records → ${stage_file#${TARGET}/}; run 'sdlc.sh accept --work-id ${WORK_ID}' at retro/sync" diff --git a/sdlc-spdd/scripts/sdlc.sh b/sdlc-spdd/scripts/sdlc.sh index 78ec3894..74e28f3d 100755 --- a/sdlc-spdd/scripts/sdlc.sh +++ b/sdlc-spdd/scripts/sdlc.sh @@ -1,40 +1,20 @@ #!/usr/bin/env bash -# Short entry point for SDLC pointer + workflow helpers. +# Short entry point for the mandatory Python SDLC engine. # Installed to sdlc-spdd/scripts/sdlc.sh in target projects (storage v3); # lives at scripts/sdlc.sh in the orchestrator repo. # -# Engine selection (REF-001): -# SDLC_ENGINE=auto Default. Prefer Python when importable (Milestone 2 SUT) -# SDLC_ENGINE=python Require Python engine -# SDLC_ENGINE=shell Legacy bash workflow CLI; gates still use Python when importable -# unless SDLC_GATE_ENGINE=shell +# Install/upgrade and selected session/capture utilities remain shell scripts, +# but workflow, registry, pointer, and gate behavior always comes from Python. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -if [[ -f "${SCRIPT_DIR}/sdlc-workflow.sh" ]]; then - # Storage v3 install / dogfood home: …/sdlc-spdd/scripts/sdlc.sh - # or orchestrator tooling if workflow scripts were co-located. - if [[ "$(basename "$(dirname "${SCRIPT_DIR}")")" == "sdlc-spdd" ]]; then - ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" - else - ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" - fi - WORKFLOW="${SCRIPT_DIR}/sdlc-workflow.sh" -elif [[ -f "${SCRIPT_DIR}/../sdlc-spdd/scripts/sdlc-workflow.sh" ]]; then - # Orchestrator repo: scripts/sdlc.sh with dogfood home under sdlc-spdd/. - ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" - WORKFLOW="${ROOT}/sdlc-spdd/scripts/sdlc-workflow.sh" -elif [[ -f "${SCRIPT_DIR}/../templates/agent-context/sdlc-workflow.sh" ]]; then - # Orchestrator source checkout before dogfood home exists. - ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" - WORKFLOW="${ROOT}/templates/agent-context/sdlc-workflow.sh" +if [[ "$(basename "$(dirname "${SCRIPT_DIR}")")" == "sdlc-spdd" ]]; then + # Installed or dogfood home: /sdlc-spdd/scripts/sdlc.sh + ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" else - ROOT="$(git -C "${PWD}" rev-parse --show-toplevel 2>/dev/null || pwd)" - WORKFLOW="${ROOT}/sdlc-spdd/scripts/sdlc-workflow.sh" - if [[ ! -f "${WORKFLOW}" ]]; then - WORKFLOW="${ROOT}/templates/agent-context/sdlc-workflow.sh" - fi + # Orchestrator source checkout: /scripts/sdlc.sh + ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" fi # Script-derived ROOT finds the orch .venv + engine source. --target/--root @@ -78,7 +58,6 @@ else fi export SDLC_ROOT="${ROOT}" -ENGINE_MODE="${SDLC_ENGINE:-auto}" if [[ -f "${SCRIPT_DIR}/lib/python.sh" ]]; then # shellcheck source=scripts/lib/python.sh @@ -88,8 +67,33 @@ elif [[ -f "${ROOT}/scripts/lib/python.sh" ]]; then source "${ROOT}/scripts/lib/python.sh" fi +case "${SDLC_ENGINE:-python}" in + auto|python) ;; + shell) + echo "sdlc: SDLC_ENGINE=shell is no longer supported; the Python engine is required" >&2 + echo "Install with: ./scripts/setup-engine-venv.sh # Python 3.12" >&2 + exit 2 + ;; + *) + echo "sdlc: unknown SDLC_ENGINE='${SDLC_ENGINE}' (Python is the only supported engine)" >&2 + exit 2 + ;; +esac + +case "${SDLC_GATE_ENGINE:-python}" in + python) ;; + shell) + echo "sdlc: SDLC_GATE_ENGINE=shell is no longer supported; Python gate semantics are required" >&2 + echo "Install with: ./scripts/setup-engine-venv.sh # Python 3.12" >&2 + exit 2 + ;; + *) + echo "sdlc: unknown SDLC_GATE_ENGINE='${SDLC_GATE_ENGINE}' (Python gates are mandatory)" >&2 + exit 2 + ;; +esac + _python_engine_available() { - # python.sh may be absent in hermetic wrapper copies of this script. declare -F resolve_engine_python >/dev/null 2>&1 || return 1 resolve_engine_python || return 1 if [[ -d "${ROOT}/engine/src/sdlc_engine" ]]; then @@ -100,17 +104,6 @@ _python_engine_available() { "${SDLC_PY}" -c 'import sdlc_engine' 2>/dev/null } -# Shell-only verbs. Python has `local capture` / `context accept`, not these. -# Routing them under SDLC_ENGINE=auto would collide with the staging CLI. -_python_engine_handles() { - case "$1" in - capture|complete|start|accept|help|-h|--help) - return 1 - ;; - esac - return 0 -} - _run_python_engine() { local args=("$@") resolve_engine_python || exit 1 @@ -126,93 +119,63 @@ if [[ $# -gt 0 ]]; then shift fi -# Python-engine-only commands that must work even when SDLC_ENGINE=shell. -# Normalize hyphen aliases: local-* → local , db-* → db . -_py_only_args=() +# Normalize compatibility aliases and bridge only the explicitly retained +# session/capture utilities. All lifecycle behavior remains in sdlc_engine. +_engine_args=() case "${cmd}" in - local) - _py_only_args=("local" "$@") - ;; - local-start) _py_only_args=("local" "start" "$@") ;; - local-list) _py_only_args=("local" "list" "$@") ;; - local-status) _py_only_args=("local" "status" "$@") ;; - local-capture) _py_only_args=("local" "capture" "$@") ;; - local-shelf) _py_only_args=("local" "shelf" "$@") ;; - local-resume) _py_only_args=("local" "resume" "$@") ;; - local-promote) _py_only_args=("local" "promote" "$@") ;; - local-abandon) _py_only_args=("local" "abandon" "$@") ;; - quick) - _py_only_args=("quick" "$@") - ;; - db) - _py_only_args=("db" "$@") - ;; - db-rebuild) _py_only_args=("db" "rebuild" "$@") ;; - db-status) _py_only_args=("db" "status" "$@") ;; - db-path) _py_only_args=("db" "path" "$@") ;; - db-query) _py_only_args=("db" "query" "$@") ;; - db-lookup) _py_only_args=("db" "lookup" "$@") ;; - db-export) _py_only_args=("db" "export" "$@") ;; - commit-message) - _py_only_args=("commit-message" "$@") - ;; - sunset) - _py_only_args=("sunset" "$@") - ;; - viewer) - _py_only_args=("viewer" "$@") - ;; - work) - _py_only_args=("work" "$@") - ;; + local-start) _engine_args=("local" "start" "$@") ;; + local-list) _engine_args=("local" "list" "$@") ;; + local-status) _engine_args=("local" "status" "$@") ;; + local-capture) _engine_args=("local" "capture" "$@") ;; + local-shelf) _engine_args=("local" "shelf" "$@") ;; + local-resume) _engine_args=("local" "resume" "$@") ;; + local-promote) _engine_args=("local" "promote" "$@") ;; + local-abandon) _engine_args=("local" "abandon" "$@") ;; + db-rebuild) _engine_args=("db" "rebuild" "$@") ;; + db-status) _engine_args=("db" "status" "$@") ;; + db-path) _engine_args=("db" "path" "$@") ;; + db-query) _engine_args=("db" "query" "$@") ;; + db-lookup) _engine_args=("db" "lookup" "$@") ;; + db-export) _engine_args=("db" "export" "$@") ;; work-init-from-adf|init-from-adf) - _py_only_args=("work" "init-from-adf" "$@") - ;; - context) - _py_only_args=("context" "$@") + _engine_args=("work" "init-from-adf" "$@") ;; guide-query) - _py_only_args=("context" "guide-query" "$@") + _engine_args=("context" "guide-query" "$@") ;; - installer|console|dashboard) - _py_only_args=("${cmd}" "$@") + start|/sdlc-workflow-start) + _engine_args=("shell" "start-agent-session.sh" "--target" "${PROJECT_ROOT}" "$@") ;; -esac -if ((${#_py_only_args[@]} > 0)); then - if ! _python_engine_available; then - echo "sdlc: '${_py_only_args[0]}' requires the Python engine (engine/sdlc_engine)" >&2 - echo "Install with: ./scripts/setup-engine-venv.sh # Python 3.12" >&2 - exit 1 - fi - _run_python_engine "${_py_only_args[@]}" -fi - -case "${ENGINE_MODE}" in - python) - if ! _python_engine_available; then - echo "sdlc: SDLC_ENGINE=python but sdlc_engine is not importable" >&2 - echo "Install with: ./scripts/setup-engine-venv.sh # Python 3.12" >&2 - exit 1 - fi - _run_python_engine "${cmd}" "$@" - ;; - auto) - if _python_engine_handles "${cmd}" && _python_engine_available; then - _run_python_engine "${cmd}" "$@" - fi + capture|/sdlc-workflow-capture) + _engine_args=("shell" "capture-session-memory.sh" "--target" "${PROJECT_ROOT}" "$@") ;; - shell) + complete|/sdlc-workflow-complete) + _engine_args=("shell" "capture-session-memory.sh" "--target" "${PROJECT_ROOT}" "--complete" "$@") ;; - *) - echo "sdlc: unknown SDLC_ENGINE='${ENGINE_MODE}' (use auto|python|shell)" >&2 - exit 2 + accept|/sdlc-accept-lessons) + _engine_args=("shell" "accept-lessons.sh" "--target" "${PROJECT_ROOT}" "$@") ;; + /sdlc-workflow-next) _engine_args=("next" "$@") ;; + /sdlc-workflow-resume) _engine_args=("resume" "$@") ;; + /sdlc-workflow-advance) _engine_args=("advance" "$@") ;; + /sdlc-workflow-skip) _engine_args=("skip" "$@") ;; + /sdlc-workflow-shelf) _engine_args=("shelf" "$@") ;; + /sdlc-workflow-sync) _engine_args=("sync" "$@") ;; + /sdlc-workflow-gate) _engine_args=("gate" "$@") ;; + /sdlc-workflow-list-shelved) _engine_args=("list-shelved" "$@") ;; + /sdlc-team-status) _engine_args=("team" "$@") ;; + /sdlc-list-work) _engine_args=("list-work" "$@") ;; + /sdlc-team-claim) _engine_args=("claim" "$@") ;; + /sdlc-team-release) _engine_args=("release" "$@") ;; + /sdlc-team-archive) _engine_args=("archive" "$@") ;; + help|-h|--help) _engine_args=("--help" "$@") ;; + *) _engine_args=("${cmd}" "$@") ;; esac -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 +if ! _python_engine_available; then + echo "sdlc: the Python engine (sdlc_engine) is required but not importable" >&2 + echo "Install with: ./scripts/setup-engine-venv.sh # Python 3.12" >&2 exit 1 fi -exec "${WORKFLOW}" "${cmd}" "$@" +_run_python_engine "${_engine_args[@]}" diff --git a/sdlc-spdd/scripts/start-agent-session.sh b/sdlc-spdd/scripts/start-agent-session.sh index 0bf4a291..cb3149fe 100755 --- a/sdlc-spdd/scripts/start-agent-session.sh +++ b/sdlc-spdd/scripts/start-agent-session.sh @@ -8,6 +8,8 @@ source "${_SCRIPT_DIR}/lib/common.sh" source "${_SCRIPT_DIR}/lib/paths.sh" # shellcheck source=/dev/null source "${_SCRIPT_DIR}/lib/milestone.sh" +# shellcheck source=/dev/null +source "${_SCRIPT_DIR}/lib/python.sh" usage() { cat <<'EOF' @@ -101,37 +103,51 @@ esac TARGET="$(sdlc_resolve_target "${TARGET}")" export SDLC_ROOT="${TARGET}" HOME="$(sdlc_home "${TARGET}")" +if [[ "$(basename "$(dirname "${_SCRIPT_DIR}")")" == "sdlc-spdd" ]]; then + ENGINE_ROOT="$(cd "${_SCRIPT_DIR}/../.." && pwd)" +else + ENGINE_ROOT="$(cd "${_SCRIPT_DIR}/.." && pwd)" +fi +SDLC_ROOT="${ENGINE_ROOT}" resolve_engine_python || exit 1 +export SDLC_ROOT="${TARGET}" + +_engine_python() { + if [[ -d "${ENGINE_ROOT}/engine/src/sdlc_engine" ]]; then + PYTHONPATH="${ENGINE_ROOT}/engine/src${PYTHONPATH:+:${PYTHONPATH}}" \ + "${SDLC_PY}" "$@" + else + "${SDLC_PY}" "$@" + fi +} -pointer_script="${HOME}/scripts/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 +SDLC_CLI="${_SCRIPT_DIR}/sdlc.sh" +if [[ ! -x "${SDLC_CLI}" ]]; then + echo "sdlc: mandatory Python dispatcher not installed: ${SDLC_CLI}" >&2 + exit 1 fi -workflow_script="${HOME}/scripts/sdlc-workflow.sh" -team_script="${HOME}/scripts/sdlc-team-registry.sh" -workflow_brief_md="Workflow tools not installed." +_engine_cli() { + "${SDLC_CLI}" --target "${TARGET}" "$@" +} + +if [[ -n "${WORK_ID}" ]]; then + _engine_cli pointer set "${WORK_ID}" >/dev/null +else + WORK_ID="$(_engine_cli pointer get)" +fi + +workflow_brief_md="No active Work ID." jira_status="" jira_ask_prompt="" -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}")" -elif [[ -f "${team_script}" && -n "${WORK_ID}" ]]; then - SDLC_ROOT="${TARGET}" - # shellcheck source=/dev/null - source "${team_script}" -fi -if [[ -n "${WORK_ID}" ]] && declare -F sdlc_team_jira_status >/dev/null 2>&1; then - jira_status="$(sdlc_team_jira_status "${WORK_ID}")" -fi -if [[ -n "${WORK_ID}" ]] && declare -F sdlc_team_jira_ask_prompt >/dev/null 2>&1; then - jira_ask_prompt="$(sdlc_team_jira_ask_prompt "${WORK_ID}")" +if [[ -n "${WORK_ID}" ]]; then + status_json="$(_engine_cli status --work-id "${WORK_ID}" --json)" + if [[ "${PHASE}" == "resume" ]]; then + PHASE="$(awk -F '"' '/"phase":/ { print $4; exit }' <<<"${status_json}")" + PHASE="${PHASE:-resume}" + fi + workflow_brief_md="$( + printf '### Python Engine Status\n\n```json\n%s\n```\n' "${status_json}" + )" fi timestamp="$(sdlc_timestamp_iso)" @@ -225,9 +241,6 @@ export SDLC_ROOT="${TARGET}" if [[ "${QUIET}" -eq 1 ]]; then export SDLC_QUIET=1 fi -if [[ "${QUIET}" -eq 0 ]] && declare -F sdlc_workflow_recommended_command >/dev/null 2>&1 && [[ -n "${WORK_ID}" ]]; then - recommended_command="$(sdlc_workflow_recommended_command "${PHASE}" "${WORK_ID}")" -fi if [[ "${QUIET}" -eq 1 ]]; then recommended_command="Quiet mode: retrieve context via SQLite/Guide/context store; no T## dogfood command." fi @@ -302,23 +315,9 @@ sqlite_section_md="" sqlite_lookup_loaded=0 if [[ -n "${WORK_ID}" ]]; then _run_db_lookup() { - local out="" - if [[ -x "${HOME}/scripts/sdlc.sh" ]]; then - out="$( - SDLC_ENGINE=python SDLC_ROOT="${TARGET}" \ - "${HOME}/scripts/sdlc.sh" db lookup \ - --work-id "${WORK_ID}" \ - --markdown 2>/dev/null || true - )" - fi - if [[ -z "${out}" ]] && python3 -c 'import sdlc_engine' 2>/dev/null; then - out="$( - python3 -m sdlc_engine --root "${TARGET}" db lookup \ - --work-id "${WORK_ID}" \ - --markdown 2>/dev/null || true - )" - fi - printf '%s' "${out}" + _engine_cli db lookup \ + --work-id "${WORK_ID}" \ + --markdown 2>/dev/null || true } sqlite_section_md="$(_run_db_lookup)" if [[ -n "${sqlite_section_md}" ]] && grep -Fq 'Local SQLite Index' <<<"${sqlite_section_md}"; then @@ -547,9 +546,8 @@ cp "${session_file}" "${current_file}" # Index hot session into SQLite when engine is available (#85). if [[ -n "${WORK_ID}" ]]; then - python3 -m sdlc_engine --root "${TARGET}" db query \ - --sql "SELECT 1" >/dev/null 2>&1 || true - python3 - </dev/null || true + _engine_cli db query --sql "SELECT 1" >/dev/null 2>&1 || true + _engine_python - </dev/null || true from sdlc_engine.db import LocalIndex from sdlc_engine.project import Project idx = LocalIndex(Project("${TARGET}")) diff --git a/tests/test-sdlc-engine-shim.sh b/tests/test-sdlc-engine-shim.sh index 6a693087..20febfcf 100755 --- a/tests/test-sdlc-engine-shim.sh +++ b/tests/test-sdlc-engine-shim.sh @@ -30,28 +30,59 @@ else bad "python next output unexpected" fi -echo "== default SDLC_ENGINE=auto uses python when importable ==" +echo "== default dispatcher uses mandatory Python engine ==" out="$( "${REPO_ROOT}/scripts/sdlc.sh" version )" if [[ "${out}" == 2.0.0a* ]]; then - ok "default auto routes version to python engine (${out})" + ok "default routes version to Python engine (${out})" else - bad "default auto unexpected version: ${out}" + bad "default Python engine unexpected version: ${out}" fi -echo "== explicit SDLC_ENGINE=shell still works ==" -out="$(SDLC_ENGINE=shell "${REPO_ROOT}/scripts/sdlc.sh" next)" -if grep -Fq 'No active Work ID' <<< "${out}" || grep -Fq 'SDLC:' <<< "${out}" || grep -Fq 'resume' <<< "${out}"; then - ok "shell engine still works" +echo "== retired shell engine overrides fail clearly ==" +if out="$(SDLC_ENGINE=shell "${REPO_ROOT}/scripts/sdlc.sh" next 2>&1)"; then + bad "SDLC_ENGINE=shell should fail" +elif grep -Fq 'SDLC_ENGINE=shell is no longer supported' <<< "${out}"; then + ok "SDLC_ENGINE=shell rejected" else - bad "shell next unexpected" + bad "SDLC_ENGINE=shell rejection unclear: ${out}" +fi +if out="$(SDLC_GATE_ENGINE=shell "${REPO_ROOT}/scripts/sdlc.sh" next 2>&1)"; then + bad "SDLC_GATE_ENGINE=shell should fail" +elif grep -Fq 'SDLC_GATE_ENGINE=shell is no longer supported' <<< "${out}"; then + ok "SDLC_GATE_ENGINE=shell rejected" +else + bad "SDLC_GATE_ENGINE=shell rejection unclear: ${out}" fi -echo "== local sessions route even when SDLC_ENGINE=shell ==" tmp="$(mktemp -d)" trap 'rm -rf "${tmp}"' EXIT -# Use --root via python engine; sdlc.sh local* always hits python. + +echo "== missing Python engine fails with install hint ==" +no_engine="${tmp}/no-engine" +mkdir -p "${no_engine}/scripts/lib" +cp "${REPO_ROOT}/scripts/sdlc.sh" "${no_engine}/scripts/sdlc.sh" +cp "${REPO_ROOT}/scripts/lib/python.sh" "${no_engine}/scripts/lib/python.sh" +cat > "${no_engine}/fake-python" <<'EOF' +#!/usr/bin/env bash +if [[ "${1:-}" == "-c" && "${2:-}" == *"sys.version_info"* ]]; then + echo "3 12" + exit 0 +fi +exit 1 +EOF +chmod +x "${no_engine}/scripts/sdlc.sh" "${no_engine}/fake-python" +if out="$(PYTHON="${no_engine}/fake-python" "${no_engine}/scripts/sdlc.sh" next 2>&1)"; then + bad "missing sdlc_engine should fail" +elif grep -Fq 'Python engine (sdlc_engine) is required' <<< "${out}" \ + && grep -Fq 'setup-engine-venv.sh' <<< "${out}"; then + ok "missing engine reports Python 3.12 setup hint" +else + bad "missing engine hint unclear: ${out}" +fi + +echo "== local sessions use Python engine ==" out="$( - SDLC_ENGINE=shell SDLC_USER=shim-test \ + SDLC_USER=shim-test \ PYTHONPATH="${REPO_ROOT}/engine/src" \ python3 -m sdlc_engine --root "${tmp}" local start --name shim-local --intent "offline" )" @@ -111,8 +142,7 @@ while IFS=$'\t' read -r alias_line wid; do # shellcheck disable=SC2086 # intentional word-splitting of alias tokens set -- ${alias_line} out="$( - SDLC_ENGINE=shell \ - PYTHONPATH="${REPO_ROOT}/engine/src" \ + PYTHONPATH="${REPO_ROOT}/engine/src" \ "${REPO_ROOT}/scripts/sdlc.sh" "$@" \ --path "${tmp}/adf/ORCH-8.adf.json" \ --work-id "${wid}" \ @@ -144,8 +174,7 @@ if (( alias_ok == 1 && alias_n == 3 )); then fi help_out="$( - SDLC_ENGINE=shell \ - PYTHONPATH="${REPO_ROOT}/engine/src" \ + PYTHONPATH="${REPO_ROOT}/engine/src" \ "${REPO_ROOT}/scripts/sdlc.sh" work init-from-adf --help 2>&1 )" if grep -Fq -- '--path' <<< "${help_out}"; then @@ -172,8 +201,7 @@ cat > "${tmp}/sdlc-spdd/spdd/canvas/FEAT-000-shim.md" <<'EOF' EOF cp "${tmp}/sdlc-spdd/spdd/canvas/FEAT-000-shim.md" "${tmp}/sdlc-spdd/requirements/milestones/FEAT-000-shim.md" out="$( - SDLC_ENGINE=shell \ - PYTHONPATH="${REPO_ROOT}/engine/src" \ + PYTHONPATH="${REPO_ROOT}/engine/src" \ python3 -m sdlc_engine --root "${tmp}" db rebuild )" if grep -Fq 'Rebuilt SQLite index' <<< "${out}" && [[ -f "${tmp}/sdlc-spdd/.sdlc/index.sqlite" ]]; then @@ -185,8 +213,7 @@ fi echo "== sdlc.sh --target db rebuild (not next / no pointer) ==" rm -f "${tmp}/sdlc-spdd/.sdlc/index.sqlite" out="$( - SDLC_ENGINE=shell \ - PYTHONPATH="${REPO_ROOT}/engine/src" \ + PYTHONPATH="${REPO_ROOT}/engine/src" \ "${REPO_ROOT}/scripts/sdlc.sh" db rebuild --target "${tmp}" )" if grep -Fq 'Rebuilt SQLite index' <<< "${out}" && [[ -f "${tmp}/sdlc-spdd/.sdlc/index.sqlite" ]]; then From 93f86418c018ee6212f85ee64bdf7c6709787684 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 14 Sep 2026 04:55:04 +0000 Subject: [PATCH 05/18] test: run receipt checks on Python dispatcher Co-authored-by: John Menke --- tests/test-verify-receipt.sh | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/tests/test-verify-receipt.sh b/tests/test-verify-receipt.sh index 507d43d0..8e33ebc5 100755 --- a/tests/test-verify-receipt.sh +++ b/tests/test-verify-receipt.sh @@ -4,14 +4,8 @@ set -euo pipefail # Leftover #6 proving test: I1 capture/complete without a verify receipt refuse. # LessonRecord title/body (or --validation prose) is not a receipt. -export SDLC_GATE_ENGINE=shell -export SDLC_ENGINE=shell - SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" -WORKFLOW="${REPO_ROOT}/templates/agent-context/sdlc-workflow.sh" -POINTER="${REPO_ROOT}/templates/agent-context/sdlc-pointer.sh" -TEAM_REG="${REPO_ROOT}/templates/agent-context/sdlc-team-registry.sh" CAPTURE="${REPO_ROOT}/scripts/capture-session-memory.sh" SDLC_SH="${REPO_ROOT}/scripts/sdlc.sh" @@ -31,9 +25,6 @@ setup_feature() { "${t}/sdlc-spdd/spdd/analysis" \ "${t}/sdlc-spdd/spdd/memory" \ "${t}/sdlc-spdd/scripts/lib" - cp "${POINTER}" "${t}/sdlc-spdd/scripts/sdlc-pointer.sh" - cp "${WORKFLOW}" "${t}/sdlc-spdd/scripts/sdlc-workflow.sh" - cp "${TEAM_REG}" "${t}/sdlc-spdd/scripts/sdlc-team-registry.sh" : > "${t}/sdlc-spdd/spdd/memory/registry.jsonl" cp "${SDLC_SH}" "${t}/sdlc-spdd/scripts/sdlc.sh" cp "${CAPTURE}" "${t}/sdlc-spdd/scripts/capture-session-memory.sh" @@ -46,7 +37,10 @@ setup_feature() { sdlc() { local t="$1" shift - SDLC_ROOT="${t}" SDLC_ENGINE=shell "${t}/sdlc-spdd/scripts/sdlc.sh" "$@" + SDLC_ROOT="${t}" \ + PYTHON="${REPO_ROOT}/.venv/bin/python" \ + PYTHONPATH="${REPO_ROOT}/engine/src" \ + "${t}/sdlc-spdd/scripts/sdlc.sh" "$@" } echo "== test_capture_without_verify_receipt_refuses ==" From 32cafbcd180b8814566664b97dea999dbf9e8830 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 14 Sep 2026 04:55:24 +0000 Subject: [PATCH 06/18] test: isolate receipt checks from code gate Co-authored-by: John Menke --- tests/test-verify-receipt.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test-verify-receipt.sh b/tests/test-verify-receipt.sh index 8e33ebc5..33442176 100755 --- a/tests/test-verify-receipt.sh +++ b/tests/test-verify-receipt.sh @@ -47,7 +47,7 @@ echo "== test_capture_without_verify_receipt_refuses ==" T="${WORK}/capture-refuse" work_id="FEAT-020-i1-receipt" setup_feature "${T}" -sdlc "${T}" resume "${work_id}" --phase code >/dev/null +sdlc "${T}" pointer set "${work_id}" >/dev/null if out="$(sdlc "${T}" capture --phase code --summary "T01 complete" --validation "looked fine" 2>&1)"; then bad "capture without receipt should refuse: ${out}" else @@ -67,7 +67,7 @@ echo "== test_complete_without_verify_receipt_refuses ==" T="${WORK}/complete-refuse" work_id="FEAT-021-i1-complete" setup_feature "${T}" -sdlc "${T}" resume "${work_id}" --phase code >/dev/null +sdlc "${T}" pointer set "${work_id}" >/dev/null if out="$(sdlc "${T}" complete --summary "T01 complete" 2>&1)"; then bad "complete without receipt should refuse: ${out}" else @@ -82,7 +82,7 @@ echo "== test_complete_fail_receipt_refuses ==" T="${WORK}/complete-fail" work_id="FEAT-022-i1-fail" setup_feature "${T}" -sdlc "${T}" resume "${work_id}" --phase code >/dev/null +sdlc "${T}" pointer set "${work_id}" >/dev/null if out="$(sdlc "${T}" complete --summary "T01 complete" \ --verify-command "pytest" --verify-exit 1 --verify-result fail 2>&1)"; then bad "complete with fail receipt should refuse: ${out}" @@ -98,7 +98,7 @@ echo "== test_capture_with_receipt_stages_verify_object ==" T="${WORK}/capture-ok" work_id="FEAT-023-i1-ok" setup_feature "${T}" -sdlc "${T}" resume "${work_id}" --phase code >/dev/null +sdlc "${T}" pointer set "${work_id}" >/dev/null if sdlc "${T}" capture --phase code --summary "T01 complete" \ --verify-command "pytest tests/test_foo.py" \ --verify-exit 0 \ @@ -122,7 +122,7 @@ echo "== test_complete_with_pass_receipt_succeeds ==" T="${WORK}/complete-ok" work_id="FEAT-024-i1-done" setup_feature "${T}" -sdlc "${T}" resume "${work_id}" --phase code >/dev/null +sdlc "${T}" pointer set "${work_id}" >/dev/null if sdlc "${T}" complete --summary "T01 complete" \ --verify-command "true" \ --verify-exit 0 \ From fafb481d7804e716b931525fd695109fcf8cb7ed Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 14 Sep 2026 04:55:49 +0000 Subject: [PATCH 07/18] docs: record REF-003 T02 completion Co-authored-by: John Menke --- .../spdd/canvas/REF-003-retire-bash-workflow-dual-path.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sdlc-spdd/spdd/canvas/REF-003-retire-bash-workflow-dual-path.md b/sdlc-spdd/spdd/canvas/REF-003-retire-bash-workflow-dual-path.md index 8b8edd25..9cbfb861 100644 --- a/sdlc-spdd/spdd/canvas/REF-003-retire-bash-workflow-dual-path.md +++ b/sdlc-spdd/spdd/canvas/REF-003-retire-bash-workflow-dual-path.md @@ -135,7 +135,7 @@ already means the only supported engine. `SDLC_ENGINE=shell` and any ### T02 - Make the dispatcher and retained utilities Python-only -- Status: Pending +- Status: Complete - Description: Remove workflow probing/fallback from both `sdlc.sh` copies; reject shell engine overrides; require resolved Python 3.12; route start/capture/complete/accept through Python or its shell bridge; stop @@ -241,8 +241,8 @@ already means the only supported engine. `SDLC_ENGINE=shell` and any ## Review Checklist - [x] T01 bridge is covered for source and installed targets -- [ ] T02 dispatcher has one mandatory engine -- [ ] T02 retained utilities have no twin dependency +- [x] T02 dispatcher has one mandatory engine +- [x] T02 retained utilities have no twin dependency - [ ] T03 twin files are absent from source and installs - [ ] T03 shipped LOC reduction is at least 3,000 - [ ] T04 current shell/pytest matrix is green From f506af392cd426eb8ca0c9655fe51b7c8f4fe3de Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 14 Sep 2026 04:58:15 +0000 Subject: [PATCH 08/18] refactor: stop shipping bash workflow twins Co-authored-by: John Menke --- scripts/init-project.sh | 14 - scripts/upgrade-project.sh | 25 +- scripts/verify-project-install.sh | 12 +- sdlc-spdd/scripts/sdlc-pointer.sh | 127 - sdlc-spdd/scripts/sdlc-team-registry.sh | 927 -------- sdlc-spdd/scripts/sdlc-workflow.sh | 2046 ----------------- sdlc-spdd/scripts/verify-project-install.sh | 12 +- templates/agent-context/sdlc-pointer.sh | 127 - templates/agent-context/sdlc-team-registry.sh | 927 -------- templates/agent-context/sdlc-workflow.sh | 2046 ----------------- tests/live-consumer/scenarios/01-install.sh | 5 +- tests/test-adapter-install.sh | 14 + 12 files changed, 56 insertions(+), 6226 deletions(-) delete mode 100755 sdlc-spdd/scripts/sdlc-pointer.sh delete mode 100755 sdlc-spdd/scripts/sdlc-team-registry.sh delete mode 100755 sdlc-spdd/scripts/sdlc-workflow.sh delete mode 100755 templates/agent-context/sdlc-pointer.sh delete mode 100755 templates/agent-context/sdlc-team-registry.sh delete mode 100755 templates/agent-context/sdlc-workflow.sh diff --git a/scripts/init-project.sh b/scripts/init-project.sh index 2fd31744..67721be3 100755 --- a/scripts/init-project.sh +++ b/scripts/init-project.sh @@ -267,20 +267,6 @@ if [[ "${INSTALL_CURSOR}" -eq 1 && "${INSTALL_COPILOT}" -eq 1 ]]; then "${TARGET}/.github/workflows/validate-sdlc-spdd-adapters.yml" fi -# Workflow CLI: sdlc.sh + pointer/workflow/team-registry managers live together -# under /scripts/. -for file in \ - sdlc-pointer.sh \ - sdlc-workflow.sh \ - sdlc-team-registry.sh; do - copy_if_missing \ - "${REPO_ROOT}/templates/agent-context/${file}" \ - "${HOME_DIR}/scripts/${file}" - if [[ "${DRY_RUN}" -eq 0 && -f "${HOME_DIR}/scripts/${file}" ]]; then - chmod +x "${HOME_DIR}/scripts/${file}" - fi -done - copy_if_missing \ "${REPO_ROOT}/templates/agent-context/hooks/notify-team-registry.example.sh" \ "${HOME_DIR}/scripts/hooks/notify-team-registry.example.sh" diff --git a/scripts/upgrade-project.sh b/scripts/upgrade-project.sh index f051195d..a705a64c 100755 --- a/scripts/upgrade-project.sh +++ b/scripts/upgrade-project.sh @@ -115,6 +115,7 @@ updated=() unchanged=() backed_up=() preserved=() +removed=() CLAUDE_BEGIN="" CLAUDE_END="" @@ -194,6 +195,21 @@ copy_executable_framework_file() { fi } +remove_retired_framework_file() { + local dest="$1" + if [[ ! -f "${dest}" ]]; then + unchanged+=("${dest} (already absent)") + return + fi + backup_existing "${dest}" + if [[ "${DRY_RUN}" -eq 1 ]]; then + echo "[dry-run] would remove retired framework file ${dest}" + else + rm -f "${dest}" + fi + removed+=("${dest}") +} + # Adapter files are rewritten so IDE stubs at the repo root reference paths # under the single-folder home. Rewriting the template into a temp copy first # keeps the cmp-based idempotency (second run sees identical content). @@ -428,14 +444,13 @@ if [[ "${UPGRADE_CURSOR}" -eq 1 && "${UPGRADE_COPILOT}" -eq 1 ]]; then "${TARGET}/.github/workflows/validate-sdlc-spdd-adapters.yml" fi -# Workflow CLI managers live with the runtime scripts under /scripts/. +# Python sdlc_engine is the only workflow implementation. Remove exact +# framework-owned bash twins left by earlier v3 installs. for file in \ sdlc-pointer.sh \ sdlc-workflow.sh \ sdlc-team-registry.sh; do - copy_executable_framework_file \ - "${REPO_ROOT}/templates/agent-context/${file}" \ - "${HOME_DIR}/scripts/${file}" + remove_retired_framework_file "${HOME_DIR}/scripts/${file}" done copy_framework_file \ @@ -547,6 +562,8 @@ echo "Created (${#created[@]}):" printf ' %s\n' "${created[@]:-none}" echo "Updated framework files (${#updated[@]}):" printf ' %s\n' "${updated[@]:-none}" +echo "Removed retired framework files (${#removed[@]}):" +printf ' %s\n' "${removed[@]:-none}" echo "Unchanged framework files (${#unchanged[@]}):" printf ' %s\n' "${unchanged[@]:-none}" echo "Preserved existing project content (${#preserved[@]}):" diff --git a/scripts/verify-project-install.sh b/scripts/verify-project-install.sh index efbab321..26f0e0be 100755 --- a/scripts/verify-project-install.sh +++ b/scripts/verify-project-install.sh @@ -103,6 +103,12 @@ check_path() { return 1 fi ;; + absent) + if [[ ! -e "${full}" ]]; then + echo " ok ${label}: ${path} (absent)" + return 0 + fi + ;; glob) shopt -s nullglob local matches=("${TARGET}"/${path}) @@ -178,9 +184,9 @@ run_part "Framework context (harness and skills)" \ run_part "Workflow CLI and docs" \ Runtime "workflow scripts directory" "${HOME_REL}/scripts" dir \ Runtime "workflow helper script" "${HOME_REL}/scripts/sdlc.sh" executable \ - Runtime "pointer manager script" "${HOME_REL}/scripts/sdlc-pointer.sh" executable \ - Runtime "workflow manager script" "${HOME_REL}/scripts/sdlc-workflow.sh" executable \ - Runtime "team registry script" "${HOME_REL}/scripts/sdlc-team-registry.sh" executable \ + Runtime "retired pointer manager" "${HOME_REL}/scripts/sdlc-pointer.sh" absent \ + Runtime "retired workflow manager" "${HOME_REL}/scripts/sdlc-workflow.sh" absent \ + Runtime "retired team registry" "${HOME_REL}/scripts/sdlc-team-registry.sh" absent \ Runtime "start session script" "${HOME_REL}/scripts/start-agent-session.sh" executable \ Runtime "capture memory script" "${HOME_REL}/scripts/capture-session-memory.sh" executable \ Runtime "accept lessons script" "${HOME_REL}/scripts/accept-lessons.sh" executable \ diff --git a/sdlc-spdd/scripts/sdlc-pointer.sh b/sdlc-spdd/scripts/sdlc-pointer.sh deleted file mode 100755 index b3a3055d..00000000 --- a/sdlc-spdd/scripts/sdlc-pointer.sh +++ /dev/null @@ -1,127 +0,0 @@ -#!/usr/bin/env bash -# Simple pointer manager for SDLC chores/tasks -# Usage: -# source sdlc-spdd/scripts/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 "" -- -# -# Default storage: sdlc-spdd/.sdlc/pointer. - -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)}" -_paths_lib="${SDLC_ROOT}/sdlc-spdd/scripts/lib/paths.sh" -if [[ -f "${_paths_lib}" ]]; then - # shellcheck source=/dev/null - source "${_paths_lib}" - SDLC_DIR="${SDLC_DIR:-$(sdlc_runtime_dir "${SDLC_ROOT}")}" -else - SDLC_DIR="${SDLC_DIR:-${SDLC_HOME:-${SDLC_ROOT}/sdlc-spdd}/.sdlc}" -fi -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 diff --git a/sdlc-spdd/scripts/sdlc-team-registry.sh b/sdlc-spdd/scripts/sdlc-team-registry.sh deleted file mode 100755 index f0093da8..00000000 --- a/sdlc-spdd/scripts/sdlc-team-registry.sh +++ /dev/null @@ -1,927 +0,0 @@ -#!/usr/bin/env bash -# Team-visible Work ID registry — committed coordination layer on top of local .sdlc/ state. -# -# Local pointer (sdlc-spdd/.sdlc/pointer) stays machine-private. -# spdd/memory/registry.jsonl 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" -_paths_lib="${SDLC_ROOT}/sdlc-spdd/scripts/lib/paths.sh" -if [[ -f "${_paths_lib}" ]]; then - # shellcheck source=/dev/null - source "${_paths_lib}" -fi - -SDLC_TEAM_REGISTRY_JSONL="$(sdlc_registry "${SDLC_ROOT}" 2>/dev/null || printf '%s/spdd/memory/registry.jsonl' "${SDLC_HOME:-${SDLC_ROOT}/sdlc-spdd}")" -SDLC_TEAM_REGISTRY_LOCK="${SDLC_DIR:-${SDLC_HOME:-${SDLC_ROOT}/sdlc-spdd}/.sdlc}/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 home canvas - if declare -F sdlc_home >/dev/null 2>&1; then - home="$(sdlc_home "${root}")" - else - home="${SDLC_HOME:-${root}/sdlc-spdd}" - fi - canvas="${home}/spdd/canvas/${work_id}.md" - [[ -f "${canvas}" ]] && printf '%s' "${canvas}" -} - -# Prints Final Status line text (empty when missing). -_team_canvas_final_status_text() { - local work_id="$1" - local canvas - canvas="$(_team_canvas_path "${work_id}")" - [[ -n "${canvas}" ]] || return 0 - awk ' - /^## Final Status/ { in_final=1; next } - /^## / { if (in_final) in_final=0 } - in_final && /^- Status:/ { - sub(/^- Status:[[:space:]]*/, "") - print - exit - } - ' "${canvas}" -} - -# Prints complete | cancelled | other based on ## Final Status. -_team_canvas_final_kind() { - local work_id="$1" - local line - line="$(_team_canvas_final_status_text "${work_id}")" - [[ -z "${line}" ]] && { printf '%s' "other"; return 0; } - line="$(printf '%s' "${line}" | tr '[:upper:]' '[:lower:]')" - if [[ "${line}" == *cancel* ]]; then - printf '%s' "cancelled" - return 0 - fi - if [[ "${line}" == *complete* ]] && [[ "${line}" != *in\ progress* ]]; then - printf '%s' "complete" - return 0 - fi - printf '%s' "other" -} - -_team_canvas_is_complete() { - local work_id="$1" - [[ "$(_team_canvas_final_kind "${work_id}")" == "complete" ]] -} - -_team_canvas_is_cancelled() { - local work_id="$1" - [[ "$(_team_canvas_final_kind "${work_id}")" == "cancelled" ]] -} - -_team_canvas_is_archivable() { - local work_id="$1" - local kind - kind="$(_team_canvas_final_kind "${work_id}")" - [[ "${kind}" == "complete" || "${kind}" == "cancelled" ]] -} - -_team_registry_note_for() { - local work_id="$1" - _team_registry_lookup_row "${work_id}" | awk -F '\t' '{ print $7; exit }' -} - -_team_registry_lookup_row() { - local work_id="$1" - _team_registry_rows | awk -F '\t' -v id="${work_id}" '$1 == id { print; exit }' -} - -_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_milestone_path() { - local work_id="$1" - local root="${SDLC_ROOT}" - if declare -F sdlc_home >/dev/null 2>&1; then - root="$(sdlc_home "${SDLC_ROOT}")" - fi - local path dir - # Prefer subdirectory stubs when present. - shopt -s nullglob - for dir in "${root}"/requirements/milestones/milestone-*/; do - path="${dir}${work_id}.md" - if [[ -f "${path}" ]]; then - shopt -u nullglob - printf '%s' "${path}" - return 0 - fi - done - shopt -u nullglob - path="${root}/requirements/milestones/${work_id}.md" - [[ -f "${path}" ]] && printf '%s' "${path}" -} - -# Reads Jira key from requirement ## Jira section (- Key: ABC-123), or jira_key frontmatter. -_team_jira_from_milestone() { - local work_id="$1" - local path - path="$(_team_milestone_path "${work_id}")" - [[ -n "${path}" ]] || return 0 - local from_section - from_section="$(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}")" - if [[ -n "${from_section}" ]]; then - printf '%s' "${from_section}" - return 0 - fi - awk ' - NR==1 && /^---[[:space:]]*$/ { in_fm=1; next } - in_fm && /^---[[:space:]]*$/ { exit } - in_fm && /^jira_key:[[:space:]]*/ { - sub(/^jira_key:[[: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_jira_from_note() { - local note="${1:-}" - local token - for token in ${note}; do - if [[ "${token}" == jira:* ]]; then - printf '%s' "${token#jira:}" - return 0 - fi - done -} - -# Resolve tracker key for a Work ID. -# Prints: | draft | missing -# Prefer claim-note jira:TOKEN, then requirement Key, else draft if ## Jira exists. -sdlc_team_jira_status() { - local work_id="${1:-}" - [[ -n "${work_id}" ]] || { printf 'missing'; return 0; } - local note="" key="" - if [[ -f "${SDLC_TEAM_REGISTRY_JSONL}" ]]; then - note="$(_team_registry_note_for "${work_id}" || true)" - key="$(_team_jira_from_note "${note}")" - fi - if [[ -n "${key}" ]]; then - printf '%s' "${key}" - return 0 - fi - key="$(_team_jira_from_milestone "${work_id}" || true)" - if [[ -n "${key}" ]]; then - printf '%s' "${key}" - return 0 - fi - if _team_milestone_has_jira_draft "${work_id}"; then - printf 'draft' - return 0 - fi - printf 'missing' -} - -# Agent-facing instruction when Jira is unset. Empty when a key is known or -# SDLC_SESSION_ASK_JIRA=0. Use in Resume Prompt / next / whereami. -sdlc_team_jira_ask_prompt() { - local work_id="${1:-}" - [[ -n "${work_id}" ]] || return 0 - [[ "${SDLC_SESSION_ASK_JIRA:-1}" == "1" ]] || return 0 - local status - status="$(sdlc_team_jira_status "${work_id}")" - case "${status}" in - missing) - printf '%s\n' "Tracker link: Jira key is missing for ${work_id}. Ask the user for the issue key (or confirm none applies) before coding or claiming tracker progress. Then run \`./scripts/sdlc.sh claim ${work_id} --jira KEY\` (or set \`- Key:\` under \`## Jira\` on the requirement and re-claim). Do not invent a key." - ;; - draft) - printf '%s\n' "Tracker link: Jira draft exists for ${work_id} but \`- Key:\` is unset. Ask the user for the issue key (or confirm none applies) before coding or claiming tracker progress. Then run \`./scripts/sdlc.sh claim ${work_id} --jira KEY\` (or set \`- Key:\` under \`## Jira\` and re-claim). Do not invent a key." - ;; - esac -} - -_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 kind cur_status cur_phase cur_op cur_note target_status note_token - _team_registry_init - while IFS= read -r work_id; do - [[ -z "${work_id}" ]] && continue - kind="$(_team_canvas_final_kind "${work_id}")" - case "${kind}" in - complete) target_status="done"; note_token="canvas Final Status: Complete" ;; - cancelled) target_status="cancelled"; note_token="canvas Final Status: Cancelled" ;; - *) continue ;; - esac - cur_status="$(awk -F '\t' -v id="${work_id}" '$1 == id { print $2; exit }' <<< "$(_team_registry_rows)")" - # Do not reopen archived rows from canvas sync. - [[ "${cur_status}" == "archived" ]] && continue - if [[ -z "${cur_status}" ]]; then - sdlc_team_register "${work_id}" "${target_status}" "sync" "" "${note_token}" - continue - fi - [[ "${cur_status}" == "${target_status}" ]] && continue - cur_phase="$(awk -F '\t' -v id="${work_id}" '$1 == id { print $3; exit }' <<< "$(_team_registry_rows)")" - cur_op="$(awk -F '\t' -v id="${work_id}" '$1 == id { print $4; exit }' <<< "$(_team_registry_rows)")" - cur_note="$(_team_compose_note "$(_team_registry_note_for "${work_id}")" "" "" "" "${note_token}")" - sdlc_team_register "${work_id}" "${target_status}" "${cur_phase}" "${cur_op}" "${cur_note}" - done < <(sdlc_team_discover_work_ids) -} - -_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 "$(dirname "${SDLC_TEAM_REGISTRY_JSONL}")" "${SDLC_DIR:-${SDLC_HOME:-${SDLC_ROOT}/sdlc-spdd}/.sdlc}" - if [[ ! -f "${SDLC_TEAM_REGISTRY_JSONL}" ]]; then - : > "${SDLC_TEAM_REGISTRY_JSONL}" - fi -} - -_team_registry_read_events() { - _team_registry_init - if [[ -f "${SDLC_TEAM_REGISTRY_JSONL}" ]] && [[ -s "${SDLC_TEAM_REGISTRY_JSONL}" ]]; then - python3 - </dev/null 2>&1; then - ( - flock -x 200 || exit 1 - "$@" - ) 200>"${SDLC_TEAM_REGISTRY_LOCK}" - else - "$@" - fi -} - - -_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 event="${6:-update}" - local owner updated - owner="$(_team_owner)" - updated="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" - _team_registry_init - if [[ -z "${note}" ]]; then - note="$(_team_registry_note_for "${work_id}")" - fi - local payload - payload="$(python3 - </dev/null 2>&1; then - sdlc_append_jsonl "${SDLC_TEAM_REGISTRY_JSONL}" "${payload}" - else - printf '%s\n' "${payload}" >> "${SDLC_TEAM_REGISTRY_JSONL}" - fi -} - -sdlc_team_register() { - local work_id="$1" - local status="$2" - local phase="${3:-}" - local operation="${4:-}" - local note="${5:-}" - local event="${6:-update}" - 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}" "${event}" - local hook_owner hook_updated hook_note row - row="$(_team_registry_lookup_row "${work_id}")" - hook_owner="$(awk -F '\t' '{ print $5 }' <<< "${row}")" - hook_updated="$(awk -F '\t' '{ print $6 }' <<< "${row}")" - hook_note="$(awk -F '\t' '{ print $7 }' <<< "${row}")" - _team_run_hook "${work_id}" "${status}" "${phase}" "${operation}" \ - "${hook_owner}" "${hook_updated}" "${hook_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 }' <<< "$(_team_registry_rows)")" - status="$(awk -F '\t' -v id="${work_id}" '$1 == id { print $2; exit }' <<< "$(_team_registry_rows)")" - updated="$(awk -F '\t' -v id="${work_id}" '$1 == id { print $6; exit }' <<< "$(_team_registry_rows)")" - [[ -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 - 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 event="update" - file="${SDLC_DIR:-${SDLC_HOME:-${SDLC_ROOT}/sdlc-spdd}/.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 - case "${status}" in - active) event="claim" ;; - shelved) event="release" ;; - archived) event="archive" ;; - done|cancelled) event="refresh" ;; - esac - sdlc_team_register "${work_id}" "${status}" "${phase}" "${operation}" "${note}" "${event}" -} - -sdlc_team_discover_work_ids() { - local root="${SDLC_ROOT}" - local home - if declare -F sdlc_home >/dev/null 2>&1; then - home="$(sdlc_home "${root}")" - else - home="${SDLC_HOME:-${root}/sdlc-spdd}" - fi - local -A seen=() - local path base - shopt -s nullglob - for path in \ - "${home}"/spdd/canvas/*.md \ - "${home}"/requirements/milestones/*.md \ - "${home}"/requirements/milestones/milestone-*/*.md; do - base="$(basename "${path}" .md)" - [[ "${base}" == "README" || "${base}" == "archive" ]] && continue - [[ "${base}" == MILESTONE-* || "${base}" == milestone-* ]] && continue - [[ -n "${base}" ]] || continue - [[ "${path}" == */archive/* || "${path}" == */archive ]] && continue - seen["${base}"]=1 - done - shopt -u nullglob - printf '%s\n' "${!seen[@]}" | sort -} - -_team_remove_path() { - local path="$1" - local dry="${2:-0}" - [[ -e "${path}" ]] || return 0 - if [[ "${dry}" -eq 1 ]]; then - echo "[dry-run] would remove ${path#${SDLC_ROOT}/}" - return 0 - fi - rm -rf "${path}" - echo "Removed ${path#${SDLC_ROOT}/}" -} - -# Remove completed/cancelled Work ID contract artifacts from the working tree. -# Keeps requirements/milestones/.md in place as historical requirement source. -# Git history retains removed files; no spdd/*/archive/ folders in storage v3. -sdlc_team_archive_work() { - local work_id="${1:-}" - local dry="${2:-0}" - local force="${3:-0}" - local root="${SDLC_ROOT}" - local kind pointer moved=0 - - if [[ -z "${work_id}" ]]; then - echo "archive: Work ID required" >&2 - return 2 - fi - - kind="$(_team_canvas_final_kind "${work_id}")" - if [[ "${force}" -ne 1 && "${kind}" != "complete" && "${kind}" != "cancelled" ]]; then - echo "archive: ${work_id} is not Complete or Cancelled (Final Status kind=${kind}). Use --force to archive anyway." >&2 - return 1 - fi - - pointer="$(sdlc_get_pointer)" - if [[ "${pointer}" == "${work_id}" ]]; then - if [[ "${dry}" -eq 1 ]]; then - echo "[dry-run] would clear pointer for ${work_id}" - else - sdlc_reset_pointer >/dev/null - echo "Cleared local pointer (was ${work_id})" - fi - fi - - local feature_src="" feature_dest="" - # Storage v3 has no per-feature mirror tree. - - local canvas_src review_src analysis_src sync_src home - if declare -F sdlc_home >/dev/null 2>&1; then - home="$(sdlc_home "${root}")" - else - home="${SDLC_HOME:-${root}/sdlc-spdd}" - fi - canvas_src="${home}/spdd/canvas/${work_id}.md" - analysis_src="${home}/spdd/analysis/${work_id}-analysis.md" - review_src="${home}/spdd/reviews/${work_id}-review.md" - sync_src="${home}/spdd/sync/${work_id}-sync.md" - [[ -f "${canvas_src}" ]] && _team_remove_path "${canvas_src}" "${dry}" && moved=1 - [[ -f "${analysis_src}" ]] && _team_remove_path "${analysis_src}" "${dry}" && moved=1 - [[ -f "${review_src}" ]] && _team_remove_path "${review_src}" "${dry}" && moved=1 - [[ -f "${sync_src}" ]] && _team_remove_path "${sync_src}" "${dry}" && moved=1 - - local session_dir - if declare -F sdlc_sessions_dir >/dev/null 2>&1; then - session_dir="$(sdlc_sessions_dir "${root}")" - else - session_dir="${home}/.sdlc/sessions" - fi - if [[ -d "${session_dir}" ]]; then - local sess - shopt -s nullglob - for sess in "${session_dir}"/*"${work_id}"*; do - [[ -f "${sess}" ]] || continue - [[ "$(basename "${sess}")" == "current-session.md" ]] && continue - _team_remove_path "${sess}" "${dry}" && moved=1 - done - shopt -u nullglob - fi - - local state_src="${SDLC_DIR:-${home}/.sdlc}/workflows/${work_id}.state" - if [[ -f "${state_src}" ]]; then - _team_remove_path "${state_src}" "${dry}" && moved=1 - fi - - if [[ "${dry}" -eq 1 ]]; then - echo "[dry-run] would mark ${work_id} archived in registry.jsonl" - return 0 - fi - - local note_token="archived:${kind}" - [[ "${force}" -eq 1 && "${kind}" == "other" ]] && note_token="archived:forced" - sdlc_team_register "${work_id}" "archived" "archive" "" "${note_token}" "archive" - if [[ "${moved}" -eq 0 ]]; then - echo "archive: ${work_id} marked archived (no movable artifacts found; milestone left in place)" - else - echo "Archived ${work_id} (${kind}). Commit deletions + spdd/memory/registry.jsonl." - fi - echo "Left in place: requirements/milestones/${work_id}.md (if present)." - echo "Left in place: spdd/memory/lessons.jsonl (archive never truncates the lessons ledger)." -} - -# Archive every Work ID whose canvas Final Status is Complete or Cancelled. -sdlc_team_archive_eligible() { - local dry="${1:-0}" - local work_id count=0 cur - _team_registry_init - while IFS= read -r work_id; do - [[ -z "${work_id}" ]] && continue - _team_canvas_is_archivable "${work_id}" || continue - cur="$(awk -F '\t' -v id="${work_id}" '$1 == id { print $2; exit }' <<< "$(_team_registry_rows)" 2>/dev/null || true)" - [[ "${cur}" == "archived" ]] && continue - if sdlc_team_archive_work "${work_id}" "${dry}" 0; then - count=$((count + 1)) - fi - done < <(sdlc_team_discover_work_ids) - echo "archive: processed ${count} eligible Work ID(s)" -} - -sdlc_team_infer_work_summary() { - local work_id="$1" - local root="${SDLC_ROOT}" - local home parts=() - if declare -F sdlc_home >/dev/null 2>&1; then - home="$(sdlc_home "${root}")" - else - home="${SDLC_HOME:-${root}/sdlc-spdd}" - fi - [[ -f "${home}/spdd/canvas/${work_id}.md" ]] && parts+=("canvas") - if [[ -n "$(_team_milestone_path "${work_id}" || true)" ]]; then - parts+=("milestone") - fi - 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 - local IFS=', ' - printf '%s' "${parts[*]}" - fi -} - -sdlc_team_list_work() { - local work_id - sdlc_team_refresh_done_status - echo "Work IDs in this repository:" - echo - 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 updated stale done_hint - reg_status="available" - phase="-" - owner="-" - updated="" - reg_status="$(awk -F '\t' -v id="${work_id}" '$1 == id { print $2; exit }' <<< "$(_team_registry_rows)")" - if [[ -n "${reg_status}" ]]; then - phase="$(awk -F '\t' -v id="${work_id}" '$1 == id { print $3; exit }' <<< "$(_team_registry_rows)")" - owner="$(awk -F '\t' -v id="${work_id}" '$1 == id { print $5; exit }' <<< "$(_team_registry_rows)")" - updated="$(awk -F '\t' -v id="${work_id}" '$1 == id { print $6; exit }' <<< "$(_team_registry_rows)")" - phase="${phase:--}" - owner="${owner:--}" - stale="$(_team_stale_label "${updated}" "${reg_status}")" - reg_status="${reg_status}${stale}" - else - reg_status="available" - fi - 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: ./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 - echo "Your local pointer: (none)" - fi - echo - echo "Team registry (commit spdd/memory/registry.jsonl to share):" - if _team_registry_rows | grep -q .; then - 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}")" - 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}")" - 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 %-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 sdlc-spdd/scripts/hooks/notify-team-registry.sh" - echo "Discover all Work IDs: ./scripts/sdlc.sh list-work" -} - -# Locate the workflow manager: same dir as this script (v3 installs place both -# under /scripts/), then /scripts/. -_team_workflow_script() { - local self_dir - self_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - local candidate - for candidate in \ - "${self_dir}/sdlc-workflow.sh" \ - "${SDLC_ROOT}/sdlc-spdd/scripts/sdlc-workflow.sh"; do - if [[ -f "${candidate}" ]]; then - printf '%s' "${candidate}" - return 0 - fi - done - return 1 -} - -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 - fi - sdlc_team_check_claim "${work_id}" "${force}" || return $? - local _workflow_script - if ! _workflow_script="$(_team_workflow_script)"; then - echo "sdlc_team_claim: sdlc-workflow.sh not installed" >&2 - 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}")" - # shellcheck source=/dev/null - source "${_workflow_script}" - # Pass force through so `sdlc.sh claim --force` can take over a foreign claim. - # Mark already-checked so resume does not print "Taking over…" a second time. - _SDLC_TEAM_CLAIM_CHECKED=1 - sdlc_workflow_resume "${work_id}" "${phase}" 1 "${force}" "${note}" - local resume_rc=$? - unset _SDLC_TEAM_CLAIM_CHECKED - (( resume_rc == 0 )) || return "${resume_rc}" - echo "Team registry updated — commit spdd/memory/registry.jsonl 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 - local _workflow_script - if ! _workflow_script="$(_team_workflow_script)"; then - echo "sdlc_team_release: sdlc-workflow.sh not installed" >&2 - return 1 - fi - # shellcheck source=/dev/null - source "${_workflow_script}" - sdlc_workflow_shelf "${reason}" - echo "Team registry updated — commit spdd/memory/registry.jsonl 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="" - 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}" "${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" - while [[ $# -gt 0 ]]; do - case "$1" in - --reason) reason="${2:-}"; shift 2 ;; - *) shift ;; - esac - done - sdlc_team_release "${reason}" - ;; - archive) - dry=0 - force=0 - all=0 - work_id="" - while [[ $# -gt 0 ]]; do - case "$1" in - --dry-run) dry=1; shift ;; - --force) force=1; shift ;; - --all|--all-eligible) all=1; shift ;; - *) - if [[ -z "${work_id}" && "$1" != -* ]]; then - work_id="$1" - fi - shift - ;; - esac - done - if [[ "${all}" -eq 1 ]]; then - sdlc_team_archive_eligible "${dry}" - else - sdlc_team_archive_work "${work_id}" "${dry}" "${force}" - fi - ;; - *) - echo "Usage: $0 {team|list-work|claim|release|archive} ..." >&2 - exit 2 - ;; - esac -fi diff --git a/sdlc-spdd/scripts/sdlc-workflow.sh b/sdlc-spdd/scripts/sdlc-workflow.sh deleted file mode 100755 index d7529e9a..00000000 --- a/sdlc-spdd/scripts/sdlc-workflow.sh +++ /dev/null @@ -1,2046 +0,0 @@ -#!/usr/bin/env bash -# Workflow state manager for SDLC-SPDD — tracks phase, gates, shelf/resume on top of sdlc-pointer.sh -# -# Usage: -# source sdlc-spdd/scripts/sdlc-workflow.sh -# sdlc_workflow_status -# sdlc_workflow_resume WORK_ID [--phase PHASE] -# sdlc_workflow_advance [--to PHASE] -# sdlc_workflow_gate PHASE [WORK_ID] -# sdlc_workflow_skip PHASE [--reason TEXT] -# sdlc_workflow_shelf [--reason TEXT] -# sdlc_workflow_sync [--work-id WORK_ID] -# -# State: sdlc-spdd/.sdlc/workflows/.state and history - -if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then - set -euo pipefail -fi - -# Track nested source depth so CLI does not re-enter when this file is sourced -# from sdlc_team_claim/release while already executing as the main script -# (sdlc.sh exec's this file with an absolute $0, which makes BASH_SOURCE==$0 -# even for nested source). -_SDLC_WORKFLOW_LOAD_DEPTH=$(( ${_SDLC_WORKFLOW_LOAD_DEPTH:-0} + 1 )) - -_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 - -# Optional readiness helper from the v3 home. -_wf_readiness_lib="${SDLC_HOME:-${SDLC_ROOT}/sdlc-spdd}/scripts/lib/readiness.sh" -if [[ -f "${_wf_readiness_lib}" ]]; then - # shellcheck source=/dev/null - source "${_wf_readiness_lib}" -fi - -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 -} - -_wf_work_readiness() { - local work_id="$1" - local canvas - canvas="$(_wf_canvas_path "${work_id}")" - [[ -n "${canvas}" ]] || return 0 - if declare -F canvas_readiness >/dev/null 2>&1; then - canvas_readiness "${canvas}" - fi -} - -_wf_is_quiet() { - local q - q="$(printf '%s' "${SDLC_QUIET:-}" | tr '[:upper:]' '[:lower:]')" - [[ "${q}" == "1" || "${q}" == "true" || "${q}" == "yes" || "${q}" == "on" ]] \ - || [[ -f "${SDLC_HOME:-${SDLC_ROOT:-.}/sdlc-spdd}/harness/quiet-mode.md" ]] -} - -_wf_quiet_blurb() { - # Keep in sync with sdlc_engine.quiet.quiet_resume_blurb (default, non-guide). - echo "Quiet mode: load resolved context and continue the product task. Skip recommended /sdlc-spdd-* T## dogfood commands unless explicitly requested." -} - -sdlc_workflow_recommended_command() { - local phase="${1:-init}" - local work_id="${2:-}" - local operation="${3:-}" - local readiness="" - # Quiet / product-test mode (#91): no T## dogfood gravity. - if _wf_is_quiet; then - _wf_quiet_blurb - return 0 - fi - if [[ -z "${operation}" && -n "${work_id}" ]]; then - operation="$(_wf_resolve_operation "${work_id}" "${phase}")" - fi - if [[ -n "${work_id}" ]] && declare -F canvas_readiness >/dev/null 2>&1; then - readiness="$(_wf_work_readiness "${work_id}")" - fi - # Architecture-first: do not recommend /sdlc-spdd-code when readiness blocks coding. - if [[ "${phase}" == "code" && -n "${readiness}" ]]; then - if declare -F readiness_allows_coding >/dev/null 2>&1 \ - && ! readiness_allows_coding "${readiness}"; then - echo "/sdlc-spdd-architect @spdd/canvas/${work_id}.md # readiness=${readiness} — not Ready For Coding" - return 0 - fi - 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 ${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 .sdlc/sessions/current-session.md, then choose the next phase command." ;; - *) echo "/sdlc-spdd-init" ;; - 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 - echo "./sdlc-spdd/scripts/start-agent-session.sh --target . --work-id ${work_id} --phase ${phase}" -} - -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 "${helper} capture --summary \"\"" - return 0 - fi - if [[ -z "${phase}" ]]; then - phase="$(_wf_read_state_var "$(_wf_state_file "${work_id}")" phase resume)" - fi - echo "${helper} capture --phase ${phase} --summary \"\"" -} - -_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 -} - -# Advisory GATE_LABELS that gate_check / this fallback never enforce. -# Keep in sync with engine/src/sdlc_engine/phases.py ADVISORY_GATES + gates_for_phase. -_wf_advisory_gates_for_phase() { - case "${1:-}" in - architect) printf '%s\n' architect_review operations_task_sized ;; - code|api-test) printf '%s\n' architect_review operations_task_sized tests_updated ;; - prompt-update|sync) printf '%s\n' canvas_synced ;; - *) ;; - esac -} - -_wf_gate_label() { - local want="$1" - local gi - for ((gi = 0; gi < ${#SDLC_GATE_NAMES[@]}; gi++)); do - if [[ "${SDLC_GATE_NAMES[$gi]}" == "${want}" ]]; then - printf '%s' "${SDLC_GATE_LABELS[$gi]}" - return 0 - fi - done - printf '%s' "${want}" -} - -_wf_print_advisory_gate_rows() { - # Leftover #14: gate must show advisory rows that did not run. - local phase="$1" - local gate label - while IFS= read -r gate; do - [[ -z "${gate}" ]] && continue - label="$(_wf_gate_label "${gate}")" - if [[ "${label}" != *"(advisory)"* ]]; then - label="${label} (advisory)" - fi - echo " ~ ${label} (did not run)" - done < <(_wf_advisory_gates_for_phase "${phase}") -} - -_wf_pass_gates_for_phase() { - local work_id="$1" - local phase="$2" - local gate - local readiness="" - while IFS= read -r gate; do - [[ -z "${gate}" ]] && continue - # Leaving architect: do not auto-pass architect_review when readiness blocks coding. - # Absent readiness stays backward-compatible (gate may still pass). - if [[ "${phase}" == "architect" && "${gate}" == "architect_review" ]]; then - readiness="$(_wf_work_readiness "${work_id}")" - if [[ -n "${readiness}" ]] && declare -F readiness_allows_coding >/dev/null 2>&1 \ - && ! readiness_allows_coding "${readiness}"; then - continue - fi - fi - 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 op_title readiness - 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}")" - readiness="$(_wf_work_readiness "${work_id}")" - op_title="" - if [[ -n "${operation}" ]]; then - op_title="$(_wf_operation_title "${work_id}" "${operation}")" - fi - - local jira_status="" jira_ask="" - if declare -F sdlc_team_jira_status >/dev/null 2>&1; then - jira_status="$(sdlc_team_jira_status "${work_id}")" - fi - if declare -F sdlc_team_jira_ask_prompt >/dev/null 2>&1; then - jira_ask="$(sdlc_team_jira_ask_prompt "${work_id}")" - fi - - cat <"\` | -| Orient / status | \`./scripts/sdlc.sh next\` or \`/sdlc-spdd-whereami\` | - -$(if [[ -n "${pending}" ]]; then - echo "Pending gates:" - while IFS= read -r line; do - [[ -z "${line}" ]] && continue - echo "- ${line}" - done <<< "${pending}" -fi) -$(if [[ -n "${jira_ask}" ]]; then - echo - echo "Tracker follow-up:" - echo "- ${jira_ask}" -fi) -EOF -} - -sdlc_workflow_next() { - 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." - echo - echo "Start here:" - echo " ./scripts/sdlc.sh resume # 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 op_title readiness jira_status jira_ask - 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="" - jira_status="" - jira_ask="" - if [[ -n "${operation}" ]]; then - op_title="$(_wf_operation_title "${work_id}" "${operation}")" - fi - - # Match Python WorkflowEngine.next_text quiet contract (#91). - if _wf_is_quiet; then - echo "== SDLC: what to do now (quiet) ==" - echo "Work ID: ${work_id}" - echo "Phase: ${phase}" - echo - _wf_quiet_blurb - return 0 - fi - - echo "== SDLC: what to do now ==" - echo "Work ID: ${work_id}" - echo "Phase: ${phase} ($(( $(_wf_phase_index "${phase}") + 1 ))/${#SDLC_PHASE_ORDER[@]})" - readiness="$(_wf_work_readiness "${work_id}")" - if [[ -n "${readiness}" ]]; then - echo "Readiness: ${readiness}" - fi - if declare -F sdlc_team_jira_status >/dev/null 2>&1; then - jira_status="$(sdlc_team_jira_status "${work_id}")" - echo "Jira: ${jira_status}" - fi - if declare -F sdlc_team_jira_ask_prompt >/dev/null 2>&1; then - jira_ask="$(sdlc_team_jira_ask_prompt "${work_id}")" - fi - 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}" "${operation}")" - if [[ "${phase}" == "code" ]]; then - if [[ -n "${readiness}" ]] && declare -F readiness_allows_coding >/dev/null 2>&1 \ - && ! readiness_allows_coding "${readiness}"; then - echo - echo "Note: canvas readiness is '${readiness}' — run architect (or prompt-update) before coding." - fi - fi - if [[ -n "${jira_ask}" ]]; then - echo - echo "Tracker follow-up:" - echo " ${jira_ask}" - fi - 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_HOME:-${SDLC_ROOT}/sdlc-spdd}/scripts/start-agent-session.sh" - 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+="}" - - 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}")" - 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 '"operation_title":"%s",' "$(_wf_json_escape "${op_title}")" - printf '"readiness":"%s",' "$(_wf_json_escape "$(_wf_work_readiness "${work_id}")")" - if _wf_is_quiet; then - printf '"quiet":true,' - else - printf '"quiet":false,' - fi - 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}" - 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() { - local helper - helper="$(_wf_shell_helper_path)" - cat < [--phase PHASE] [--force] - ${helper} advance [--to PHASE] [--force] - ${helper} gate [--work-id ID] # check prerequisites to enter a phase (exit 0/1) - ${helper} skip --reason "why" - ${helper} shelf --reason "why" - ${helper} sync [--work-id ID] - ${helper} list-shelved - ${helper} team # team registry + your pointer - ${helper} list-work # all Work IDs in the repo - ${helper} claim [--force] [--branch NAME] [--pr #N] [--jira KEY] - ${helper} release --reason "why" - ${helper} sync-team # mark done/cancelled from canvas Final Status - ${helper} archive # move Complete/Cancelled work into archive/ - ${helper} archive --all # archive every eligible Complete/Cancelled Work ID - ${helper} accept [--work-id ID] [--commit] # promote staged lessons to ledger - -In chat: /sdlc-next or /sdlc-spdd-whereami -Workflow chat: /sdlc-claim, /sdlc-shelf, /sdlc-advance, /sdlc-next, /sdlc-team - -Team sharing: commit spdd/memory/registry.jsonl after claim/release/shelf/archive. -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; auto jira Key from requirements/milestones/.md on claim). - -Typical loop: - 1. ${helper} next - 2. run the assistant command (or ${helper} start) - 3. ${helper} advance - 4. ${helper} capture --summary "..." - -Code phase: next operation (T01, T02, ...) is read from the REASONS Canvas automatically. -EOF -} - -_wf_requirement_path() { - local work_id="$1" - local home - home="$(_wf_home_path)" - local path dir - shopt -s nullglob - for dir in "${home}"/requirements/milestones/milestone-*/; do - path="${dir}${work_id}.md" - if [[ -f "${path}" ]]; then - shopt -u nullglob - printf '%s' "${path}" - return 0 - fi - done - shopt -u nullglob - path="${home}/requirements/milestones/${work_id}.md" - if [[ -f "${path}" ]]; then - printf '%s' "${path}" - fi - return 0 -} - -# Extract shared lean-ledger slices for one Work ID (## WID sections + ### … WID … blocks). -_wf_ledger_section_for_work() { - local file="$1" - local work_id="$2" - [[ -f "${file}" && -n "${work_id}" ]] || return 0 - awk -v wid="${work_id}" ' - BEGIN { in_h2 = 0; in_h3 = 0 } - /^## / { - rest = $0 - sub(/^##[[:space:]]+/, "", rest) - in_h2 = (rest == wid || index(rest, wid " ") == 1 || index(rest, wid " -") == 1) - in_h3 = 0 - if (in_h2) print - next - } - /^### / { - in_h3 = (index($0, wid) > 0) - if (in_h3) print - next - } - in_h2 || in_h3 { print } - ' "${file}" -} - -_wf_paths_lib() { - local root="${SDLC_ROOT}" - local home="${SDLC_HOME:-${root}/sdlc-spdd}" - [[ -f "${home}/scripts/lib/paths.sh" ]] && printf '%s' "${home}/scripts/lib/paths.sh" -} - -_wf_ensure_paths_lib() { - if declare -F sdlc_ledger >/dev/null 2>&1; then - return 0 - fi - local lib - lib="$(_wf_paths_lib)" - [[ -n "${lib}" && -f "${lib}" ]] || return 1 - # shellcheck source=/dev/null - source "${lib}" -} - -_wf_ledger_record_count() { - local root="$1" - local work_id="$2" - local kinds_csv="${3:-}" - _wf_ensure_paths_lib || return 1 - local ledger stage - ledger="$(sdlc_ledger "${root}")" - stage="$(sdlc_stage "${root}")" - python3 - </dev/null - return $? - fi - python3 -c 'import sdlc_engine' 2>/dev/null -} - -_wf_run_python_gate() { - local phase="$1" - local work_id="$2" - if [[ -d "${SDLC_ROOT}/engine/src/sdlc_engine" ]]; then - PYTHONPATH="${SDLC_ROOT}/engine/src${PYTHONPATH:+:${PYTHONPATH}}" \ - python3 -m sdlc_engine --root "${SDLC_ROOT}" gate --phase "${phase}" --work-id "${work_id}" - return $? - fi - python3 -m sdlc_engine --root "${SDLC_ROOT}" gate --phase "${phase}" --work-id "${work_id}" -} - -_wf_gate_ledger_files() { - if _wf_ensure_paths_lib 2>/dev/null && declare -F sdlc_ledger >/dev/null 2>&1; then - sdlc_ledger "${SDLC_ROOT}" - printf '\n' - sdlc_stage "${SDLC_ROOT}" - printf '\n' - return 0 - fi - local home - home="$(_wf_home_path)" - printf '%s\n' "${home}/spdd/memory/lessons.jsonl" - printf '%s\n' "${home}/.sdlc/staged/lessons.jsonl" -} - -_wf_gate_ledger_has_record() { - # $1 work id; $2 mode: "any" (default), "verify" (I1 receipt), or "retro" - local work_id="$1" - local mode="${2:-any}" - local file matches - while IFS= read -r file; do - [[ -n "${file}" && -f "${file}" ]] || continue - matches="$(grep -E "\"work_id\"[[:space:]]*:[[:space:]]*\"${work_id}\"" "${file}" 2>/dev/null || true)" - [[ -n "${matches}" ]] || continue - if [[ "${mode}" == "retro" ]]; then - if printf '%s\n' "${matches}" \ - | grep -Eq '"kind"[[:space:]]*:[[:space:]]*"(decision|pitfall|pattern)"'; then - return 0 - fi - elif [[ "${mode}" == "verify" ]]; then - if printf '%s\n' "${matches}" \ - | grep -Eq '"verify"' \ - && printf '%s\n' "${matches}" \ - | grep -Eq '"command"[[:space:]]*:[[:space:]]*"[^"]+"' \ - && printf '%s\n' "${matches}" \ - | grep -Eq '"exit"[[:space:]]*:' \ - && printf '%s\n' "${matches}" \ - | grep -Eq '"result"[[:space:]]*:[[:space:]]*"(pass|fail)"'; then - return 0 - fi - else - return 0 - fi - done < <(_wf_gate_ledger_files) - return 1 -} - -_wf_gate_has_skip() { - local work_id="$1" - local phase="$2" - [[ -n "$(_wf_read_state_var "$(_wf_state_file "${work_id}")" "skip_${phase}")" ]] -} - -sdlc_workflow_gate() { - local phase="$1" - local work_id="${2:-}" - - if [[ -z "${phase}" ]]; then - echo "sdlc_workflow_gate: usage: gate [--work-id ID]" >&2 - return 2 - fi - if ! _wf_valid_phase "${phase}"; then - echo "sdlc_workflow_gate: unknown phase '${phase}'" >&2 - return 2 - fi - if [[ -z "${work_id}" ]]; then - work_id="$(sdlc_get_pointer)" - fi - if [[ -z "${work_id}" ]]; then - echo "sdlc_workflow_gate: no Work ID (pass --work-id or set the pointer via claim/resume)" >&2 - return 2 - fi - - if _wf_python_gate_available; then - _wf_run_python_gate "${phase}" "${work_id}" - return $? - fi - - # Shell fallback — not the Milestone 2 SUT (REF-001). Used when Python is - # unavailable or SDLC_GATE_ENGINE=shell. Do not treat a pass here as C-COMPLY. - local home req canvas - local -a failures=() - home="$(_wf_home_path)" - req="$(_wf_requirement_path "${work_id}")" - canvas="${home}/spdd/canvas/${work_id}.md" - local requirement_msg="requirement missing: create requirements/milestones/${work_id}.md first (requirements come before the REASONS canvas)" - local canvas_msg="canvas missing: create spdd/canvas/${work_id}.md via /sdlc-spdd-plan (the requirement and analysis come first)" - - case "${phase}" in - init) ;; - analysis) - [[ -n "${req}" ]] || failures+=("${requirement_msg}") - ;; - plan) - [[ -n "${req}" ]] || failures+=("${requirement_msg}") - if [[ ! -f "${home}/spdd/analysis/${work_id}-analysis.md" ]] \ - && ! _wf_gate_has_skip "${work_id}" analysis; then - failures+=("analysis missing: create spdd/analysis/${work_id}-analysis.md via /sdlc-spdd-analysis (or record an explicit skip: ./scripts/sdlc.sh skip analysis --reason \"...\")") - fi - ;; - architect) - [[ -f "${canvas}" ]] || failures+=("${canvas_msg}") - [[ -n "${req}" ]] || failures+=("${requirement_msg}") - ;; - code) - [[ -f "${canvas}" ]] || failures+=("${canvas_msg}") - if [[ -f "${canvas}" ]] \ - && ! grep -Eqi 'ready[[:space:]]+for[[:space:]]+coding' "${canvas}" 2>/dev/null; then - failures+=("canvas not Ready For Coding: run /sdlc-spdd-architect on spdd/canvas/${work_id}.md and mark it Ready For Coding before coding") - fi - [[ -n "${req}" ]] || failures+=("${requirement_msg}") - ;; - api-test|review) - [[ -f "${canvas}" ]] || failures+=("${canvas_msg}") - if ! _wf_gate_ledger_has_record "${work_id}" any; then - failures+=("no ledger evidence for ${work_id}: stage progress via ./scripts/sdlc.sh capture before ${phase}") - elif ! _wf_gate_ledger_has_record "${work_id}" verify; then - failures+=("no Validation receipt for ${work_id}: a dummy lesson is not enough; capture command/exit/result (run Validation) before ${phase} (skipping tests does not open ${phase})") - fi - ;; - prompt-update) - [[ -f "${canvas}" ]] || failures+=("${canvas_msg}") - ;; - retro) - if [[ ! -f "${home}/spdd/reviews/${work_id}-review.md" ]] \ - && ! _wf_gate_has_skip "${work_id}" review; then - failures+=("review missing: create spdd/reviews/${work_id}-review.md via /sdlc-spdd-review (or record an explicit skip: ./scripts/sdlc.sh skip review --reason \"...\")") - fi - ;; - sync) - if ! _wf_gate_ledger_has_record "${work_id}" retro \ - && ! _wf_gate_has_skip "${work_id}" retro; then - failures+=("no retro lesson for ${work_id}: capture a decision/pitfall/pattern via ./scripts/sdlc.sh capture (or record an explicit skip: ./scripts/sdlc.sh skip retro --reason \"...\") before sync") - fi - ;; - esac - - if ((${#failures[@]} == 0)); then - echo "gate ${phase}: OK for ${work_id}" - _wf_print_advisory_gate_rows "${phase}" - return 0 - fi - echo "gate ${phase}: BLOCKED for ${work_id}" >&2 - local failure - for failure in "${failures[@]}"; do - echo " - ${failure}" >&2 - done - _wf_print_advisory_gate_rows "${phase}" >&2 - return 1 -} - -_wf_progress_evidence_for_work() { - local root="$1" - local work_id="$2" - _wf_ensure_paths_lib || return 0 - local ledger stage - ledger="$(sdlc_ledger "${root}")" - stage="$(sdlc_stage "${root}")" - python3 - </dev/null; then - inferred="code" - fi - fi - if [[ -n "${progress_evidence}" ]]; then - inferred="code" - fi - if [[ -f "${review}" ]]; then - inferred="review" - fi - if _wf_has_retro_evidence "${work_id}"; then - inferred="retro" - fi - if [[ -f "${sync_log}" ]]; then - inferred="sync" - fi - - local session_file - _wf_ensure_paths_lib || true - if declare -F sdlc_sessions_dir >/dev/null 2>&1; then - session_file="$(sdlc_sessions_dir "${root}")/current-session.md" - else - session_file="$(_wf_home_path)/.sdlc/sessions/current-session.md" - fi - 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 session_idx - stored_idx="$(_wf_phase_index "${inferred}")" - session_idx="$(_wf_phase_index "${session_phase}")" - if (( session_idx > stored_idx )); then - inferred="${session_phase}" - fi - fi - fi - - echo "${inferred}" -} - -_wf_infer_gates_from_artifacts() { - local work_id="$1" - local root="${SDLC_ROOT}" - local home - home="$(_wf_home_path)" - local req analysis canvas review sync_log progress_evidence - local readiness="" - - req="$(_wf_requirement_path "${work_id}")" - analysis="${home}/spdd/analysis/${work_id}-analysis.md" - canvas="${home}/spdd/canvas/${work_id}.md" - review="${home}/spdd/reviews/${work_id}-review.md" - sync_log="${home}/spdd/sync/${work_id}-sync.md" - progress_evidence="$(_wf_progress_evidence_for_work "${root}" "${work_id}")" - - if [[ -n "${req}" ]]; then - echo "requirement_documented=passed" - fi - if [[ -f "${canvas}" ]]; then - echo "canvas_exists=passed" - fi - if [[ -f "${canvas}" ]] && declare -F canvas_readiness >/dev/null 2>&1; then - readiness="$(canvas_readiness "${canvas}")" - fi - case "${readiness}" in - ready-for-coding|reviewed|complete) - echo "architect_review=passed" - echo "operations_task_sized=passed" - ;; - esac - if [[ -z "${readiness}" && -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 [[ -n "${progress_evidence}" ]]; then - echo "code_maps_to_ops=passed" - echo "tests_updated=passed" - fi - if [[ -f "${review}" ]]; then - echo "review_completed=passed" - echo "safeguards_checked=passed" - fi - if _wf_has_retro_evidence "${work_id}"; then - echo "retro_completed=passed" - fi - if [[ -f "${sync_log}" ]]; then - echo "canvas_synced=passed" - fi - return 0 -} - -_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_canvas_path() { - local work_id="$1" - local canvas - canvas="$(_wf_home_path)/spdd/canvas/${work_id}.md" - if [[ -f "${canvas}" ]]; then - printf '%s' "${canvas}" - fi - return 0 -} - -_wf_infer_next_operation() { - local work_id="$1" - local canvas - canvas="$(_wf_canvas_path "${work_id}")" - [[ -n "${canvas}" ]] || return 0 - - # Use [0-9][0-9] (not {2}) — mawk does not support brace quantifiers. - # Stop matching Status once a ## section boundary is hit so empty - # "Final Status / Status:" lines do not keep the last T## incomplete. - awk ' - BEGIN { op = ""; have_status = 0; complete = 0 } - /^##[[:space:]]/ { - if (op != "" && (!have_status || !complete)) { - print op - exit - } - op = "" - have_status = 0 - complete = 0 - next - } - /^### T[0-9][0-9]/ { - if (op != "" && (!have_status || !complete)) { - print op - exit - } - op = $2 - sub(/-.*/, "", op) - have_status = 0 - complete = 0 - next - } - op != "" && /^-[[:space:]]*[Ss]tatus:[[:space:]]*/ { - have_status = 1 - status = tolower($0) - sub(/^-[[:space:]]*status:[[:space:]]*/, "", status) - gsub(/^[[:space:]]+|[[:space:]]+$/, "", status) - if (status ~ /^(complete|done|skipped)$/) { - 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() { - echo "./sdlc-spdd/scripts/sdlc.sh" -} - -_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_sync_operation "${work_id}" "${resolved}" - _wf_log_history "${work_id}" sync "phase=${resolved} operation=$(_wf_read_state_var "${file}" operation)" -} - -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)" - if [[ -n "${milestone}" ]]; then - _wf_set_state_var "${work_id}" milestone "${milestone}" - fi - _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)" - if [[ "${phase}" != "resume" ]]; then - _wf_set_state_var "${work_id}" phase "${phase}" - fi - _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_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_HOME:-${SDLC_ROOT}/sdlc-spdd}/scripts/capture-session-memory.sh" - 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_complete() { - local have_cmd=0 - local have_exit=0 - local have_result=0 - local -a passthrough=() - local has_phase=0 - - while [[ $# -gt 0 ]]; do - case "$1" in - --verify-command) - have_cmd=1 - passthrough+=("$1" "${2:-}") - shift 2 - ;; - --verify-exit) - have_exit=1 - passthrough+=("$1" "${2:-}") - shift 2 - ;; - --verify-result) - have_result=1 - passthrough+=("$1" "${2:-}") - shift 2 - ;; - --phase) - has_phase=1 - passthrough+=("$1" "${2:-}") - shift 2 - ;; - *) - passthrough+=("$1") - shift - ;; - esac - done - - if [[ "${have_cmd}" -ne 1 || "${have_exit}" -ne 1 || "${have_result}" -ne 1 ]]; then - echo "sdlc_workflow_complete: verify receipt required (command, exit, pass/fail). Refuse complete without it." >&2 - echo "Pass --verify-command, --verify-exit, and --verify-result=pass." >&2 - return 1 - fi - if [[ "${has_phase}" -ne 1 ]]; then - passthrough+=(--phase code) - fi - sdlc_workflow_capture --complete "${passthrough[@]}" -} - -sdlc_workflow_resume() { - local work_id="$1" - 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 - return 2 - fi - - if declare -F sdlc_team_check_claim >/dev/null 2>&1; then - # Skip when claim already checked (avoids duplicate "Taking over…" on claim --force) - if [[ "${_SDLC_TEAM_CLAIM_CHECKED:-0}" != "1" ]]; then - sdlc_team_check_claim "${work_id}" "${force_claim}" || return $? - fi - 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}")" - if [[ "${resolved_phase}" == "code" ]]; then - local readiness - readiness="$(_wf_work_readiness "${work_id}")" - if [[ -n "${readiness}" ]] && declare -F readiness_allows_coding >/dev/null 2>&1 \ - && ! readiness_allows_coding "${readiness}"; then - echo "Note: canvas readiness is '${readiness}' — not Ready For Coding; prefer /sdlc-spdd-architect before coding." - fi - fi - echo "Quick check: $(_wf_shell_helper_path) 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" "${team_note}" - echo "Team: commit spdd/memory/registry.jsonl to share this claim." - fi -} - -sdlc_workflow_advance() { - local to_phase="${1:-}" - local force="${2:-0}" - 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 readiness - 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 - - # Architecture-first: refuse entering code when canvas readiness blocks coding. - if [[ "${next}" == "code" && "${force}" != "1" ]]; then - readiness="$(_wf_work_readiness "${work_id}")" - if [[ -n "${readiness}" ]] && declare -F readiness_allows_coding >/dev/null 2>&1 \ - && ! readiness_allows_coding "${readiness}"; then - echo "sdlc_workflow_advance: canvas readiness is '${readiness}' — not Ready For Coding." >&2 - echo "Run /sdlc-spdd-architect (or prompt-update), then retry. Override: advance --force" >&2 - return 3 - 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}" - 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: $(_wf_shell_helper_path) next" -} - -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 - 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 spdd/memory/registry.jsonl to share shelf status." - fi - echo "Shelved ${work_id}" - echo "Reason: ${reason}" - echo "Resume later: ./scripts/sdlc.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})" - if [[ -n "${rs}" ]]; then - echo " reason: ${rs}" - fi - 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 - if [[ -n "${milestone}" ]]; then - echo "Milestone: ${milestone}" - fi - if [[ -n "${last_session}" ]]; then - echo "Last session: ${last_session}" - fi - if [[ -n "${last_capture}" ]]; then - echo "Last capture: ${last_capture}" - fi - - 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 "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 (skip nested re-source from team registry) -if [[ "${BASH_SOURCE[0]}" == "${0}" && "${_SDLC_WORKFLOW_LOAD_DEPTH}" -eq 1 ]]; then - cmd="${1:-status}" - shift || true - 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 - 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 - phase="" - 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}" "${force_claim}" - ;; - advance|/sdlc-workflow-advance) - to="" - force_advance=0 - while [[ $# -gt 0 ]]; do - case "$1" in - --to) to="${2:-}"; shift 2 ;; - --force) force_advance=1; shift ;; - *) to="${1}"; shift ;; - esac - done - sdlc_workflow_advance "${to}" "${force_advance}" - ;; - 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}" - ;; - gate|/sdlc-workflow-gate) - phase="" - work_id="" - while [[ $# -gt 0 ]]; do - case "$1" in - --work-id) work_id="${2:-}"; shift 2 ;; - --phase) phase="${2:-}"; shift 2 ;; - *) - if [[ -z "${phase}" && "$1" != -* ]]; then - phase="$1" - fi - shift - ;; - esac - done - sdlc_workflow_gate "${phase}" "${work_id}" - ;; - list-shelved|/sdlc-workflow-list-shelved) - sdlc_workflow_list_shelved - ;; - capture|/sdlc-workflow-capture) - sdlc_workflow_capture "$@" - ;; - complete|/sdlc-workflow-complete) - sdlc_workflow_complete "$@" - ;; - accept|/sdlc-accept-lessons) - accept_script="${SDLC_HOME:-${SDLC_ROOT}/sdlc-spdd}/scripts/accept-lessons.sh" - if [[ ! -x "${accept_script}" ]]; then - echo "accept-lessons.sh not found" >&2 - exit 1 - fi - "${accept_script}" --target "${SDLC_ROOT}" "$@" - ;; - 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="" - 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}" "${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 - fi - ;; - archive|/sdlc-archive) - dry=0 - force=0 - all=0 - work_id="" - while [[ $# -gt 0 ]]; do - case "$1" in - --dry-run) dry=1; shift ;; - --force) force=1; shift ;; - --all|--all-eligible) all=1; shift ;; - --work-id) work_id="${2:-}"; shift 2 ;; - *) - if [[ -z "${work_id}" && "$1" != -* ]]; then - work_id="$1" - fi - shift - ;; - esac - done - if ! declare -F sdlc_team_archive_work >/dev/null 2>&1; then - echo "team registry not installed" >&2 - exit 1 - fi - if [[ "${all}" -eq 1 ]]; then - sdlc_team_archive_eligible "${dry}" - else - sdlc_team_archive_work "${work_id}" "${dry}" "${force}" - 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|complete|accept|resume|advance|gate|skip|shelf|sync|sync-team|team|list-work|claim|release|archive|list-shelved|help} ..." >&2 - echo "Try: $0 help" >&2 - exit 2 - ;; - esac -fi diff --git a/sdlc-spdd/scripts/verify-project-install.sh b/sdlc-spdd/scripts/verify-project-install.sh index efbab321..26f0e0be 100755 --- a/sdlc-spdd/scripts/verify-project-install.sh +++ b/sdlc-spdd/scripts/verify-project-install.sh @@ -103,6 +103,12 @@ check_path() { return 1 fi ;; + absent) + if [[ ! -e "${full}" ]]; then + echo " ok ${label}: ${path} (absent)" + return 0 + fi + ;; glob) shopt -s nullglob local matches=("${TARGET}"/${path}) @@ -178,9 +184,9 @@ run_part "Framework context (harness and skills)" \ run_part "Workflow CLI and docs" \ Runtime "workflow scripts directory" "${HOME_REL}/scripts" dir \ Runtime "workflow helper script" "${HOME_REL}/scripts/sdlc.sh" executable \ - Runtime "pointer manager script" "${HOME_REL}/scripts/sdlc-pointer.sh" executable \ - Runtime "workflow manager script" "${HOME_REL}/scripts/sdlc-workflow.sh" executable \ - Runtime "team registry script" "${HOME_REL}/scripts/sdlc-team-registry.sh" executable \ + Runtime "retired pointer manager" "${HOME_REL}/scripts/sdlc-pointer.sh" absent \ + Runtime "retired workflow manager" "${HOME_REL}/scripts/sdlc-workflow.sh" absent \ + Runtime "retired team registry" "${HOME_REL}/scripts/sdlc-team-registry.sh" absent \ Runtime "start session script" "${HOME_REL}/scripts/start-agent-session.sh" executable \ Runtime "capture memory script" "${HOME_REL}/scripts/capture-session-memory.sh" executable \ Runtime "accept lessons script" "${HOME_REL}/scripts/accept-lessons.sh" executable \ diff --git a/templates/agent-context/sdlc-pointer.sh b/templates/agent-context/sdlc-pointer.sh deleted file mode 100755 index b3a3055d..00000000 --- a/templates/agent-context/sdlc-pointer.sh +++ /dev/null @@ -1,127 +0,0 @@ -#!/usr/bin/env bash -# Simple pointer manager for SDLC chores/tasks -# Usage: -# source sdlc-spdd/scripts/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 "" -- -# -# Default storage: sdlc-spdd/.sdlc/pointer. - -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)}" -_paths_lib="${SDLC_ROOT}/sdlc-spdd/scripts/lib/paths.sh" -if [[ -f "${_paths_lib}" ]]; then - # shellcheck source=/dev/null - source "${_paths_lib}" - SDLC_DIR="${SDLC_DIR:-$(sdlc_runtime_dir "${SDLC_ROOT}")}" -else - SDLC_DIR="${SDLC_DIR:-${SDLC_HOME:-${SDLC_ROOT}/sdlc-spdd}/.sdlc}" -fi -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 diff --git a/templates/agent-context/sdlc-team-registry.sh b/templates/agent-context/sdlc-team-registry.sh deleted file mode 100755 index f0093da8..00000000 --- a/templates/agent-context/sdlc-team-registry.sh +++ /dev/null @@ -1,927 +0,0 @@ -#!/usr/bin/env bash -# Team-visible Work ID registry — committed coordination layer on top of local .sdlc/ state. -# -# Local pointer (sdlc-spdd/.sdlc/pointer) stays machine-private. -# spdd/memory/registry.jsonl 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" -_paths_lib="${SDLC_ROOT}/sdlc-spdd/scripts/lib/paths.sh" -if [[ -f "${_paths_lib}" ]]; then - # shellcheck source=/dev/null - source "${_paths_lib}" -fi - -SDLC_TEAM_REGISTRY_JSONL="$(sdlc_registry "${SDLC_ROOT}" 2>/dev/null || printf '%s/spdd/memory/registry.jsonl' "${SDLC_HOME:-${SDLC_ROOT}/sdlc-spdd}")" -SDLC_TEAM_REGISTRY_LOCK="${SDLC_DIR:-${SDLC_HOME:-${SDLC_ROOT}/sdlc-spdd}/.sdlc}/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 home canvas - if declare -F sdlc_home >/dev/null 2>&1; then - home="$(sdlc_home "${root}")" - else - home="${SDLC_HOME:-${root}/sdlc-spdd}" - fi - canvas="${home}/spdd/canvas/${work_id}.md" - [[ -f "${canvas}" ]] && printf '%s' "${canvas}" -} - -# Prints Final Status line text (empty when missing). -_team_canvas_final_status_text() { - local work_id="$1" - local canvas - canvas="$(_team_canvas_path "${work_id}")" - [[ -n "${canvas}" ]] || return 0 - awk ' - /^## Final Status/ { in_final=1; next } - /^## / { if (in_final) in_final=0 } - in_final && /^- Status:/ { - sub(/^- Status:[[:space:]]*/, "") - print - exit - } - ' "${canvas}" -} - -# Prints complete | cancelled | other based on ## Final Status. -_team_canvas_final_kind() { - local work_id="$1" - local line - line="$(_team_canvas_final_status_text "${work_id}")" - [[ -z "${line}" ]] && { printf '%s' "other"; return 0; } - line="$(printf '%s' "${line}" | tr '[:upper:]' '[:lower:]')" - if [[ "${line}" == *cancel* ]]; then - printf '%s' "cancelled" - return 0 - fi - if [[ "${line}" == *complete* ]] && [[ "${line}" != *in\ progress* ]]; then - printf '%s' "complete" - return 0 - fi - printf '%s' "other" -} - -_team_canvas_is_complete() { - local work_id="$1" - [[ "$(_team_canvas_final_kind "${work_id}")" == "complete" ]] -} - -_team_canvas_is_cancelled() { - local work_id="$1" - [[ "$(_team_canvas_final_kind "${work_id}")" == "cancelled" ]] -} - -_team_canvas_is_archivable() { - local work_id="$1" - local kind - kind="$(_team_canvas_final_kind "${work_id}")" - [[ "${kind}" == "complete" || "${kind}" == "cancelled" ]] -} - -_team_registry_note_for() { - local work_id="$1" - _team_registry_lookup_row "${work_id}" | awk -F '\t' '{ print $7; exit }' -} - -_team_registry_lookup_row() { - local work_id="$1" - _team_registry_rows | awk -F '\t' -v id="${work_id}" '$1 == id { print; exit }' -} - -_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_milestone_path() { - local work_id="$1" - local root="${SDLC_ROOT}" - if declare -F sdlc_home >/dev/null 2>&1; then - root="$(sdlc_home "${SDLC_ROOT}")" - fi - local path dir - # Prefer subdirectory stubs when present. - shopt -s nullglob - for dir in "${root}"/requirements/milestones/milestone-*/; do - path="${dir}${work_id}.md" - if [[ -f "${path}" ]]; then - shopt -u nullglob - printf '%s' "${path}" - return 0 - fi - done - shopt -u nullglob - path="${root}/requirements/milestones/${work_id}.md" - [[ -f "${path}" ]] && printf '%s' "${path}" -} - -# Reads Jira key from requirement ## Jira section (- Key: ABC-123), or jira_key frontmatter. -_team_jira_from_milestone() { - local work_id="$1" - local path - path="$(_team_milestone_path "${work_id}")" - [[ -n "${path}" ]] || return 0 - local from_section - from_section="$(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}")" - if [[ -n "${from_section}" ]]; then - printf '%s' "${from_section}" - return 0 - fi - awk ' - NR==1 && /^---[[:space:]]*$/ { in_fm=1; next } - in_fm && /^---[[:space:]]*$/ { exit } - in_fm && /^jira_key:[[:space:]]*/ { - sub(/^jira_key:[[: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_jira_from_note() { - local note="${1:-}" - local token - for token in ${note}; do - if [[ "${token}" == jira:* ]]; then - printf '%s' "${token#jira:}" - return 0 - fi - done -} - -# Resolve tracker key for a Work ID. -# Prints: | draft | missing -# Prefer claim-note jira:TOKEN, then requirement Key, else draft if ## Jira exists. -sdlc_team_jira_status() { - local work_id="${1:-}" - [[ -n "${work_id}" ]] || { printf 'missing'; return 0; } - local note="" key="" - if [[ -f "${SDLC_TEAM_REGISTRY_JSONL}" ]]; then - note="$(_team_registry_note_for "${work_id}" || true)" - key="$(_team_jira_from_note "${note}")" - fi - if [[ -n "${key}" ]]; then - printf '%s' "${key}" - return 0 - fi - key="$(_team_jira_from_milestone "${work_id}" || true)" - if [[ -n "${key}" ]]; then - printf '%s' "${key}" - return 0 - fi - if _team_milestone_has_jira_draft "${work_id}"; then - printf 'draft' - return 0 - fi - printf 'missing' -} - -# Agent-facing instruction when Jira is unset. Empty when a key is known or -# SDLC_SESSION_ASK_JIRA=0. Use in Resume Prompt / next / whereami. -sdlc_team_jira_ask_prompt() { - local work_id="${1:-}" - [[ -n "${work_id}" ]] || return 0 - [[ "${SDLC_SESSION_ASK_JIRA:-1}" == "1" ]] || return 0 - local status - status="$(sdlc_team_jira_status "${work_id}")" - case "${status}" in - missing) - printf '%s\n' "Tracker link: Jira key is missing for ${work_id}. Ask the user for the issue key (or confirm none applies) before coding or claiming tracker progress. Then run \`./scripts/sdlc.sh claim ${work_id} --jira KEY\` (or set \`- Key:\` under \`## Jira\` on the requirement and re-claim). Do not invent a key." - ;; - draft) - printf '%s\n' "Tracker link: Jira draft exists for ${work_id} but \`- Key:\` is unset. Ask the user for the issue key (or confirm none applies) before coding or claiming tracker progress. Then run \`./scripts/sdlc.sh claim ${work_id} --jira KEY\` (or set \`- Key:\` under \`## Jira\` and re-claim). Do not invent a key." - ;; - esac -} - -_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 kind cur_status cur_phase cur_op cur_note target_status note_token - _team_registry_init - while IFS= read -r work_id; do - [[ -z "${work_id}" ]] && continue - kind="$(_team_canvas_final_kind "${work_id}")" - case "${kind}" in - complete) target_status="done"; note_token="canvas Final Status: Complete" ;; - cancelled) target_status="cancelled"; note_token="canvas Final Status: Cancelled" ;; - *) continue ;; - esac - cur_status="$(awk -F '\t' -v id="${work_id}" '$1 == id { print $2; exit }' <<< "$(_team_registry_rows)")" - # Do not reopen archived rows from canvas sync. - [[ "${cur_status}" == "archived" ]] && continue - if [[ -z "${cur_status}" ]]; then - sdlc_team_register "${work_id}" "${target_status}" "sync" "" "${note_token}" - continue - fi - [[ "${cur_status}" == "${target_status}" ]] && continue - cur_phase="$(awk -F '\t' -v id="${work_id}" '$1 == id { print $3; exit }' <<< "$(_team_registry_rows)")" - cur_op="$(awk -F '\t' -v id="${work_id}" '$1 == id { print $4; exit }' <<< "$(_team_registry_rows)")" - cur_note="$(_team_compose_note "$(_team_registry_note_for "${work_id}")" "" "" "" "${note_token}")" - sdlc_team_register "${work_id}" "${target_status}" "${cur_phase}" "${cur_op}" "${cur_note}" - done < <(sdlc_team_discover_work_ids) -} - -_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 "$(dirname "${SDLC_TEAM_REGISTRY_JSONL}")" "${SDLC_DIR:-${SDLC_HOME:-${SDLC_ROOT}/sdlc-spdd}/.sdlc}" - if [[ ! -f "${SDLC_TEAM_REGISTRY_JSONL}" ]]; then - : > "${SDLC_TEAM_REGISTRY_JSONL}" - fi -} - -_team_registry_read_events() { - _team_registry_init - if [[ -f "${SDLC_TEAM_REGISTRY_JSONL}" ]] && [[ -s "${SDLC_TEAM_REGISTRY_JSONL}" ]]; then - python3 - </dev/null 2>&1; then - ( - flock -x 200 || exit 1 - "$@" - ) 200>"${SDLC_TEAM_REGISTRY_LOCK}" - else - "$@" - fi -} - - -_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 event="${6:-update}" - local owner updated - owner="$(_team_owner)" - updated="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" - _team_registry_init - if [[ -z "${note}" ]]; then - note="$(_team_registry_note_for "${work_id}")" - fi - local payload - payload="$(python3 - </dev/null 2>&1; then - sdlc_append_jsonl "${SDLC_TEAM_REGISTRY_JSONL}" "${payload}" - else - printf '%s\n' "${payload}" >> "${SDLC_TEAM_REGISTRY_JSONL}" - fi -} - -sdlc_team_register() { - local work_id="$1" - local status="$2" - local phase="${3:-}" - local operation="${4:-}" - local note="${5:-}" - local event="${6:-update}" - 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}" "${event}" - local hook_owner hook_updated hook_note row - row="$(_team_registry_lookup_row "${work_id}")" - hook_owner="$(awk -F '\t' '{ print $5 }' <<< "${row}")" - hook_updated="$(awk -F '\t' '{ print $6 }' <<< "${row}")" - hook_note="$(awk -F '\t' '{ print $7 }' <<< "${row}")" - _team_run_hook "${work_id}" "${status}" "${phase}" "${operation}" \ - "${hook_owner}" "${hook_updated}" "${hook_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 }' <<< "$(_team_registry_rows)")" - status="$(awk -F '\t' -v id="${work_id}" '$1 == id { print $2; exit }' <<< "$(_team_registry_rows)")" - updated="$(awk -F '\t' -v id="${work_id}" '$1 == id { print $6; exit }' <<< "$(_team_registry_rows)")" - [[ -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 - 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 event="update" - file="${SDLC_DIR:-${SDLC_HOME:-${SDLC_ROOT}/sdlc-spdd}/.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 - case "${status}" in - active) event="claim" ;; - shelved) event="release" ;; - archived) event="archive" ;; - done|cancelled) event="refresh" ;; - esac - sdlc_team_register "${work_id}" "${status}" "${phase}" "${operation}" "${note}" "${event}" -} - -sdlc_team_discover_work_ids() { - local root="${SDLC_ROOT}" - local home - if declare -F sdlc_home >/dev/null 2>&1; then - home="$(sdlc_home "${root}")" - else - home="${SDLC_HOME:-${root}/sdlc-spdd}" - fi - local -A seen=() - local path base - shopt -s nullglob - for path in \ - "${home}"/spdd/canvas/*.md \ - "${home}"/requirements/milestones/*.md \ - "${home}"/requirements/milestones/milestone-*/*.md; do - base="$(basename "${path}" .md)" - [[ "${base}" == "README" || "${base}" == "archive" ]] && continue - [[ "${base}" == MILESTONE-* || "${base}" == milestone-* ]] && continue - [[ -n "${base}" ]] || continue - [[ "${path}" == */archive/* || "${path}" == */archive ]] && continue - seen["${base}"]=1 - done - shopt -u nullglob - printf '%s\n' "${!seen[@]}" | sort -} - -_team_remove_path() { - local path="$1" - local dry="${2:-0}" - [[ -e "${path}" ]] || return 0 - if [[ "${dry}" -eq 1 ]]; then - echo "[dry-run] would remove ${path#${SDLC_ROOT}/}" - return 0 - fi - rm -rf "${path}" - echo "Removed ${path#${SDLC_ROOT}/}" -} - -# Remove completed/cancelled Work ID contract artifacts from the working tree. -# Keeps requirements/milestones/.md in place as historical requirement source. -# Git history retains removed files; no spdd/*/archive/ folders in storage v3. -sdlc_team_archive_work() { - local work_id="${1:-}" - local dry="${2:-0}" - local force="${3:-0}" - local root="${SDLC_ROOT}" - local kind pointer moved=0 - - if [[ -z "${work_id}" ]]; then - echo "archive: Work ID required" >&2 - return 2 - fi - - kind="$(_team_canvas_final_kind "${work_id}")" - if [[ "${force}" -ne 1 && "${kind}" != "complete" && "${kind}" != "cancelled" ]]; then - echo "archive: ${work_id} is not Complete or Cancelled (Final Status kind=${kind}). Use --force to archive anyway." >&2 - return 1 - fi - - pointer="$(sdlc_get_pointer)" - if [[ "${pointer}" == "${work_id}" ]]; then - if [[ "${dry}" -eq 1 ]]; then - echo "[dry-run] would clear pointer for ${work_id}" - else - sdlc_reset_pointer >/dev/null - echo "Cleared local pointer (was ${work_id})" - fi - fi - - local feature_src="" feature_dest="" - # Storage v3 has no per-feature mirror tree. - - local canvas_src review_src analysis_src sync_src home - if declare -F sdlc_home >/dev/null 2>&1; then - home="$(sdlc_home "${root}")" - else - home="${SDLC_HOME:-${root}/sdlc-spdd}" - fi - canvas_src="${home}/spdd/canvas/${work_id}.md" - analysis_src="${home}/spdd/analysis/${work_id}-analysis.md" - review_src="${home}/spdd/reviews/${work_id}-review.md" - sync_src="${home}/spdd/sync/${work_id}-sync.md" - [[ -f "${canvas_src}" ]] && _team_remove_path "${canvas_src}" "${dry}" && moved=1 - [[ -f "${analysis_src}" ]] && _team_remove_path "${analysis_src}" "${dry}" && moved=1 - [[ -f "${review_src}" ]] && _team_remove_path "${review_src}" "${dry}" && moved=1 - [[ -f "${sync_src}" ]] && _team_remove_path "${sync_src}" "${dry}" && moved=1 - - local session_dir - if declare -F sdlc_sessions_dir >/dev/null 2>&1; then - session_dir="$(sdlc_sessions_dir "${root}")" - else - session_dir="${home}/.sdlc/sessions" - fi - if [[ -d "${session_dir}" ]]; then - local sess - shopt -s nullglob - for sess in "${session_dir}"/*"${work_id}"*; do - [[ -f "${sess}" ]] || continue - [[ "$(basename "${sess}")" == "current-session.md" ]] && continue - _team_remove_path "${sess}" "${dry}" && moved=1 - done - shopt -u nullglob - fi - - local state_src="${SDLC_DIR:-${home}/.sdlc}/workflows/${work_id}.state" - if [[ -f "${state_src}" ]]; then - _team_remove_path "${state_src}" "${dry}" && moved=1 - fi - - if [[ "${dry}" -eq 1 ]]; then - echo "[dry-run] would mark ${work_id} archived in registry.jsonl" - return 0 - fi - - local note_token="archived:${kind}" - [[ "${force}" -eq 1 && "${kind}" == "other" ]] && note_token="archived:forced" - sdlc_team_register "${work_id}" "archived" "archive" "" "${note_token}" "archive" - if [[ "${moved}" -eq 0 ]]; then - echo "archive: ${work_id} marked archived (no movable artifacts found; milestone left in place)" - else - echo "Archived ${work_id} (${kind}). Commit deletions + spdd/memory/registry.jsonl." - fi - echo "Left in place: requirements/milestones/${work_id}.md (if present)." - echo "Left in place: spdd/memory/lessons.jsonl (archive never truncates the lessons ledger)." -} - -# Archive every Work ID whose canvas Final Status is Complete or Cancelled. -sdlc_team_archive_eligible() { - local dry="${1:-0}" - local work_id count=0 cur - _team_registry_init - while IFS= read -r work_id; do - [[ -z "${work_id}" ]] && continue - _team_canvas_is_archivable "${work_id}" || continue - cur="$(awk -F '\t' -v id="${work_id}" '$1 == id { print $2; exit }' <<< "$(_team_registry_rows)" 2>/dev/null || true)" - [[ "${cur}" == "archived" ]] && continue - if sdlc_team_archive_work "${work_id}" "${dry}" 0; then - count=$((count + 1)) - fi - done < <(sdlc_team_discover_work_ids) - echo "archive: processed ${count} eligible Work ID(s)" -} - -sdlc_team_infer_work_summary() { - local work_id="$1" - local root="${SDLC_ROOT}" - local home parts=() - if declare -F sdlc_home >/dev/null 2>&1; then - home="$(sdlc_home "${root}")" - else - home="${SDLC_HOME:-${root}/sdlc-spdd}" - fi - [[ -f "${home}/spdd/canvas/${work_id}.md" ]] && parts+=("canvas") - if [[ -n "$(_team_milestone_path "${work_id}" || true)" ]]; then - parts+=("milestone") - fi - 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 - local IFS=', ' - printf '%s' "${parts[*]}" - fi -} - -sdlc_team_list_work() { - local work_id - sdlc_team_refresh_done_status - echo "Work IDs in this repository:" - echo - 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 updated stale done_hint - reg_status="available" - phase="-" - owner="-" - updated="" - reg_status="$(awk -F '\t' -v id="${work_id}" '$1 == id { print $2; exit }' <<< "$(_team_registry_rows)")" - if [[ -n "${reg_status}" ]]; then - phase="$(awk -F '\t' -v id="${work_id}" '$1 == id { print $3; exit }' <<< "$(_team_registry_rows)")" - owner="$(awk -F '\t' -v id="${work_id}" '$1 == id { print $5; exit }' <<< "$(_team_registry_rows)")" - updated="$(awk -F '\t' -v id="${work_id}" '$1 == id { print $6; exit }' <<< "$(_team_registry_rows)")" - phase="${phase:--}" - owner="${owner:--}" - stale="$(_team_stale_label "${updated}" "${reg_status}")" - reg_status="${reg_status}${stale}" - else - reg_status="available" - fi - 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: ./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 - echo "Your local pointer: (none)" - fi - echo - echo "Team registry (commit spdd/memory/registry.jsonl to share):" - if _team_registry_rows | grep -q .; then - 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}")" - 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}")" - 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 %-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 sdlc-spdd/scripts/hooks/notify-team-registry.sh" - echo "Discover all Work IDs: ./scripts/sdlc.sh list-work" -} - -# Locate the workflow manager: same dir as this script (v3 installs place both -# under /scripts/), then /scripts/. -_team_workflow_script() { - local self_dir - self_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - local candidate - for candidate in \ - "${self_dir}/sdlc-workflow.sh" \ - "${SDLC_ROOT}/sdlc-spdd/scripts/sdlc-workflow.sh"; do - if [[ -f "${candidate}" ]]; then - printf '%s' "${candidate}" - return 0 - fi - done - return 1 -} - -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 - fi - sdlc_team_check_claim "${work_id}" "${force}" || return $? - local _workflow_script - if ! _workflow_script="$(_team_workflow_script)"; then - echo "sdlc_team_claim: sdlc-workflow.sh not installed" >&2 - 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}")" - # shellcheck source=/dev/null - source "${_workflow_script}" - # Pass force through so `sdlc.sh claim --force` can take over a foreign claim. - # Mark already-checked so resume does not print "Taking over…" a second time. - _SDLC_TEAM_CLAIM_CHECKED=1 - sdlc_workflow_resume "${work_id}" "${phase}" 1 "${force}" "${note}" - local resume_rc=$? - unset _SDLC_TEAM_CLAIM_CHECKED - (( resume_rc == 0 )) || return "${resume_rc}" - echo "Team registry updated — commit spdd/memory/registry.jsonl 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 - local _workflow_script - if ! _workflow_script="$(_team_workflow_script)"; then - echo "sdlc_team_release: sdlc-workflow.sh not installed" >&2 - return 1 - fi - # shellcheck source=/dev/null - source "${_workflow_script}" - sdlc_workflow_shelf "${reason}" - echo "Team registry updated — commit spdd/memory/registry.jsonl 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="" - 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}" "${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" - while [[ $# -gt 0 ]]; do - case "$1" in - --reason) reason="${2:-}"; shift 2 ;; - *) shift ;; - esac - done - sdlc_team_release "${reason}" - ;; - archive) - dry=0 - force=0 - all=0 - work_id="" - while [[ $# -gt 0 ]]; do - case "$1" in - --dry-run) dry=1; shift ;; - --force) force=1; shift ;; - --all|--all-eligible) all=1; shift ;; - *) - if [[ -z "${work_id}" && "$1" != -* ]]; then - work_id="$1" - fi - shift - ;; - esac - done - if [[ "${all}" -eq 1 ]]; then - sdlc_team_archive_eligible "${dry}" - else - sdlc_team_archive_work "${work_id}" "${dry}" "${force}" - fi - ;; - *) - echo "Usage: $0 {team|list-work|claim|release|archive} ..." >&2 - exit 2 - ;; - esac -fi diff --git a/templates/agent-context/sdlc-workflow.sh b/templates/agent-context/sdlc-workflow.sh deleted file mode 100755 index d7529e9a..00000000 --- a/templates/agent-context/sdlc-workflow.sh +++ /dev/null @@ -1,2046 +0,0 @@ -#!/usr/bin/env bash -# Workflow state manager for SDLC-SPDD — tracks phase, gates, shelf/resume on top of sdlc-pointer.sh -# -# Usage: -# source sdlc-spdd/scripts/sdlc-workflow.sh -# sdlc_workflow_status -# sdlc_workflow_resume WORK_ID [--phase PHASE] -# sdlc_workflow_advance [--to PHASE] -# sdlc_workflow_gate PHASE [WORK_ID] -# sdlc_workflow_skip PHASE [--reason TEXT] -# sdlc_workflow_shelf [--reason TEXT] -# sdlc_workflow_sync [--work-id WORK_ID] -# -# State: sdlc-spdd/.sdlc/workflows/.state and history - -if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then - set -euo pipefail -fi - -# Track nested source depth so CLI does not re-enter when this file is sourced -# from sdlc_team_claim/release while already executing as the main script -# (sdlc.sh exec's this file with an absolute $0, which makes BASH_SOURCE==$0 -# even for nested source). -_SDLC_WORKFLOW_LOAD_DEPTH=$(( ${_SDLC_WORKFLOW_LOAD_DEPTH:-0} + 1 )) - -_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 - -# Optional readiness helper from the v3 home. -_wf_readiness_lib="${SDLC_HOME:-${SDLC_ROOT}/sdlc-spdd}/scripts/lib/readiness.sh" -if [[ -f "${_wf_readiness_lib}" ]]; then - # shellcheck source=/dev/null - source "${_wf_readiness_lib}" -fi - -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 -} - -_wf_work_readiness() { - local work_id="$1" - local canvas - canvas="$(_wf_canvas_path "${work_id}")" - [[ -n "${canvas}" ]] || return 0 - if declare -F canvas_readiness >/dev/null 2>&1; then - canvas_readiness "${canvas}" - fi -} - -_wf_is_quiet() { - local q - q="$(printf '%s' "${SDLC_QUIET:-}" | tr '[:upper:]' '[:lower:]')" - [[ "${q}" == "1" || "${q}" == "true" || "${q}" == "yes" || "${q}" == "on" ]] \ - || [[ -f "${SDLC_HOME:-${SDLC_ROOT:-.}/sdlc-spdd}/harness/quiet-mode.md" ]] -} - -_wf_quiet_blurb() { - # Keep in sync with sdlc_engine.quiet.quiet_resume_blurb (default, non-guide). - echo "Quiet mode: load resolved context and continue the product task. Skip recommended /sdlc-spdd-* T## dogfood commands unless explicitly requested." -} - -sdlc_workflow_recommended_command() { - local phase="${1:-init}" - local work_id="${2:-}" - local operation="${3:-}" - local readiness="" - # Quiet / product-test mode (#91): no T## dogfood gravity. - if _wf_is_quiet; then - _wf_quiet_blurb - return 0 - fi - if [[ -z "${operation}" && -n "${work_id}" ]]; then - operation="$(_wf_resolve_operation "${work_id}" "${phase}")" - fi - if [[ -n "${work_id}" ]] && declare -F canvas_readiness >/dev/null 2>&1; then - readiness="$(_wf_work_readiness "${work_id}")" - fi - # Architecture-first: do not recommend /sdlc-spdd-code when readiness blocks coding. - if [[ "${phase}" == "code" && -n "${readiness}" ]]; then - if declare -F readiness_allows_coding >/dev/null 2>&1 \ - && ! readiness_allows_coding "${readiness}"; then - echo "/sdlc-spdd-architect @spdd/canvas/${work_id}.md # readiness=${readiness} — not Ready For Coding" - return 0 - fi - 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 ${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 .sdlc/sessions/current-session.md, then choose the next phase command." ;; - *) echo "/sdlc-spdd-init" ;; - 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 - echo "./sdlc-spdd/scripts/start-agent-session.sh --target . --work-id ${work_id} --phase ${phase}" -} - -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 "${helper} capture --summary \"\"" - return 0 - fi - if [[ -z "${phase}" ]]; then - phase="$(_wf_read_state_var "$(_wf_state_file "${work_id}")" phase resume)" - fi - echo "${helper} capture --phase ${phase} --summary \"\"" -} - -_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 -} - -# Advisory GATE_LABELS that gate_check / this fallback never enforce. -# Keep in sync with engine/src/sdlc_engine/phases.py ADVISORY_GATES + gates_for_phase. -_wf_advisory_gates_for_phase() { - case "${1:-}" in - architect) printf '%s\n' architect_review operations_task_sized ;; - code|api-test) printf '%s\n' architect_review operations_task_sized tests_updated ;; - prompt-update|sync) printf '%s\n' canvas_synced ;; - *) ;; - esac -} - -_wf_gate_label() { - local want="$1" - local gi - for ((gi = 0; gi < ${#SDLC_GATE_NAMES[@]}; gi++)); do - if [[ "${SDLC_GATE_NAMES[$gi]}" == "${want}" ]]; then - printf '%s' "${SDLC_GATE_LABELS[$gi]}" - return 0 - fi - done - printf '%s' "${want}" -} - -_wf_print_advisory_gate_rows() { - # Leftover #14: gate must show advisory rows that did not run. - local phase="$1" - local gate label - while IFS= read -r gate; do - [[ -z "${gate}" ]] && continue - label="$(_wf_gate_label "${gate}")" - if [[ "${label}" != *"(advisory)"* ]]; then - label="${label} (advisory)" - fi - echo " ~ ${label} (did not run)" - done < <(_wf_advisory_gates_for_phase "${phase}") -} - -_wf_pass_gates_for_phase() { - local work_id="$1" - local phase="$2" - local gate - local readiness="" - while IFS= read -r gate; do - [[ -z "${gate}" ]] && continue - # Leaving architect: do not auto-pass architect_review when readiness blocks coding. - # Absent readiness stays backward-compatible (gate may still pass). - if [[ "${phase}" == "architect" && "${gate}" == "architect_review" ]]; then - readiness="$(_wf_work_readiness "${work_id}")" - if [[ -n "${readiness}" ]] && declare -F readiness_allows_coding >/dev/null 2>&1 \ - && ! readiness_allows_coding "${readiness}"; then - continue - fi - fi - 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 op_title readiness - 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}")" - readiness="$(_wf_work_readiness "${work_id}")" - op_title="" - if [[ -n "${operation}" ]]; then - op_title="$(_wf_operation_title "${work_id}" "${operation}")" - fi - - local jira_status="" jira_ask="" - if declare -F sdlc_team_jira_status >/dev/null 2>&1; then - jira_status="$(sdlc_team_jira_status "${work_id}")" - fi - if declare -F sdlc_team_jira_ask_prompt >/dev/null 2>&1; then - jira_ask="$(sdlc_team_jira_ask_prompt "${work_id}")" - fi - - cat <"\` | -| Orient / status | \`./scripts/sdlc.sh next\` or \`/sdlc-spdd-whereami\` | - -$(if [[ -n "${pending}" ]]; then - echo "Pending gates:" - while IFS= read -r line; do - [[ -z "${line}" ]] && continue - echo "- ${line}" - done <<< "${pending}" -fi) -$(if [[ -n "${jira_ask}" ]]; then - echo - echo "Tracker follow-up:" - echo "- ${jira_ask}" -fi) -EOF -} - -sdlc_workflow_next() { - 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." - echo - echo "Start here:" - echo " ./scripts/sdlc.sh resume # 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 op_title readiness jira_status jira_ask - 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="" - jira_status="" - jira_ask="" - if [[ -n "${operation}" ]]; then - op_title="$(_wf_operation_title "${work_id}" "${operation}")" - fi - - # Match Python WorkflowEngine.next_text quiet contract (#91). - if _wf_is_quiet; then - echo "== SDLC: what to do now (quiet) ==" - echo "Work ID: ${work_id}" - echo "Phase: ${phase}" - echo - _wf_quiet_blurb - return 0 - fi - - echo "== SDLC: what to do now ==" - echo "Work ID: ${work_id}" - echo "Phase: ${phase} ($(( $(_wf_phase_index "${phase}") + 1 ))/${#SDLC_PHASE_ORDER[@]})" - readiness="$(_wf_work_readiness "${work_id}")" - if [[ -n "${readiness}" ]]; then - echo "Readiness: ${readiness}" - fi - if declare -F sdlc_team_jira_status >/dev/null 2>&1; then - jira_status="$(sdlc_team_jira_status "${work_id}")" - echo "Jira: ${jira_status}" - fi - if declare -F sdlc_team_jira_ask_prompt >/dev/null 2>&1; then - jira_ask="$(sdlc_team_jira_ask_prompt "${work_id}")" - fi - 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}" "${operation}")" - if [[ "${phase}" == "code" ]]; then - if [[ -n "${readiness}" ]] && declare -F readiness_allows_coding >/dev/null 2>&1 \ - && ! readiness_allows_coding "${readiness}"; then - echo - echo "Note: canvas readiness is '${readiness}' — run architect (or prompt-update) before coding." - fi - fi - if [[ -n "${jira_ask}" ]]; then - echo - echo "Tracker follow-up:" - echo " ${jira_ask}" - fi - 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_HOME:-${SDLC_ROOT}/sdlc-spdd}/scripts/start-agent-session.sh" - 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+="}" - - 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}")" - 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 '"operation_title":"%s",' "$(_wf_json_escape "${op_title}")" - printf '"readiness":"%s",' "$(_wf_json_escape "$(_wf_work_readiness "${work_id}")")" - if _wf_is_quiet; then - printf '"quiet":true,' - else - printf '"quiet":false,' - fi - 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}" - 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() { - local helper - helper="$(_wf_shell_helper_path)" - cat < [--phase PHASE] [--force] - ${helper} advance [--to PHASE] [--force] - ${helper} gate [--work-id ID] # check prerequisites to enter a phase (exit 0/1) - ${helper} skip --reason "why" - ${helper} shelf --reason "why" - ${helper} sync [--work-id ID] - ${helper} list-shelved - ${helper} team # team registry + your pointer - ${helper} list-work # all Work IDs in the repo - ${helper} claim [--force] [--branch NAME] [--pr #N] [--jira KEY] - ${helper} release --reason "why" - ${helper} sync-team # mark done/cancelled from canvas Final Status - ${helper} archive # move Complete/Cancelled work into archive/ - ${helper} archive --all # archive every eligible Complete/Cancelled Work ID - ${helper} accept [--work-id ID] [--commit] # promote staged lessons to ledger - -In chat: /sdlc-next or /sdlc-spdd-whereami -Workflow chat: /sdlc-claim, /sdlc-shelf, /sdlc-advance, /sdlc-next, /sdlc-team - -Team sharing: commit spdd/memory/registry.jsonl after claim/release/shelf/archive. -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; auto jira Key from requirements/milestones/.md on claim). - -Typical loop: - 1. ${helper} next - 2. run the assistant command (or ${helper} start) - 3. ${helper} advance - 4. ${helper} capture --summary "..." - -Code phase: next operation (T01, T02, ...) is read from the REASONS Canvas automatically. -EOF -} - -_wf_requirement_path() { - local work_id="$1" - local home - home="$(_wf_home_path)" - local path dir - shopt -s nullglob - for dir in "${home}"/requirements/milestones/milestone-*/; do - path="${dir}${work_id}.md" - if [[ -f "${path}" ]]; then - shopt -u nullglob - printf '%s' "${path}" - return 0 - fi - done - shopt -u nullglob - path="${home}/requirements/milestones/${work_id}.md" - if [[ -f "${path}" ]]; then - printf '%s' "${path}" - fi - return 0 -} - -# Extract shared lean-ledger slices for one Work ID (## WID sections + ### … WID … blocks). -_wf_ledger_section_for_work() { - local file="$1" - local work_id="$2" - [[ -f "${file}" && -n "${work_id}" ]] || return 0 - awk -v wid="${work_id}" ' - BEGIN { in_h2 = 0; in_h3 = 0 } - /^## / { - rest = $0 - sub(/^##[[:space:]]+/, "", rest) - in_h2 = (rest == wid || index(rest, wid " ") == 1 || index(rest, wid " -") == 1) - in_h3 = 0 - if (in_h2) print - next - } - /^### / { - in_h3 = (index($0, wid) > 0) - if (in_h3) print - next - } - in_h2 || in_h3 { print } - ' "${file}" -} - -_wf_paths_lib() { - local root="${SDLC_ROOT}" - local home="${SDLC_HOME:-${root}/sdlc-spdd}" - [[ -f "${home}/scripts/lib/paths.sh" ]] && printf '%s' "${home}/scripts/lib/paths.sh" -} - -_wf_ensure_paths_lib() { - if declare -F sdlc_ledger >/dev/null 2>&1; then - return 0 - fi - local lib - lib="$(_wf_paths_lib)" - [[ -n "${lib}" && -f "${lib}" ]] || return 1 - # shellcheck source=/dev/null - source "${lib}" -} - -_wf_ledger_record_count() { - local root="$1" - local work_id="$2" - local kinds_csv="${3:-}" - _wf_ensure_paths_lib || return 1 - local ledger stage - ledger="$(sdlc_ledger "${root}")" - stage="$(sdlc_stage "${root}")" - python3 - </dev/null - return $? - fi - python3 -c 'import sdlc_engine' 2>/dev/null -} - -_wf_run_python_gate() { - local phase="$1" - local work_id="$2" - if [[ -d "${SDLC_ROOT}/engine/src/sdlc_engine" ]]; then - PYTHONPATH="${SDLC_ROOT}/engine/src${PYTHONPATH:+:${PYTHONPATH}}" \ - python3 -m sdlc_engine --root "${SDLC_ROOT}" gate --phase "${phase}" --work-id "${work_id}" - return $? - fi - python3 -m sdlc_engine --root "${SDLC_ROOT}" gate --phase "${phase}" --work-id "${work_id}" -} - -_wf_gate_ledger_files() { - if _wf_ensure_paths_lib 2>/dev/null && declare -F sdlc_ledger >/dev/null 2>&1; then - sdlc_ledger "${SDLC_ROOT}" - printf '\n' - sdlc_stage "${SDLC_ROOT}" - printf '\n' - return 0 - fi - local home - home="$(_wf_home_path)" - printf '%s\n' "${home}/spdd/memory/lessons.jsonl" - printf '%s\n' "${home}/.sdlc/staged/lessons.jsonl" -} - -_wf_gate_ledger_has_record() { - # $1 work id; $2 mode: "any" (default), "verify" (I1 receipt), or "retro" - local work_id="$1" - local mode="${2:-any}" - local file matches - while IFS= read -r file; do - [[ -n "${file}" && -f "${file}" ]] || continue - matches="$(grep -E "\"work_id\"[[:space:]]*:[[:space:]]*\"${work_id}\"" "${file}" 2>/dev/null || true)" - [[ -n "${matches}" ]] || continue - if [[ "${mode}" == "retro" ]]; then - if printf '%s\n' "${matches}" \ - | grep -Eq '"kind"[[:space:]]*:[[:space:]]*"(decision|pitfall|pattern)"'; then - return 0 - fi - elif [[ "${mode}" == "verify" ]]; then - if printf '%s\n' "${matches}" \ - | grep -Eq '"verify"' \ - && printf '%s\n' "${matches}" \ - | grep -Eq '"command"[[:space:]]*:[[:space:]]*"[^"]+"' \ - && printf '%s\n' "${matches}" \ - | grep -Eq '"exit"[[:space:]]*:' \ - && printf '%s\n' "${matches}" \ - | grep -Eq '"result"[[:space:]]*:[[:space:]]*"(pass|fail)"'; then - return 0 - fi - else - return 0 - fi - done < <(_wf_gate_ledger_files) - return 1 -} - -_wf_gate_has_skip() { - local work_id="$1" - local phase="$2" - [[ -n "$(_wf_read_state_var "$(_wf_state_file "${work_id}")" "skip_${phase}")" ]] -} - -sdlc_workflow_gate() { - local phase="$1" - local work_id="${2:-}" - - if [[ -z "${phase}" ]]; then - echo "sdlc_workflow_gate: usage: gate [--work-id ID]" >&2 - return 2 - fi - if ! _wf_valid_phase "${phase}"; then - echo "sdlc_workflow_gate: unknown phase '${phase}'" >&2 - return 2 - fi - if [[ -z "${work_id}" ]]; then - work_id="$(sdlc_get_pointer)" - fi - if [[ -z "${work_id}" ]]; then - echo "sdlc_workflow_gate: no Work ID (pass --work-id or set the pointer via claim/resume)" >&2 - return 2 - fi - - if _wf_python_gate_available; then - _wf_run_python_gate "${phase}" "${work_id}" - return $? - fi - - # Shell fallback — not the Milestone 2 SUT (REF-001). Used when Python is - # unavailable or SDLC_GATE_ENGINE=shell. Do not treat a pass here as C-COMPLY. - local home req canvas - local -a failures=() - home="$(_wf_home_path)" - req="$(_wf_requirement_path "${work_id}")" - canvas="${home}/spdd/canvas/${work_id}.md" - local requirement_msg="requirement missing: create requirements/milestones/${work_id}.md first (requirements come before the REASONS canvas)" - local canvas_msg="canvas missing: create spdd/canvas/${work_id}.md via /sdlc-spdd-plan (the requirement and analysis come first)" - - case "${phase}" in - init) ;; - analysis) - [[ -n "${req}" ]] || failures+=("${requirement_msg}") - ;; - plan) - [[ -n "${req}" ]] || failures+=("${requirement_msg}") - if [[ ! -f "${home}/spdd/analysis/${work_id}-analysis.md" ]] \ - && ! _wf_gate_has_skip "${work_id}" analysis; then - failures+=("analysis missing: create spdd/analysis/${work_id}-analysis.md via /sdlc-spdd-analysis (or record an explicit skip: ./scripts/sdlc.sh skip analysis --reason \"...\")") - fi - ;; - architect) - [[ -f "${canvas}" ]] || failures+=("${canvas_msg}") - [[ -n "${req}" ]] || failures+=("${requirement_msg}") - ;; - code) - [[ -f "${canvas}" ]] || failures+=("${canvas_msg}") - if [[ -f "${canvas}" ]] \ - && ! grep -Eqi 'ready[[:space:]]+for[[:space:]]+coding' "${canvas}" 2>/dev/null; then - failures+=("canvas not Ready For Coding: run /sdlc-spdd-architect on spdd/canvas/${work_id}.md and mark it Ready For Coding before coding") - fi - [[ -n "${req}" ]] || failures+=("${requirement_msg}") - ;; - api-test|review) - [[ -f "${canvas}" ]] || failures+=("${canvas_msg}") - if ! _wf_gate_ledger_has_record "${work_id}" any; then - failures+=("no ledger evidence for ${work_id}: stage progress via ./scripts/sdlc.sh capture before ${phase}") - elif ! _wf_gate_ledger_has_record "${work_id}" verify; then - failures+=("no Validation receipt for ${work_id}: a dummy lesson is not enough; capture command/exit/result (run Validation) before ${phase} (skipping tests does not open ${phase})") - fi - ;; - prompt-update) - [[ -f "${canvas}" ]] || failures+=("${canvas_msg}") - ;; - retro) - if [[ ! -f "${home}/spdd/reviews/${work_id}-review.md" ]] \ - && ! _wf_gate_has_skip "${work_id}" review; then - failures+=("review missing: create spdd/reviews/${work_id}-review.md via /sdlc-spdd-review (or record an explicit skip: ./scripts/sdlc.sh skip review --reason \"...\")") - fi - ;; - sync) - if ! _wf_gate_ledger_has_record "${work_id}" retro \ - && ! _wf_gate_has_skip "${work_id}" retro; then - failures+=("no retro lesson for ${work_id}: capture a decision/pitfall/pattern via ./scripts/sdlc.sh capture (or record an explicit skip: ./scripts/sdlc.sh skip retro --reason \"...\") before sync") - fi - ;; - esac - - if ((${#failures[@]} == 0)); then - echo "gate ${phase}: OK for ${work_id}" - _wf_print_advisory_gate_rows "${phase}" - return 0 - fi - echo "gate ${phase}: BLOCKED for ${work_id}" >&2 - local failure - for failure in "${failures[@]}"; do - echo " - ${failure}" >&2 - done - _wf_print_advisory_gate_rows "${phase}" >&2 - return 1 -} - -_wf_progress_evidence_for_work() { - local root="$1" - local work_id="$2" - _wf_ensure_paths_lib || return 0 - local ledger stage - ledger="$(sdlc_ledger "${root}")" - stage="$(sdlc_stage "${root}")" - python3 - </dev/null; then - inferred="code" - fi - fi - if [[ -n "${progress_evidence}" ]]; then - inferred="code" - fi - if [[ -f "${review}" ]]; then - inferred="review" - fi - if _wf_has_retro_evidence "${work_id}"; then - inferred="retro" - fi - if [[ -f "${sync_log}" ]]; then - inferred="sync" - fi - - local session_file - _wf_ensure_paths_lib || true - if declare -F sdlc_sessions_dir >/dev/null 2>&1; then - session_file="$(sdlc_sessions_dir "${root}")/current-session.md" - else - session_file="$(_wf_home_path)/.sdlc/sessions/current-session.md" - fi - 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 session_idx - stored_idx="$(_wf_phase_index "${inferred}")" - session_idx="$(_wf_phase_index "${session_phase}")" - if (( session_idx > stored_idx )); then - inferred="${session_phase}" - fi - fi - fi - - echo "${inferred}" -} - -_wf_infer_gates_from_artifacts() { - local work_id="$1" - local root="${SDLC_ROOT}" - local home - home="$(_wf_home_path)" - local req analysis canvas review sync_log progress_evidence - local readiness="" - - req="$(_wf_requirement_path "${work_id}")" - analysis="${home}/spdd/analysis/${work_id}-analysis.md" - canvas="${home}/spdd/canvas/${work_id}.md" - review="${home}/spdd/reviews/${work_id}-review.md" - sync_log="${home}/spdd/sync/${work_id}-sync.md" - progress_evidence="$(_wf_progress_evidence_for_work "${root}" "${work_id}")" - - if [[ -n "${req}" ]]; then - echo "requirement_documented=passed" - fi - if [[ -f "${canvas}" ]]; then - echo "canvas_exists=passed" - fi - if [[ -f "${canvas}" ]] && declare -F canvas_readiness >/dev/null 2>&1; then - readiness="$(canvas_readiness "${canvas}")" - fi - case "${readiness}" in - ready-for-coding|reviewed|complete) - echo "architect_review=passed" - echo "operations_task_sized=passed" - ;; - esac - if [[ -z "${readiness}" && -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 [[ -n "${progress_evidence}" ]]; then - echo "code_maps_to_ops=passed" - echo "tests_updated=passed" - fi - if [[ -f "${review}" ]]; then - echo "review_completed=passed" - echo "safeguards_checked=passed" - fi - if _wf_has_retro_evidence "${work_id}"; then - echo "retro_completed=passed" - fi - if [[ -f "${sync_log}" ]]; then - echo "canvas_synced=passed" - fi - return 0 -} - -_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_canvas_path() { - local work_id="$1" - local canvas - canvas="$(_wf_home_path)/spdd/canvas/${work_id}.md" - if [[ -f "${canvas}" ]]; then - printf '%s' "${canvas}" - fi - return 0 -} - -_wf_infer_next_operation() { - local work_id="$1" - local canvas - canvas="$(_wf_canvas_path "${work_id}")" - [[ -n "${canvas}" ]] || return 0 - - # Use [0-9][0-9] (not {2}) — mawk does not support brace quantifiers. - # Stop matching Status once a ## section boundary is hit so empty - # "Final Status / Status:" lines do not keep the last T## incomplete. - awk ' - BEGIN { op = ""; have_status = 0; complete = 0 } - /^##[[:space:]]/ { - if (op != "" && (!have_status || !complete)) { - print op - exit - } - op = "" - have_status = 0 - complete = 0 - next - } - /^### T[0-9][0-9]/ { - if (op != "" && (!have_status || !complete)) { - print op - exit - } - op = $2 - sub(/-.*/, "", op) - have_status = 0 - complete = 0 - next - } - op != "" && /^-[[:space:]]*[Ss]tatus:[[:space:]]*/ { - have_status = 1 - status = tolower($0) - sub(/^-[[:space:]]*status:[[:space:]]*/, "", status) - gsub(/^[[:space:]]+|[[:space:]]+$/, "", status) - if (status ~ /^(complete|done|skipped)$/) { - 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() { - echo "./sdlc-spdd/scripts/sdlc.sh" -} - -_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_sync_operation "${work_id}" "${resolved}" - _wf_log_history "${work_id}" sync "phase=${resolved} operation=$(_wf_read_state_var "${file}" operation)" -} - -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)" - if [[ -n "${milestone}" ]]; then - _wf_set_state_var "${work_id}" milestone "${milestone}" - fi - _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)" - if [[ "${phase}" != "resume" ]]; then - _wf_set_state_var "${work_id}" phase "${phase}" - fi - _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_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_HOME:-${SDLC_ROOT}/sdlc-spdd}/scripts/capture-session-memory.sh" - 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_complete() { - local have_cmd=0 - local have_exit=0 - local have_result=0 - local -a passthrough=() - local has_phase=0 - - while [[ $# -gt 0 ]]; do - case "$1" in - --verify-command) - have_cmd=1 - passthrough+=("$1" "${2:-}") - shift 2 - ;; - --verify-exit) - have_exit=1 - passthrough+=("$1" "${2:-}") - shift 2 - ;; - --verify-result) - have_result=1 - passthrough+=("$1" "${2:-}") - shift 2 - ;; - --phase) - has_phase=1 - passthrough+=("$1" "${2:-}") - shift 2 - ;; - *) - passthrough+=("$1") - shift - ;; - esac - done - - if [[ "${have_cmd}" -ne 1 || "${have_exit}" -ne 1 || "${have_result}" -ne 1 ]]; then - echo "sdlc_workflow_complete: verify receipt required (command, exit, pass/fail). Refuse complete without it." >&2 - echo "Pass --verify-command, --verify-exit, and --verify-result=pass." >&2 - return 1 - fi - if [[ "${has_phase}" -ne 1 ]]; then - passthrough+=(--phase code) - fi - sdlc_workflow_capture --complete "${passthrough[@]}" -} - -sdlc_workflow_resume() { - local work_id="$1" - 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 - return 2 - fi - - if declare -F sdlc_team_check_claim >/dev/null 2>&1; then - # Skip when claim already checked (avoids duplicate "Taking over…" on claim --force) - if [[ "${_SDLC_TEAM_CLAIM_CHECKED:-0}" != "1" ]]; then - sdlc_team_check_claim "${work_id}" "${force_claim}" || return $? - fi - 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}")" - if [[ "${resolved_phase}" == "code" ]]; then - local readiness - readiness="$(_wf_work_readiness "${work_id}")" - if [[ -n "${readiness}" ]] && declare -F readiness_allows_coding >/dev/null 2>&1 \ - && ! readiness_allows_coding "${readiness}"; then - echo "Note: canvas readiness is '${readiness}' — not Ready For Coding; prefer /sdlc-spdd-architect before coding." - fi - fi - echo "Quick check: $(_wf_shell_helper_path) 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" "${team_note}" - echo "Team: commit spdd/memory/registry.jsonl to share this claim." - fi -} - -sdlc_workflow_advance() { - local to_phase="${1:-}" - local force="${2:-0}" - 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 readiness - 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 - - # Architecture-first: refuse entering code when canvas readiness blocks coding. - if [[ "${next}" == "code" && "${force}" != "1" ]]; then - readiness="$(_wf_work_readiness "${work_id}")" - if [[ -n "${readiness}" ]] && declare -F readiness_allows_coding >/dev/null 2>&1 \ - && ! readiness_allows_coding "${readiness}"; then - echo "sdlc_workflow_advance: canvas readiness is '${readiness}' — not Ready For Coding." >&2 - echo "Run /sdlc-spdd-architect (or prompt-update), then retry. Override: advance --force" >&2 - return 3 - 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}" - 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: $(_wf_shell_helper_path) next" -} - -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 - 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 spdd/memory/registry.jsonl to share shelf status." - fi - echo "Shelved ${work_id}" - echo "Reason: ${reason}" - echo "Resume later: ./scripts/sdlc.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})" - if [[ -n "${rs}" ]]; then - echo " reason: ${rs}" - fi - 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 - if [[ -n "${milestone}" ]]; then - echo "Milestone: ${milestone}" - fi - if [[ -n "${last_session}" ]]; then - echo "Last session: ${last_session}" - fi - if [[ -n "${last_capture}" ]]; then - echo "Last capture: ${last_capture}" - fi - - 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 "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 (skip nested re-source from team registry) -if [[ "${BASH_SOURCE[0]}" == "${0}" && "${_SDLC_WORKFLOW_LOAD_DEPTH}" -eq 1 ]]; then - cmd="${1:-status}" - shift || true - 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 - 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 - phase="" - 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}" "${force_claim}" - ;; - advance|/sdlc-workflow-advance) - to="" - force_advance=0 - while [[ $# -gt 0 ]]; do - case "$1" in - --to) to="${2:-}"; shift 2 ;; - --force) force_advance=1; shift ;; - *) to="${1}"; shift ;; - esac - done - sdlc_workflow_advance "${to}" "${force_advance}" - ;; - 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}" - ;; - gate|/sdlc-workflow-gate) - phase="" - work_id="" - while [[ $# -gt 0 ]]; do - case "$1" in - --work-id) work_id="${2:-}"; shift 2 ;; - --phase) phase="${2:-}"; shift 2 ;; - *) - if [[ -z "${phase}" && "$1" != -* ]]; then - phase="$1" - fi - shift - ;; - esac - done - sdlc_workflow_gate "${phase}" "${work_id}" - ;; - list-shelved|/sdlc-workflow-list-shelved) - sdlc_workflow_list_shelved - ;; - capture|/sdlc-workflow-capture) - sdlc_workflow_capture "$@" - ;; - complete|/sdlc-workflow-complete) - sdlc_workflow_complete "$@" - ;; - accept|/sdlc-accept-lessons) - accept_script="${SDLC_HOME:-${SDLC_ROOT}/sdlc-spdd}/scripts/accept-lessons.sh" - if [[ ! -x "${accept_script}" ]]; then - echo "accept-lessons.sh not found" >&2 - exit 1 - fi - "${accept_script}" --target "${SDLC_ROOT}" "$@" - ;; - 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="" - 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}" "${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 - fi - ;; - archive|/sdlc-archive) - dry=0 - force=0 - all=0 - work_id="" - while [[ $# -gt 0 ]]; do - case "$1" in - --dry-run) dry=1; shift ;; - --force) force=1; shift ;; - --all|--all-eligible) all=1; shift ;; - --work-id) work_id="${2:-}"; shift 2 ;; - *) - if [[ -z "${work_id}" && "$1" != -* ]]; then - work_id="$1" - fi - shift - ;; - esac - done - if ! declare -F sdlc_team_archive_work >/dev/null 2>&1; then - echo "team registry not installed" >&2 - exit 1 - fi - if [[ "${all}" -eq 1 ]]; then - sdlc_team_archive_eligible "${dry}" - else - sdlc_team_archive_work "${work_id}" "${dry}" "${force}" - 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|complete|accept|resume|advance|gate|skip|shelf|sync|sync-team|team|list-work|claim|release|archive|list-shelved|help} ..." >&2 - echo "Try: $0 help" >&2 - exit 2 - ;; - esac -fi diff --git a/tests/live-consumer/scenarios/01-install.sh b/tests/live-consumer/scenarios/01-install.sh index fd078721..76d5838f 100755 --- a/tests/live-consumer/scenarios/01-install.sh +++ b/tests/live-consumer/scenarios/01-install.sh @@ -9,8 +9,9 @@ HOME="$(live_home "${ROOT}")" echo "== 01 install layout ==" [[ -x "${HOME}/scripts/sdlc.sh" ]] && ok "sdlc.sh installed" || bad "sdlc.sh missing" -[[ -x "${HOME}/scripts/sdlc-pointer.sh" ]] && ok "pointer script" || bad "pointer script" -[[ -x "${HOME}/scripts/sdlc-workflow.sh" ]] && ok "workflow script" || bad "workflow script" +[[ ! -e "${HOME}/scripts/sdlc-pointer.sh" ]] && ok "retired pointer script absent" || bad "pointer script should be absent" +[[ ! -e "${HOME}/scripts/sdlc-workflow.sh" ]] && ok "retired workflow script absent" || bad "workflow script should be absent" +[[ ! -e "${HOME}/scripts/sdlc-team-registry.sh" ]] && ok "retired registry script absent" || bad "registry script should be absent" [[ -f "${HOME}/spdd/memory/registry.jsonl" ]] && ok "registry.jsonl" || bad "registry.jsonl" [[ -f "${ROOT}/.cursor/rules/sdlc-spdd.mdc" ]] && ok "cursor rule" || bad "cursor rule" [[ -d "${ROOT}/.cursor/commands" ]] && ok "cursor commands dir" || bad "cursor commands dir" diff --git a/tests/test-adapter-install.sh b/tests/test-adapter-install.sh index c45455cf..d04906e0 100755 --- a/tests/test-adapter-install.sh +++ b/tests/test-adapter-install.sh @@ -218,6 +218,10 @@ assert_v3_seed_files() { assert_file "${t}/sdlc-spdd/scripts/index-spdd-analysis.sh" assert_file "${t}/sdlc-spdd/spdd/analysis/.gitkeep" assert_file "${t}/sdlc-spdd/scripts/resolve-agent-context.sh" + assert_file "${t}/sdlc-spdd/scripts/sdlc.sh" + assert_absent "${t}/sdlc-spdd/scripts/sdlc-pointer.sh" + assert_absent "${t}/sdlc-spdd/scripts/sdlc-workflow.sh" + assert_absent "${t}/sdlc-spdd/scripts/sdlc-team-registry.sh" assert_file "${t}/sdlc-spdd/scripts/hooks/pre-commit.sample" assert_same "${t}/sdlc-spdd/scripts/hooks/pre-commit.sample" \ "${REPO_ROOT}/templates/agent-context/hooks/pre-commit.sample" @@ -349,6 +353,10 @@ expect_pass "verify all three after upgrade" "${VERIFY}" --target "${T}" --requi echo "== Test 10: Upgrade preserves project-owned Claude memory; refreshes adapter workflow ==" T="${WORK}/preserve"; mkdir -p "${T}/.github/workflows" "${SETUP}" --target "${T}" --cursor --copilot >/dev/null 2>&1 +for retired in sdlc-pointer.sh sdlc-workflow.sh sdlc-team-registry.sh; do + printf '#!/usr/bin/env bash\n# retired framework twin\n' > "${T}/sdlc-spdd/scripts/${retired}" + chmod +x "${T}/sdlc-spdd/scripts/${retired}" +done custom_claude="custom existing claude instructions" printf '%s' "${custom_claude}" > "${T}/CLAUDE.md" cat > "${T}/.github/workflows/validate-sdlc-spdd-adapters.yml" <<'OLDWF' @@ -369,6 +377,12 @@ jobs: run: ./sdlc-spdd/scripts/validate-command-adapters.sh --target . OLDWF "${UPGRADE}" --target "${T}" --all >/dev/null 2>&1 +assert_absent "${T}/sdlc-spdd/scripts/sdlc-pointer.sh" +assert_absent "${T}/sdlc-spdd/scripts/sdlc-workflow.sh" +assert_absent "${T}/sdlc-spdd/scripts/sdlc-team-registry.sh" +assert_glob_exists "${T}/.sdlc-spdd-upgrade-backups"/*/sdlc-spdd/scripts/sdlc-pointer.sh "retired pointer backup" +assert_glob_exists "${T}/.sdlc-spdd-upgrade-backups"/*/sdlc-spdd/scripts/sdlc-workflow.sh "retired workflow backup" +assert_glob_exists "${T}/.sdlc-spdd-upgrade-backups"/*/sdlc-spdd/scripts/sdlc-team-registry.sh "retired registry backup" assert_contains "${T}/CLAUDE.md" "${custom_claude}" "custom Claude content" assert_claude_grounded "${T}" assert_target_adapter_workflow "${T}" From 718a51746c5f6d5b5beaf08225d8b24d3e88e0b8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 14 Sep 2026 04:58:57 +0000 Subject: [PATCH 09/18] docs: record REF-003 T03 completion Co-authored-by: John Menke --- .../spdd/canvas/REF-003-retire-bash-workflow-dual-path.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sdlc-spdd/spdd/canvas/REF-003-retire-bash-workflow-dual-path.md b/sdlc-spdd/spdd/canvas/REF-003-retire-bash-workflow-dual-path.md index 9cbfb861..e8721eee 100644 --- a/sdlc-spdd/spdd/canvas/REF-003-retire-bash-workflow-dual-path.md +++ b/sdlc-spdd/spdd/canvas/REF-003-retire-bash-workflow-dual-path.md @@ -153,7 +153,7 @@ already means the only supported engine. `SDLC_ENGINE=shell` and any ### T03 - Stop shipping and delete the bash twin -- Status: Pending +- Status: Complete - Description: Remove twin installation from init/upgrade, delete exact retired paths during upgrade, require their absence in install verification, and delete template plus dogfood copies. @@ -243,8 +243,8 @@ already means the only supported engine. `SDLC_ENGINE=shell` and any - [x] T01 bridge is covered for source and installed targets - [x] T02 dispatcher has one mandatory engine - [x] T02 retained utilities have no twin dependency -- [ ] T03 twin files are absent from source and installs -- [ ] T03 shipped LOC reduction is at least 3,000 +- [x] T03 twin files are absent from source and installs +- [x] T03 shipped LOC reduction is at least 3,000 - [ ] T04 current shell/pytest matrix is green - [ ] T05 current docs state one engine - [ ] T06 review, retro, sync, and dependency docs are complete From 26046135f2b6046b350e22c443b92108a1386767 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 14 Sep 2026 05:01:51 +0000 Subject: [PATCH 10/18] test: migrate workflow coverage to Python engine Co-authored-by: John Menke --- .github/workflows/test-archive-work.yml | 37 - .github/workflows/test-sdlc-pointer.yml | 36 - .github/workflows/test-sdlc-workflow.yml | 45 +- .../scenarios/02-pointer-workflow.sh | 8 +- tests/research/test_ref001_sut.py | 82 +- tests/test-archive-work.sh | 251 ----- tests/test-gate-advisory-visible.sh | 16 +- tests/test-gate-review-validation.sh | 17 +- tests/test-integration-merge.sh | 4 +- tests/test-sdlc-pointer.sh | 100 -- tests/test-sdlc-workflow.sh | 872 ------------------ 11 files changed, 69 insertions(+), 1399 deletions(-) delete mode 100644 .github/workflows/test-archive-work.yml delete mode 100644 .github/workflows/test-sdlc-pointer.yml delete mode 100755 tests/test-archive-work.sh delete mode 100755 tests/test-sdlc-pointer.sh delete mode 100755 tests/test-sdlc-workflow.sh diff --git a/.github/workflows/test-archive-work.yml b/.github/workflows/test-archive-work.yml deleted file mode 100644 index 9304d150..00000000 --- a/.github/workflows/test-archive-work.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Test Archive Work - -on: - pull_request: - paths: - - 'templates/agent-context/sdlc-team-registry.sh' - - 'templates/agent-context/sdlc-workflow.sh' - - 'templates/agent-context/sdlc-pointer.sh' - - 'scripts/sdlc.sh' - - 'templates/agent-context/work-registry.tsv' - - 'tests/test-archive-work.sh' - - '.github/workflows/test-archive-work.yml' - push: - branches: [main] - paths: - - 'templates/agent-context/sdlc-team-registry.sh' - - 'templates/agent-context/sdlc-workflow.sh' - - 'templates/agent-context/sdlc-pointer.sh' - - 'scripts/sdlc.sh' - - 'templates/agent-context/work-registry.tsv' - - 'tests/test-archive-work.sh' - - '.github/workflows/test-archive-work.yml' - -jobs: - test-archive-work: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Check shell syntax - run: | - bash -n templates/agent-context/sdlc-team-registry.sh - bash -n templates/agent-context/sdlc-workflow.sh - bash -n tests/test-archive-work.sh - - - name: Run archive-work regression tests - run: ./tests/test-archive-work.sh diff --git a/.github/workflows/test-sdlc-pointer.yml b/.github/workflows/test-sdlc-pointer.yml deleted file mode 100644 index 09929efc..00000000 --- a/.github/workflows/test-sdlc-pointer.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Test SDLC Pointer - -on: - pull_request: - paths: - - 'templates/agent-context/sdlc-pointer.sh' - - 'templates/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: - - 'templates/agent-context/sdlc-pointer.sh' - - 'templates/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 templates/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 diff --git a/.github/workflows/test-sdlc-workflow.yml b/.github/workflows/test-sdlc-workflow.yml index 9e1be16b..0fe59b9d 100644 --- a/.github/workflows/test-sdlc-workflow.yml +++ b/.github/workflows/test-sdlc-workflow.yml @@ -3,9 +3,8 @@ name: Test SDLC Workflow on: pull_request: paths: - - 'templates/agent-context/sdlc-workflow.sh' - - 'templates/agent-context/sdlc-pointer.sh' - - 'templates/agent-context/sdlc-team-registry.sh' + - 'engine/src/sdlc_engine/**' + - 'engine/tests_unit/**' - 'templates/agent-context/README.md' - 'scripts/start-agent-session.sh' - 'scripts/capture-session-memory.sh' @@ -22,9 +21,7 @@ on: - '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' - - 'tests/test-archive-work.sh' + - 'tests/test-sdlc-engine-shim.sh' - 'tests/test-verify-receipt.sh' - 'tests/test-gate-review-validation.sh' - 'tests/test-gate-advisory-visible.sh' @@ -37,9 +34,8 @@ on: push: branches: [main] paths: - - 'templates/agent-context/sdlc-workflow.sh' - - 'templates/agent-context/sdlc-pointer.sh' - - 'templates/agent-context/sdlc-team-registry.sh' + - 'engine/src/sdlc_engine/**' + - 'engine/tests_unit/**' - 'templates/agent-context/README.md' - 'scripts/start-agent-session.sh' - 'scripts/capture-session-memory.sh' @@ -56,9 +52,7 @@ on: - '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' - - 'tests/test-archive-work.sh' + - 'tests/test-sdlc-engine-shim.sh' - 'tests/test-verify-receipt.sh' - 'tests/test-gate-review-validation.sh' - 'tests/test-gate-advisory-visible.sh' @@ -74,13 +68,16 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Install Python engine + run: python3 -m pip install -e './engine[dev]' - name: Check shell syntax run: | - bash -n templates/agent-context/sdlc-pointer.sh - bash -n templates/agent-context/sdlc-workflow.sh bash -n scripts/sdlc.sh - bash -n tests/test-sdlc-workflow.sh + bash -n tests/test-sdlc-engine-shim.sh bash -n tests/test-verify-receipt.sh bash -n tests/test-gate-review-validation.sh bash -n tests/test-gate-advisory-visible.sh @@ -92,11 +89,15 @@ jobs: bash -n scripts/lib/paths.sh bash -n scripts/validate-command-adapters.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 + - name: Run Python workflow, pointer, archive, and CLI regressions + run: > + PYTHONPATH=engine/src pytest -q + engine/tests_unit/test_workflow.py + engine/tests_unit/test_workflow_gates.py + engine/tests_unit/test_pointer.py + engine/tests_unit/test_registry_archive.py + engine/tests_unit/test_cli.py + engine/tests_unit/test_cli_shell.py - name: Run I1 verify-receipt proving tests run: ./tests/test-verify-receipt.sh @@ -113,5 +114,5 @@ jobs: - name: Run leftover #16 constraint-honesty proving tests run: ./tests/test-quality-gates-constraint-honest.sh - - name: Run archive-work regression tests - run: ./tests/test-archive-work.sh + - name: Run Python dispatcher regression tests + run: ./tests/test-sdlc-engine-shim.sh diff --git a/tests/live-consumer/scenarios/02-pointer-workflow.sh b/tests/live-consumer/scenarios/02-pointer-workflow.sh index 1ec8c579..337e1758 100755 --- a/tests/live-consumer/scenarios/02-pointer-workflow.sh +++ b/tests/live-consumer/scenarios/02-pointer-workflow.sh @@ -8,9 +8,7 @@ ROOT="${1:?target root required}" HOME="$(live_home "${ROOT}")" echo "== 02 pointer + workflow lifecycle ==" -PTR="${HOME}/scripts/sdlc-pointer.sh" - -if SDLC_ROOT="${ROOT}" "${PTR}" reset >/dev/null 2>&1 || true; then +if live_sdlc "${ROOT}" pointer reset >/dev/null 2>&1 || true; then ok "pointer reset" else bad "pointer reset" @@ -22,7 +20,7 @@ else bad "claim ${WORK_ID}" fi -ptr="$(SDLC_ROOT="${ROOT}" "${PTR}" get)" +ptr="$(live_sdlc "${ROOT}" pointer get)" [[ "${ptr}" == "${WORK_ID}" ]] && ok "pointer matches claim" || bad "pointer=${ptr}" next_out="$(live_sdlc "${ROOT}" next)" @@ -70,7 +68,7 @@ else bad "shelf" fi -ptr="$(SDLC_ROOT="${ROOT}" "${PTR}" get)" +ptr="$(live_sdlc "${ROOT}" pointer get)" [[ -z "${ptr}" ]] && ok "shelf cleared pointer" || bad "pointer still set after shelf" if live_sdlc "${ROOT}" list-shelved | grep -Fq "${WORK_ID}"; then diff --git a/tests/research/test_ref001_sut.py b/tests/research/test_ref001_sut.py index b042ed38..8b41ed2b 100644 --- a/tests/research/test_ref001_sut.py +++ b/tests/research/test_ref001_sut.py @@ -1,9 +1,9 @@ -"""REF-001: Python gate_check is the SUT; shell CLI delegates when importable. +"""REF-001/REF-003: Python gate_check is the sole workflow SUT. A headings-only canvas with a 'ready for coding' substring is the historical -confound (shell grep pass / Python FEAT-014 fail). These tests prove the SUT -path rejects it, and that SDLC_ENGINE=shell still delegates when Python can -be imported. SDLC_GATE_ENGINE=shell is the labeled non-SUT fallback. +confound (former shell grep pass / Python FEAT-014 fail). These tests prove the +SUT path rejects it and retired shell-engine overrides cannot select another +implementation. """ from __future__ import annotations @@ -21,7 +21,6 @@ TESTING = ROOT / "TESTING.md" ROADMAP = ROOT / "sdlc-spdd" / "ROADMAP.md" SDLC_SH = ROOT / "scripts" / "sdlc.sh" -WORKFLOW = ROOT / "templates" / "agent-context" / "sdlc-workflow.sh" WID = "FEAT-REF001-headings" @@ -97,49 +96,36 @@ def test_python_gate_rejects_headings_only(self) -> None: failures, ) - def test_shell_engine_delegates_to_python_when_importable(self) -> None: - with tempfile.TemporaryDirectory() as tmp: - root = Path(tmp) - _seed(root) - env = _env() - env["SDLC_ENGINE"] = "shell" - env["SDLC_ROOT"] = str(root) - env["SDLC_HOME"] = str(root / "sdlc-spdd") - proc = subprocess.run( - ["bash", str(WORKFLOW), "gate", "--phase", "code", "--work-id", WID], - cwd=str(ROOT), - env=env, - capture_output=True, - text=True, - check=False, - ) - self.assertNotEqual(proc.returncode, 0, proc.stdout + proc.stderr) - blob = (proc.stdout + proc.stderr).lower() - self.assertTrue( - "empty requirements" in blob or "files:" in blob or "no t##" in blob, - proc.stdout + proc.stderr, - ) + def test_shell_engine_override_is_rejected(self) -> None: + env = _env() + env["SDLC_ENGINE"] = "shell" + proc = subprocess.run( + ["bash", str(SDLC_SH), "next"], + cwd=str(ROOT), + env=env, + capture_output=True, + text=True, + check=False, + ) + self.assertNotEqual(proc.returncode, 0, proc.stdout + proc.stderr) + self.assertIn("no longer supported", (proc.stdout + proc.stderr).lower()) - def test_gate_engine_shell_fallback_is_not_sut(self) -> None: - with tempfile.TemporaryDirectory() as tmp: - root = Path(tmp) - _seed(root) - env = _env() - env["SDLC_GATE_ENGINE"] = "shell" - env["SDLC_ROOT"] = str(root) - env["SDLC_HOME"] = str(root / "sdlc-spdd") - proc = subprocess.run( - ["bash", str(WORKFLOW), "gate", "--phase", "code", "--work-id", WID], - cwd=str(ROOT), - env=env, - capture_output=True, - text=True, - check=False, - ) - self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr) + def test_gate_engine_shell_override_is_rejected(self) -> None: + env = _env() + env["SDLC_GATE_ENGINE"] = "shell" + proc = subprocess.run( + ["bash", str(SDLC_SH), "next"], + cwd=str(ROOT), + env=env, + capture_output=True, + text=True, + check=False, + ) + self.assertNotEqual(proc.returncode, 0, proc.stdout + proc.stderr) + self.assertIn("python gate semantics", (proc.stdout + proc.stderr).lower()) - def test_sdlc_sh_default_auto_uses_python(self) -> None: - self.assertIn("SDLC_ENGINE:-auto", SDLC_SH.read_text(encoding="utf-8")) + def test_sdlc_sh_default_uses_python(self) -> None: + self.assertIn("Python engine is required", SDLC_SH.read_text(encoding="utf-8")) env = _env() env.pop("SDLC_ENGINE", None) proc = subprocess.run( @@ -154,8 +140,8 @@ def test_sdlc_sh_default_auto_uses_python(self) -> None: self.assertTrue(proc.stdout.strip().startswith("2.0.0a"), proc.stdout) def test_python_parser_has_no_top_level_capture(self) -> None: - # sdlc.sh capture stages lessons; Python only has `local capture`. - # Default auto must not route the shell verb into argparse. + # sdlc.sh capture bridges the retained capture utility explicitly; + # lifecycle parsing itself has no ambiguous top-level capture verb. from io import StringIO from contextlib import redirect_stderr diff --git a/tests/test-archive-work.sh b/tests/test-archive-work.sh deleted file mode 100755 index ba51f516..00000000 --- a/tests/test-archive-work.sh +++ /dev/null @@ -1,251 +0,0 @@ -#!/usr/bin/env bash -# Regression harness for completed/cancelled Work ID archive (storage v3: remove artifacts). -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" -WORKFLOW="${REPO_ROOT}/templates/agent-context/sdlc-workflow.sh" -POINTER="${REPO_ROOT}/templates/agent-context/sdlc-pointer.sh" -TEAM="${REPO_ROOT}/templates/agent-context/sdlc-team-registry.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)); } - -registry_file() { - local t="$1" - printf '%s' "${t}/sdlc-spdd/spdd/memory/registry.jsonl" -} - -registry_matches() { - local t="$1" work_id="$2" regex="$3" - local reg - reg="$(registry_file "${t}")" - [[ -f "${reg}" ]] && grep -q "\"work_id\": \"${work_id}\"" "${reg}" && grep -Eq "${regex}" "${reg}" -} - -wf() { SDLC_ROOT="${1}" "${WORKFLOW}" "${@:2}"; } - -setup_work() { - local t="$1" - local work_id="$2" - local final_status="$3" - mkdir -p \ - "${t}/sdlc-spdd/scripts" \ - "${t}/sdlc-spdd/spdd/canvas" \ - "${t}/sdlc-spdd/spdd/analysis" \ - "${t}/sdlc-spdd/spdd/reviews" \ - "${t}/sdlc-spdd/spdd/sync" \ - "${t}/sdlc-spdd/requirements/milestones" \ - "${t}/sdlc-spdd/.sdlc/workflows" \ - "${t}/sdlc-spdd/.sdlc/sessions" \ - "${t}/sdlc-spdd/scripts" - cp "${POINTER}" "${t}/sdlc-spdd/scripts/sdlc-pointer.sh" - cp "${WORKFLOW}" "${t}/sdlc-spdd/scripts/sdlc-workflow.sh" - cp "${TEAM}" "${t}/sdlc-spdd/scripts/sdlc-team-registry.sh" - mkdir -p "${t}/sdlc-spdd/spdd/memory" "${t}/sdlc-spdd/scripts/lib" - cp "${REPO_ROOT}/scripts/lib/paths.sh" "${t}/sdlc-spdd/scripts/lib/paths.sh" - : > "${t}/sdlc-spdd/spdd/memory/registry.jsonl" - cp "${REPO_ROOT}/scripts/sdlc.sh" "${t}/sdlc-spdd/scripts/sdlc.sh" - chmod +x \ - "${t}/sdlc-spdd/scripts/sdlc-pointer.sh" \ - "${t}/sdlc-spdd/scripts/sdlc-workflow.sh" \ - "${t}/sdlc-spdd/scripts/sdlc-team-registry.sh" \ - "${t}/sdlc-spdd/scripts/sdlc.sh" - - cat > "${t}/sdlc-spdd/spdd/canvas/${work_id}.md" < "${t}/sdlc-spdd/spdd/analysis/${work_id}-analysis.md" - printf '# review\n' > "${t}/sdlc-spdd/spdd/reviews/${work_id}-review.md" - printf '# sync\n' > "${t}/sdlc-spdd/spdd/sync/${work_id}-sync.md" - printf '# feature\n' > "${t}/sdlc-spdd/requirements/milestones/${work_id}.md" - printf 'phase=code\nactive=1\n' > "${t}/sdlc-spdd/.sdlc/workflows/${work_id}.state" - printf '# session for %s\n' "${work_id}" > "${t}/sdlc-spdd/.sdlc/sessions/20260727T000000Z-plan-${work_id}.md" - printf '# current\n' > "${t}/sdlc-spdd/.sdlc/sessions/current-session.md" -} - -echo "== Test 1: refuse in-progress work without --force ==" -T="${WORK}/refuse" -setup_work "${T}" "FEAT-100-active" "In Progress" -if SDLC_ROOT="${T}" wf "${T}" archive FEAT-100-active >/dev/null 2>&1; then - bad "archive should refuse In Progress" -else - ok "archive refuses In Progress" -fi -if [[ -f "${T}/sdlc-spdd/spdd/canvas/FEAT-100-active.md" ]]; then - ok "in-progress canvas left in place" -else - bad "in-progress canvas was removed" -fi - -echo "== Test 2: archive completed work removes artifacts ==" -T="${WORK}/complete" -setup_work "${T}" "FEAT-101-done" "Complete" -SDLC_USER="archiver" SDLC_ROOT="${T}" wf "${T}" claim FEAT-101-done >/dev/null -SDLC_ROOT="${T}" wf "${T}" archive FEAT-101-done >/dev/null -if [[ ! -f "${T}/sdlc-spdd/spdd/canvas/FEAT-101-done.md" \ - && ! -f "${T}/sdlc-spdd/spdd/analysis/FEAT-101-done-analysis.md" \ - && ! -f "${T}/sdlc-spdd/spdd/reviews/FEAT-101-done-review.md" \ - && ! -f "${T}/sdlc-spdd/spdd/sync/FEAT-101-done-sync.md" ]]; then - ok "canvas and sidecar artifacts removed" -else - bad "contract artifacts still present after archive" -fi -if [[ -f "${T}/sdlc-spdd/requirements/milestones/FEAT-101-done.md" ]]; then - ok "milestone requirement left in place" -else - bad "milestone should not be removed" -fi -if [[ ! -f "${T}/sdlc-spdd/.sdlc/sessions/20260727T000000Z-plan-FEAT-101-done.md" \ - && -f "${T}/sdlc-spdd/.sdlc/sessions/current-session.md" ]]; then - ok "matching session brief removed; current-session kept" -else - bad "session archive behavior incorrect" -fi -if registry_matches "${T}" "FEAT-101-done" '"status": "archived"'; then - ok "registry status set to archived" -else - bad "registry missing archived row" -fi -ptr="$(SDLC_ROOT="${T}" "${T}/sdlc-spdd/scripts/sdlc-pointer.sh" get)" -if [[ -z "${ptr}" ]]; then ok "pointer cleared on archive"; else bad "pointer still set (${ptr})"; fi - -echo "== Test 3: archive cancelled work ==" -T="${WORK}/cancelled" -setup_work "${T}" "FEAT-102-cancel" "Cancelled" -SDLC_ROOT="${T}" wf "${T}" archive FEAT-102-cancel >/dev/null -if [[ ! -f "${T}/sdlc-spdd/spdd/canvas/FEAT-102-cancel.md" ]] \ - && registry_matches "${T}" "FEAT-102-cancel" '"status": "archived"' \ - && registry_matches "${T}" "FEAT-102-cancel" 'archived:cancelled'; then - ok "cancelled work archived with note token" -else - bad "cancelled archive failed" -fi - -echo "== Test 4: canceled spelling (US) treated as cancelled ==" -T="${WORK}/canceled-us" -setup_work "${T}" "FEAT-103-us" "Canceled — scope cut" -SDLC_ROOT="${T}" wf "${T}" archive FEAT-103-us >/dev/null -if [[ ! -f "${T}/sdlc-spdd/spdd/canvas/FEAT-103-us.md" ]]; then - ok "Canceled spelling is archivable" -else - bad "Canceled spelling not accepted" -fi - -echo "== Test 5: dry-run does not remove files ==" -T="${WORK}/dry" -setup_work "${T}" "FEAT-104-dry" "Complete" -out="$(SDLC_ROOT="${T}" wf "${T}" archive FEAT-104-dry --dry-run)" -if [[ -f "${T}/sdlc-spdd/spdd/canvas/FEAT-104-dry.md" ]]; then - ok "dry-run leaves canvas in place" -else - bad "dry-run removed canvas" -fi -if grep -Fq '[dry-run]' <<< "${out}"; then - ok "dry-run prints planned removals" -else - bad "dry-run missing plan output" -fi - -echo "== Test 6: --all archives every eligible Work ID ==" -T="${WORK}/all" -setup_work "${T}" "FEAT-105-a" "Complete" -setup_work "${T}" "FEAT-105-b" "Cancelled" -setup_work "${T}" "FEAT-105-c" "In Progress" -SDLC_ROOT="${T}" wf "${T}" archive --all >/dev/null -if [[ ! -f "${T}/sdlc-spdd/spdd/canvas/FEAT-105-a.md" \ - && ! -f "${T}/sdlc-spdd/spdd/canvas/FEAT-105-b.md" \ - && -f "${T}/sdlc-spdd/spdd/canvas/FEAT-105-c.md" ]]; then - ok "--all archives complete+cancelled, skips in-progress" -else - bad "--all selection incorrect" -fi - -echo "== Test 7: list-work ignores stray legacy archive paths ==" -T="${WORK}/discover" -setup_work "${T}" "FEAT-106-live" "In Progress" -mkdir -p "${T}/sdlc-spdd/spdd/canvas/archive" -printf '# old canvas\n' > "${T}/sdlc-spdd/spdd/canvas/archive/FEAT-999-old.md" -out="$(SDLC_ROOT="${T}" wf "${T}" list-work)" -if grep -q 'FEAT-106-live' <<< "${out}" && ! grep -q 'FEAT-999-old' <<< "${out}"; then - ok "list-work skips Work IDs only under legacy archive paths" -else - bad "list-work discover leaked archive entries" -fi - -echo "== Test 8: sync-team marks cancelled without archiving ==" -T="${WORK}/sync-cancel" -setup_work "${T}" "FEAT-107-sync" "Cancelled" -SDLC_ROOT="${T}" wf "${T}" sync-team >/dev/null -if registry_matches "${T}" "FEAT-107-sync" '"status": "cancelled"' \ - && [[ -f "${T}/sdlc-spdd/spdd/canvas/FEAT-107-sync.md" ]]; then - ok "sync-team sets cancelled and leaves files" -else - bad "sync-team cancelled behavior wrong" -fi - -echo "== Test 9: sdlc.sh wrapper archive path ==" -T="${WORK}/wrapper" -setup_work "${T}" "FEAT-108-wrap" "Complete" -if SDLC_ROOT="${T}" "${T}/sdlc-spdd/scripts/sdlc.sh" archive FEAT-108-wrap >/dev/null \ - && [[ ! -f "${T}/sdlc-spdd/spdd/canvas/FEAT-108-wrap.md" ]]; then - ok "sdlc.sh archive wrapper works" -else - bad "sdlc.sh archive wrapper failed" -fi - -echo "== Test 10: --force archives non-terminal work ==" -T="${WORK}/force" -setup_work "${T}" "FEAT-109-force" "In Progress" -if SDLC_ROOT="${T}" wf "${T}" archive FEAT-109-force --force >/dev/null \ - && [[ ! -f "${T}/sdlc-spdd/spdd/canvas/FEAT-109-force.md" ]] \ - && registry_matches "${T}" "FEAT-109-force" 'archived:forced'; then - ok "--force archives non-terminal work" -else - bad "--force archive failed" -fi - -echo "== Test 12: archive leaves lessons.jsonl in place ==" -T="${WORK}/ledger" -setup_work "${T}" "FEAT-111-mem" "Complete" -mkdir -p "${T}/sdlc-spdd/spdd/memory" -printf '%s\n' '{"id":"pitfall:FEAT-111-mem:engine:test","kind":"pitfall","work_id":"FEAT-111-mem","area":"engine","title":"keep me","body":"archive must not drop this dogfood record from lessons.jsonl.","source":"test","keywords":[],"schema":1}' > "${T}/sdlc-spdd/spdd/memory/lessons.jsonl" -before="$(cat "${T}/sdlc-spdd/spdd/memory/lessons.jsonl")" -out="$(SDLC_ROOT="${T}" wf "${T}" archive FEAT-111-mem)" -after="$(cat "${T}/sdlc-spdd/spdd/memory/lessons.jsonl")" -if [[ "${before}" == "${after}" ]] && grep -Fq 'lessons.jsonl' <<< "${out}"; then - ok "archive leaves lessons.jsonl unchanged and says so" -else - bad "archive mutated lessons.jsonl or omitted leave-ledger message" -fi - -echo "== Test 11: re-archive is a no-op for --all ==" -T="${WORK}/rearchive" -setup_work "${T}" "FEAT-110-once" "Complete" -SDLC_ROOT="${T}" wf "${T}" archive FEAT-110-once >/dev/null -out="$(SDLC_ROOT="${T}" wf "${T}" archive --all)" -if grep -q 'processed 0 eligible' <<< "${out}"; then - ok "--all skips already-archived registry rows" -else - if [[ ! -f "${T}/sdlc-spdd/spdd/canvas/FEAT-110-once.md" ]]; then - ok "--all did not duplicate archive (artifacts remain removed)" - else - bad "re-archive behavior unexpected: ${out}" - fi -fi - -echo -echo "Results: ${pass} passed, ${fail} failed" -if [[ "${fail}" -gt 0 ]]; then - exit 1 -fi -echo "All archive-work tests passed." diff --git a/tests/test-gate-advisory-visible.sh b/tests/test-gate-advisory-visible.sh index dc3c6654..782d9fa1 100755 --- a/tests/test-gate-advisory-visible.sh +++ b/tests/test-gate-advisory-visible.sh @@ -4,14 +4,8 @@ set -euo pipefail # Leftover #14 proving test: sdlc.sh gate must show advisory rows that did not run. # GATE_LABELS append "(advisory)" on next; gate used to print only failures. -export SDLC_ENGINE=shell -export SDLC_GATE_ENGINE=shell - SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" -WORKFLOW="${REPO_ROOT}/templates/agent-context/sdlc-workflow.sh" -POINTER="${REPO_ROOT}/templates/agent-context/sdlc-pointer.sh" -TEAM_REG="${REPO_ROOT}/templates/agent-context/sdlc-team-registry.sh" CAPTURE="${REPO_ROOT}/scripts/capture-session-memory.sh" SDLC_SH="${REPO_ROOT}/scripts/sdlc.sh" @@ -55,9 +49,6 @@ setup_feature() { "${t}/sdlc-spdd/spdd/analysis" \ "${t}/sdlc-spdd/spdd/memory" \ "${t}/sdlc-spdd/scripts/lib" - cp "${POINTER}" "${t}/sdlc-spdd/scripts/sdlc-pointer.sh" - cp "${WORKFLOW}" "${t}/sdlc-spdd/scripts/sdlc-workflow.sh" - cp "${TEAM_REG}" "${t}/sdlc-spdd/scripts/sdlc-team-registry.sh" : > "${t}/sdlc-spdd/spdd/memory/registry.jsonl" cp "${SDLC_SH}" "${t}/sdlc-spdd/scripts/sdlc.sh" cp "${CAPTURE}" "${t}/sdlc-spdd/scripts/capture-session-memory.sh" @@ -70,7 +61,9 @@ setup_feature() { sdlc() { local t="$1" shift - SDLC_ROOT="${t}" SDLC_ENGINE=shell SDLC_GATE_ENGINE=shell \ + SDLC_ROOT="${t}" \ + PYTHON="${REPO_ROOT}/.venv/bin/python" \ + PYTHONPATH="${REPO_ROOT}/engine/src" \ "${t}/sdlc-spdd/scripts/sdlc.sh" "$@" } @@ -80,7 +73,6 @@ work_id="FEAT-014-advisory-visible" setup_feature "${T}" write_requirement "${T}" "${work_id}" write_canvas "${T}" "${work_id}" -sdlc "${T}" resume "${work_id}" --phase plan >/dev/null if out="$(sdlc "${T}" gate architect --work-id "${work_id}" 2>&1)"; then if grep -q 'Architect review completed (advisory)' <<< "${out}" \ && grep -q 'Operations are task-sized (advisory)' <<< "${out}" \ @@ -97,7 +89,6 @@ echo "== test_gate_architect_blocked_still_shows_advisory ==" T="${WORK}/architect-blocked" work_id="FEAT-014-advisory-blocked" setup_feature "${T}" -sdlc "${T}" resume "${work_id}" --phase plan >/dev/null if out="$(sdlc "${T}" gate architect --work-id "${work_id}" 2>&1)"; then bad "gate architect must fail without canvas/requirement: ${out}" else @@ -115,7 +106,6 @@ T="${WORK}/analysis-ok" work_id="FEAT-014-no-advisory" setup_feature "${T}" write_requirement "${T}" "${work_id}" -sdlc "${T}" resume "${work_id}" --phase init >/dev/null if out="$(sdlc "${T}" gate analysis --work-id "${work_id}" 2>&1)"; then if grep -q '(advisory)' <<< "${out}" || grep -q 'did not run' <<< "${out}"; then bad "gate analysis should not invent advisory rows: ${out}" diff --git a/tests/test-gate-review-validation.sh b/tests/test-gate-review-validation.sh index 6c9c6177..dcd3e3b4 100755 --- a/tests/test-gate-review-validation.sh +++ b/tests/test-gate-review-validation.sh @@ -4,14 +4,8 @@ set -euo pipefail # Leftover #7 proving test: gate review requires Validation ran, not any ledger row. # Capture a dummy lesson, skip tests, `gate review` must fail. -export SDLC_ENGINE=shell -export SDLC_GATE_ENGINE=shell - SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" -WORKFLOW="${REPO_ROOT}/templates/agent-context/sdlc-workflow.sh" -POINTER="${REPO_ROOT}/templates/agent-context/sdlc-pointer.sh" -TEAM_REG="${REPO_ROOT}/templates/agent-context/sdlc-team-registry.sh" CAPTURE="${REPO_ROOT}/scripts/capture-session-memory.sh" SDLC_SH="${REPO_ROOT}/scripts/sdlc.sh" @@ -48,9 +42,6 @@ setup_feature() { "${t}/sdlc-spdd/spdd/analysis" \ "${t}/sdlc-spdd/spdd/memory" \ "${t}/sdlc-spdd/scripts/lib" - cp "${POINTER}" "${t}/sdlc-spdd/scripts/sdlc-pointer.sh" - cp "${WORKFLOW}" "${t}/sdlc-spdd/scripts/sdlc-workflow.sh" - cp "${TEAM_REG}" "${t}/sdlc-spdd/scripts/sdlc-team-registry.sh" : > "${t}/sdlc-spdd/spdd/memory/registry.jsonl" cp "${SDLC_SH}" "${t}/sdlc-spdd/scripts/sdlc.sh" cp "${CAPTURE}" "${t}/sdlc-spdd/scripts/capture-session-memory.sh" @@ -63,7 +54,9 @@ setup_feature() { sdlc() { local t="$1" shift - SDLC_ROOT="${t}" SDLC_ENGINE=shell SDLC_GATE_ENGINE=shell \ + SDLC_ROOT="${t}" \ + PYTHON="${REPO_ROOT}/.venv/bin/python" \ + PYTHONPATH="${REPO_ROOT}/engine/src" \ "${t}/sdlc-spdd/scripts/sdlc.sh" "$@" } @@ -72,7 +65,7 @@ T="${WORK}/dummy-skip" work_id="FEAT-025-dummy-review" setup_feature "${T}" write_canvas "${T}" "${work_id}" -sdlc "${T}" resume "${work_id}" --phase plan >/dev/null +sdlc "${T}" pointer set "${work_id}" >/dev/null if ! sdlc "${T}" capture --phase plan --summary "dummy lesson" \ --validation "skipped tests" >/dev/null; then bad "plan-phase dummy capture should succeed" @@ -99,7 +92,7 @@ T="${WORK}/receipt-ok" work_id="FEAT-026-review-receipt" setup_feature "${T}" write_canvas "${T}" "${work_id}" -sdlc "${T}" resume "${work_id}" --phase code >/dev/null +sdlc "${T}" pointer set "${work_id}" >/dev/null if sdlc "${T}" capture --phase code --summary "T01 complete" \ --verify-command "pytest tests/test_foo.py" \ --verify-exit 0 \ diff --git a/tests/test-integration-merge.sh b/tests/test-integration-merge.sh index 5a28e484..f740079e 100755 --- a/tests/test-integration-merge.sh +++ b/tests/test-integration-merge.sh @@ -124,9 +124,7 @@ fi echo "== F. Nested harnesses ==" for t in \ test-scripts-lib.sh \ - test-resolve-agent-context.sh \ - test-archive-work.sh \ - test-sdlc-pointer.sh; do + test-resolve-agent-context.sh; do if "${REPO_ROOT}/tests/${t}" >/dev/null; then ok "nested ${t}" else diff --git a/tests/test-sdlc-pointer.sh b/tests/test-sdlc-pointer.sh deleted file mode 100755 index 4679caa4..00000000 --- a/tests/test-sdlc-pointer.sh +++ /dev/null @@ -1,100 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Regression harness for templates/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}/templates/agent-context/sdlc-pointer.sh" -if [[ ! -f "${POINTER}" ]]; then - POINTER="${REPO_ROOT}/sdlc-spdd/scripts/sdlc-pointer.sh" -fi -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}" -"${REPO_ROOT}/scripts/init-project.sh" --target "${T}" >/dev/null -"${START}" --target "${T}" --work-id FEAT-002-session --phase plan >/dev/null -HOME="${T}/sdlc-spdd" -current="$(SDLC_ROOT="${T}" "${HOME}/scripts/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}/sdlc-spdd/scripts/sdlc-pointer.sh" ]]; then - ok "init-project installs executable pointer script" -else - bad "init-project missing sdlc-spdd/scripts/sdlc-pointer.sh" -fi - -# --------------------------------------------------------------------------- -echo -echo "Results: ${pass} passed, ${fail} failed" -if [[ "${fail}" -gt 0 ]]; then - exit 1 -fi diff --git a/tests/test-sdlc-workflow.sh b/tests/test-sdlc-workflow.sh deleted file mode 100755 index e23ac559..00000000 --- a/tests/test-sdlc-workflow.sh +++ /dev/null @@ -1,872 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Hermetic: this harness tests the bash workflow CLI, not the Milestone 2 SUT. -# Python gate_check is the SUT (REF-001); do not let a local pip install hijack gates. -export SDLC_GATE_ENGINE=shell - -# Regression harness for templates/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}/templates/agent-context/sdlc-workflow.sh" -POINTER="${REPO_ROOT}/templates/agent-context/sdlc-pointer.sh" -TEAM_REG="${REPO_ROOT}/templates/agent-context/sdlc-team-registry.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)); } - -registry_file() { - local t="$1" - printf '%s' "${t}/sdlc-spdd/spdd/memory/registry.jsonl" -} - -registry_matches() { - local t="$1" work_id="$2" regex="$3" - local reg - reg="$(registry_file "${t}")" - [[ -f "${reg}" ]] && grep -q "\"work_id\": \"${work_id}\"" "${reg}" && grep -Eq "${regex}" "${reg}" -} - -wf() { SDLC_ROOT="${1}" "${WORKFLOW}" "${@:2}"; } - -setup_feature() { - local t="$1" - local work_id="$2" - # Workflow CLI and all framework state live under /sdlc-spdd/. - mkdir -p "${t}/sdlc-spdd/.sdlc/sessions" \ - "${t}/sdlc-spdd/spdd/canvas" \ - "${t}/sdlc-spdd/spdd/analysis" \ - "${t}/sdlc-spdd/scripts/lib" - cp "${POINTER}" "${t}/sdlc-spdd/scripts/sdlc-pointer.sh" - cp "${WORKFLOW}" "${t}/sdlc-spdd/scripts/sdlc-workflow.sh" - cp "${TEAM_REG}" "${t}/sdlc-spdd/scripts/sdlc-team-registry.sh" - cp "${REPO_ROOT}/scripts/lib/paths.sh" "${t}/sdlc-spdd/scripts/lib/paths.sh" - mkdir -p "${t}/sdlc-spdd/spdd/memory" - : > "${t}/sdlc-spdd/spdd/memory/registry.jsonl" - cp "${REPO_ROOT}/scripts/lib/readiness.sh" "${t}/sdlc-spdd/scripts/lib/readiness.sh" - chmod +x "${t}/sdlc-spdd/scripts/sdlc-pointer.sh" "${t}/sdlc-spdd/scripts/sdlc-workflow.sh" "${t}/sdlc-spdd/scripts/sdlc-team-registry.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}/sdlc-spdd/scripts/sdlc-pointer.sh" get)" -if [[ "${ptr}" == "FEAT-001-alpha" ]]; then ok "resume sets pointer"; else bad "pointer not set"; fi -if [[ -f "${T}/sdlc-spdd/.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-spdd/.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-spdd/.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}/sdlc-spdd/scripts/sdlc-pointer.sh" get)" -if [[ -z "${ptr}" ]]; then ok "shelf clears pointer"; else bad "pointer should be empty"; fi -active="$(grep '^active=' "${T}/sdlc-spdd/.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}/sdlc-spdd/scripts/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}/sdlc-spdd/requirements/milestones" -printf '# req\n' > "${T}/sdlc-spdd/requirements/milestones/${work_id}.md" -printf '# analysis\n' > "${T}/sdlc-spdd/spdd/analysis/${work_id}-analysis.md" -printf '# canvas\nReady For Coding\n' > "${T}/sdlc-spdd/spdd/canvas/${work_id}.md" -wf "${T}" resume "${work_id}" >/dev/null -phase="$(grep '^phase=' "${T}/sdlc-spdd/.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-spdd/.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-spdd/.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-spdd/.sdlc/workflows/${work_id}.state"; then - ok "capture-session-memory records workflow capture" -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}/sdlc-spdd/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}/sdlc-spdd/scripts" -cp "${REPO_ROOT}/scripts/sdlc.sh" "${T}/sdlc-spdd/scripts/sdlc.sh" -chmod +x "${T}/sdlc-spdd/scripts/sdlc.sh" -SDLC_ROOT="${T}" "${T}/sdlc-spdd/scripts/sdlc.sh" resume "${work_id}" >/dev/null -out="$(SDLC_ROOT="${T}" "${T}/sdlc-spdd/scripts/sdlc.sh" next)" -if grep -q "${work_id}" <<< "${out}"; then ok "sdlc.sh wrapper works"; else bad "sdlc.sh wrapper failed"; fi - -# --------------------------------------------------------------------------- -echo "== Test 10b: sdlc.sh claim does not re-enter CLI (exec + nested source) ==" -T="${WORK}/wrapper-claim" -work_id="FEAT-005b-claim" -setup_feature "${T}" "${work_id}" -mkdir -p "${T}/sdlc-spdd/scripts" "${T}/sdlc-spdd/spdd/canvas" -printf '%s\n' "# ${work_id}" '' '## Final Status' '' '- Status: In Progress' \ - > "${T}/sdlc-spdd/spdd/canvas/${work_id}.md" -cp "${REPO_ROOT}/scripts/sdlc.sh" "${T}/sdlc-spdd/scripts/sdlc.sh" -chmod +x "${T}/sdlc-spdd/scripts/sdlc.sh" -if SDLC_USER="tester" SDLC_ROOT="${T}" "${T}/sdlc-spdd/scripts/sdlc.sh" claim "${work_id}" >/dev/null 2>"${T}/claim.err"; then - if registry_matches "${T}" "${work_id}" '"status": "active"'; then - ok "sdlc.sh claim updates registry without CLI re-entry" - else - bad "sdlc.sh claim exited 0 but registry missing row" - fi -else - bad "sdlc.sh claim failed (possible CLI re-entry): $(head -3 "${T}/claim.err")" -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}/sdlc-spdd/.sdlc/sessions/current-session.md" \ - && grep -q 'Assistant command' "${T}/sdlc-spdd/.sdlc/sessions/current-session.md"; then - ok "session brief embeds workflow state" -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}/sdlc-spdd/spdd/canvas/${work_id}.md" -wf "${T}" resume "${work_id}" >/dev/null -wf "${T}" sync "${work_id}" >/dev/null -op="$(grep '^operation=' "${T}/sdlc-spdd/.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 12b: empty Final Status does not keep last op incomplete ==" -T="${WORK}/ops-final" -work_id="FEAT-012b-final" -setup_feature "${T}" "${work_id}" -cat > "${T}/sdlc-spdd/spdd/canvas/${work_id}.md" <<'EOF' -# REASONS Canvas: FEAT-012b-final - empty Final Status - -## Metadata -- Work ID: FEAT-012b-final -- Status: In Progress -- Readiness: Ready For Coding - -## O - Operations - -### T01 - First - -- Status: Complete - -### T02 - Second - -- Status: Complete - -## Final Status - -- Status: -- Completed Date: -EOF -wf "${T}" resume "${work_id}" --phase code >/dev/null -wf "${T}" sync "${work_id}" >/dev/null -op="$(grep '^operation=' "${T}/sdlc-spdd/.sdlc/workflows/${work_id}.state" | cut -d= -f2 || true)" -if [[ -z "${op}" ]]; then - ok "all-complete canvas has empty next operation" -else - bad "expected empty operation, got '${op}'" -fi -out="$(wf "${T}" next)" -if grep -q 'all canvas operations complete' <<< "${out}"; then - ok "next reports all operations complete" -else - bad "next should say all operations complete: ${out}" -fi - -# --------------------------------------------------------------------------- -echo "== Test 12c: code phase with Needs Analysis redirects next to architect ==" -T="${WORK}/ops-readiness" -work_id="FEAT-012c-readiness" -setup_feature "${T}" "${work_id}" -cat > "${T}/sdlc-spdd/spdd/canvas/${work_id}.md" <<'EOF' -# REASONS Canvas: FEAT-012c-readiness - -## Metadata -- Work ID: FEAT-012c-readiness -- Status: In Progress -- Readiness: Needs Analysis - -## O - Operations - -### T01 - First - -- Status: Not Started - -## Final Status - -- Status: -EOF -wf "${T}" resume "${work_id}" --phase code >/dev/null -out="$(wf "${T}" next)" -if grep -q 'sdlc-spdd-architect' <<< "${out}" && grep -q 'not Ready For Coding' <<< "${out}"; then - ok "next redirects to architect when readiness blocks coding" -else - bad "next should recommend architect when Needs Analysis: ${out}" -fi -json="$(wf "${T}" status --json)" -if grep -q '"readiness":"needs-analysis"' <<< "${json}" \ - && grep -q 'sdlc-spdd-architect' <<< "${json}"; then - ok "json readiness + recommended_command reflect gate" -else - bad "json missing readiness gate fields: ${json}" -fi - -# --------------------------------------------------------------------------- -echo "== Test 12d: advance to code refused when readiness blocks coding ==" -T="${WORK}/advance-readiness" -work_id="FEAT-012d-advance" -setup_feature "${T}" "${work_id}" -cat > "${T}/sdlc-spdd/spdd/canvas/${work_id}.md" <<'EOF' -# REASONS Canvas: FEAT-012d-advance - -## Metadata -- Work ID: FEAT-012d-advance -- Status: In Progress -- Readiness: Needs Clarification - -## O - Operations - -### T01 - First - -- Status: Not Started - -## Final Status - -- Status: -EOF -wf "${T}" resume "${work_id}" --phase architect >/dev/null -if wf "${T}" advance >/dev/null 2>"${WORK}/advance-err.txt"; then - bad "advance architect→code should fail when Needs Clarification" -else - if grep -q "not Ready For Coding" "${WORK}/advance-err.txt"; then - ok "advance refuses code when readiness blocks" - else - bad "advance error missing readiness message: $(cat "${WORK}/advance-err.txt")" - fi -fi -phase="$(grep '^phase=' "${T}/sdlc-spdd/.sdlc/workflows/${work_id}.state" | cut -d= -f2)" -if [[ "${phase}" == "architect" ]]; then - ok "phase stays architect after refused advance" -else - bad "expected phase architect, got ${phase}" -fi -if wf "${T}" advance --force >/dev/null; then - ok "advance --force overrides readiness gate" -else - bad "advance --force should succeed" -fi -phase="$(grep '^phase=' "${T}/sdlc-spdd/.sdlc/workflows/${work_id}.state" | cut -d= -f2)" -if [[ "${phase}" == "code" ]]; then - ok "force advance reaches code" -else - bad "expected phase code after --force, got ${phase}" -fi - -# --------------------------------------------------------------------------- -echo "== Test 12e: resume --phase code warns when readiness blocks ==" -T="${WORK}/resume-readiness" -work_id="FEAT-012e-resume" -setup_feature "${T}" "${work_id}" -cat > "${T}/sdlc-spdd/spdd/canvas/${work_id}.md" <<'EOF' -# REASONS Canvas: FEAT-012e-resume - -## Metadata -- Work ID: FEAT-012e-resume -- Readiness: Blocked - -## O - Operations - -### T01 - First -- Status: Not Started -EOF -out="$(wf "${T}" resume "${work_id}" --phase code)" -if grep -q 'not Ready For Coding' <<< "${out}" && grep -q 'sdlc-spdd-architect' <<< "${out}"; then - ok "resume warns and recommends architect when blocked" -else - bad "resume missing readiness warning: ${out}" -fi -out="$(wf "${T}" next)" -if grep -q 'Readiness: blocked' <<< "${out}"; then - ok "next prints Readiness line" -else - bad "next missing Readiness line: ${out}" -fi - -# --------------------------------------------------------------------------- -echo "== Test 12f: advance to code succeeds when Ready For Coding ==" -T="${WORK}/advance-ok" -work_id="FEAT-012f-ok" -setup_feature "${T}" "${work_id}" -cat > "${T}/sdlc-spdd/spdd/canvas/${work_id}.md" <<'EOF' -# REASONS Canvas: FEAT-012f-ok - -## Metadata -- Work ID: FEAT-012f-ok -- Readiness: Ready For Coding - -## O - Operations - -### T01 - First -- Status: Not Started -EOF -wf "${T}" resume "${work_id}" --phase architect >/dev/null -if wf "${T}" advance >/dev/null; then - ok "advance architect→code when Ready For Coding" -else - bad "advance should succeed when Ready For Coding" -fi -phase="$(grep '^phase=' "${T}/sdlc-spdd/.sdlc/workflows/${work_id}.state" | cut -d= -f2)" -if [[ "${phase}" == "code" ]]; then ok "phase is code after ready advance"; else bad "expected code, got ${phase}"; fi -out="$(wf "${T}" next)" -if grep -q 'sdlc-spdd-code' <<< "${out}"; then ok "next recommends code when ready"; else bad "next should recommend code: ${out}"; fi - -# --------------------------------------------------------------------------- -echo "== Test 12g: absent readiness still allows advance to code (compat) ==" -T="${WORK}/advance-absent" -work_id="FEAT-012g-absent" -setup_feature "${T}" "${work_id}" -cat > "${T}/sdlc-spdd/spdd/canvas/${work_id}.md" <<'EOF' -# REASONS Canvas: FEAT-012g-absent - -## Metadata -- Work ID: FEAT-012g-absent -- Status: In Progress - -## O - Operations - -### T01 - First -- Status: Not Started -EOF -wf "${T}" resume "${work_id}" --phase architect >/dev/null -if wf "${T}" advance >/dev/null; then - ok "advance allowed when readiness absent" -else - bad "absent readiness should not block advance" -fi - -# --------------------------------------------------------------------------- -echo "== Test 12h: YAML readiness + brief Readiness row + gate inference ==" -T="${WORK}/yaml-ready" -work_id="FEAT-012h-yaml" -setup_feature "${T}" "${work_id}" -cat > "${T}/sdlc-spdd/spdd/canvas/${work_id}.md" <<'EOF' ---- -readiness: needs-redesign ---- -# REASONS Canvas: FEAT-012h-yaml - -## Metadata -- Work ID: FEAT-012h-yaml - -## O - Operations - -### T01 - First -- Status: Not Started -EOF -wf "${T}" resume "${work_id}" --phase code >/dev/null -wf "${T}" sync "${work_id}" >/dev/null -json="$(wf "${T}" status --json)" -if grep -q '"readiness":"needs-redesign"' <<< "${json}"; then - ok "json reads YAML readiness" -else - bad "json YAML readiness: ${json}" -fi -brief="$(SDLC_ROOT="${T}" bash -c "source '${T}/sdlc-spdd/scripts/sdlc-workflow.sh'; sdlc_workflow_brief_markdown '${work_id}'")" -if grep -q '| Readiness | needs-redesign |' <<< "${brief}"; then - ok "brief includes Readiness row" -else - bad "brief missing Readiness: ${brief}" -fi -# Sync should not mark architect_review passed for needs-redesign -gate="$(grep '^gate_architect_review=' "${T}/sdlc-spdd/.sdlc/workflows/${work_id}.state" | cut -d= -f2 || true)" -if [[ "${gate}" != "passed" ]]; then - ok "architect_review not auto-passed for needs-redesign" -else - bad "architect_review should not be passed for needs-redesign (got ${gate})" -fi - -# --------------------------------------------------------------------------- -echo "== Test 12i: start-agent-session recommends architect when code blocked ==" -T="${WORK}/start-ready" -work_id="FEAT-012i-start" -setup_feature "${T}" "${work_id}" -mkdir -p "${T}/sdlc-spdd/scripts/lib" -cp "${REPO_ROOT}/scripts/lib/"*.sh "${T}/sdlc-spdd/scripts/lib/" -cat > "${T}/sdlc-spdd/spdd/canvas/${work_id}.md" <<'EOF' -# REASONS Canvas: FEAT-012i-start - -## Metadata -- Work ID: FEAT-012i-start -- Readiness: Needs Analysis - -## O - Operations -### T01 - First -- Status: Not Started -EOF -# Point SDLC_ROOT at T so workflow readiness lib resolves; start uses TARGET workflow copy -out="$("${START}" --target "${T}" --work-id "${work_id}" --phase code 2>&1)" -brief="${T}/sdlc-spdd/.sdlc/sessions/current-session.md" -if grep -q 'sdlc-spdd-architect' "${brief}" && grep -q 'Readiness | needs-analysis' "${brief}"; then - ok "session brief readiness-gates code recommendation" -else - bad "brief should recommend architect + show readiness: $(grep -E 'Recommended|Readiness' "${brief}" || true)" -fi - -# --------------------------------------------------------------------------- -echo "== Test 12j: advance --to code from plan refused when blocked ==" -T="${WORK}/advance-to" -work_id="FEAT-012j-to" -setup_feature "${T}" "${work_id}" -cat > "${T}/sdlc-spdd/spdd/canvas/${work_id}.md" <<'EOF' -# REASONS Canvas: FEAT-012j-to - -## Metadata -- Work ID: FEAT-012j-to -- Readiness: Blocked - -## O - Operations -### T01 - First -- Status: Not Started -EOF -wf "${T}" resume "${work_id}" --phase plan >/dev/null -if wf "${T}" advance --to code >/dev/null 2>"${WORK}/advance-to-err.txt"; then - bad "advance --to code should refuse when Blocked" -else - if grep -q "not Ready For Coding" "${WORK}/advance-to-err.txt"; then - ok "advance --to code refuses when Blocked" - else - bad "missing readiness error: $(cat "${WORK}/advance-to-err.txt")" - fi -fi -phase="$(grep '^phase=' "${T}/sdlc-spdd/.sdlc/workflows/${work_id}.state" | cut -d= -f2)" -if [[ "${phase}" == "plan" ]]; then ok "phase stays plan after refused --to code"; else bad "expected plan, got ${phase}"; fi - -# Ready For Coding Metadata passes architect_review on sync (no analysis file required) -T="${WORK}/gate-meta" -work_id="FEAT-012j-gate" -setup_feature "${T}" "${work_id}" -cat > "${T}/sdlc-spdd/spdd/canvas/${work_id}.md" <<'EOF' -# REASONS Canvas: FEAT-012j-gate - -## Metadata -- Work ID: FEAT-012j-gate -- Readiness: Ready For Coding - -## O - Operations -### T01 - First -- Status: Not Started -EOF -wf "${T}" resume "${work_id}" --phase architect >/dev/null -wf "${T}" sync "${work_id}" >/dev/null -gate="$(grep '^gate_architect_review=' "${T}/sdlc-spdd/.sdlc/workflows/${work_id}.state" | cut -d= -f2 || true)" -if [[ "${gate}" == "passed" ]]; then - ok "sync passes architect_review from Metadata Ready For Coding" -else - bad "expected architect_review=passed, got '${gate}'" -fi -gate_c="$(grep '^gate_canvas_exists=' "${T}/sdlc-spdd/.sdlc/workflows/${work_id}.state" | cut -d= -f2 || true)" -if [[ "${gate_c}" == "passed" ]]; then - ok "sync passes canvas_exists without analysis artifact" -else - bad "expected canvas_exists=passed without analysis, got '${gate_c}'" -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}/sdlc-spdd/scripts/lib" -cp "${REPO_ROOT}/scripts/sdlc.sh" "${T}/sdlc-spdd/scripts/sdlc.sh" -cp "${CAPTURE}" "${T}/sdlc-spdd/scripts/capture-session-memory.sh" -# capture-session-memory.sh sources /scripts/lib/*.sh (FEAT-001) -cp "${REPO_ROOT}/scripts/lib/"*.sh "${T}/sdlc-spdd/scripts/lib/" -chmod +x "${T}/sdlc-spdd/scripts/sdlc.sh" "${T}/sdlc-spdd/scripts/capture-session-memory.sh" -SDLC_ROOT="${T}" "${T}/sdlc-spdd/scripts/sdlc.sh" resume "${work_id}" >/dev/null -if SDLC_ROOT="${T}" "${T}/sdlc-spdd/scripts/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}/sdlc-spdd/scripts/sdlc.sh" resume FEAT-999-other >/dev/null 2>&1 || true -if SDLC_ROOT="${T}" "${T}/sdlc-spdd/scripts/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 "== 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 registry_matches "${T}" "${work_id}" '"status": "active"'; 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 14b: claim --force takes over foreign claim ==" -T="${WORK}/team-claim-force" -work_id="FEAT-009b-force" -setup_feature "${T}" "${work_id}" -mkdir -p "${T}/sdlc-spdd/scripts" -cp "${REPO_ROOT}/scripts/sdlc.sh" "${T}/sdlc-spdd/scripts/sdlc.sh" -chmod +x "${T}/sdlc-spdd/scripts/sdlc.sh" -SDLC_USER="alice" SDLC_ROOT="${T}" wf "${T}" claim "${work_id}" >/dev/null -if SDLC_USER="bob" SDLC_ROOT="${T}" "${T}/sdlc-spdd/scripts/sdlc.sh" claim "${work_id}" >/dev/null 2>&1; then - bad "claim without --force should refuse foreign owner" -else - ok "claim without --force refuses foreign owner" -fi -if SDLC_USER="bob" SDLC_ROOT="${T}" "${T}/sdlc-spdd/scripts/sdlc.sh" claim "${work_id}" --force >"${T}/claim-force.out" 2>"${T}/claim-force.err"; then - if registry_matches "${T}" "${work_id}" '"status": "active".*"owner": "bob"'; then - ok "claim --force takes over via sdlc.sh wrapper" - else - bad "claim --force succeeded but owner not bob" - fi - takeover_count="$(grep -c 'Taking over' "${T}/claim-force.err" || true)" - if [[ "${takeover_count}" -eq 1 ]]; then - ok "claim --force prints Taking over once" - else - bad "claim --force Taking over count=${takeover_count} (want 1)" - fi -else - bad "claim --force should succeed" -fi - -# --------------------------------------------------------------------------- -echo "== Test 15: list-work discovers repo Work IDs ==" -T="${WORK}/team" -work_id="FEAT-009-team" -printf '# REASONS Canvas: %s\n\n## Metadata\n\n- Work ID: %s\n' "${work_id}" "${work_id}" \ - > "${T}/sdlc-spdd/spdd/canvas/${work_id}.md" -# reuse team fixture from Test 14 (bob owns after --force resume) -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 '%s\n' \ - '{"event":"claim","work_id":"FEAT-010-stale","status":"active","phase":"code","operation":"","owner":"alice","note":"","ts":"2020-01-01T00:00:00Z"}' \ - > "${T}/sdlc-spdd/spdd/memory/registry.jsonl" -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}" -cat > "${T}/sdlc-spdd/spdd/canvas/${work_id}.md" <<'EOF' -# CHORE-001-done - -## Final Status - -- Status: Complete -EOF -SDLC_ROOT="${T}" wf "${T}" sync-team >/dev/null -if registry_matches "${T}" "CHORE-001-done" '"status": "done"'; 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 registry_matches "${T}" "${work_id}" 'branch:cursor/feat-011' \ - && registry_matches "${T}" "${work_id}" 'pr:#99'; 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}/sdlc-spdd/scripts/hooks" -cat > "${T}/sdlc-spdd/scripts/hooks/notify.sh" <> "${hook_log}" -EOF -chmod +x "${T}/sdlc-spdd/scripts/hooks/notify.sh" -SDLC_TEAM_REGISTRY_HOOK="${T}/sdlc-spdd/scripts/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 "== 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}/sdlc-spdd/requirements/milestones" -cat > "${T}/sdlc-spdd/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 registry_matches "${T}" "${work_id}" 'jira:ORCH-42'; 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 "== Test 21: agent-driven Jira ask on missing / draft / present ==" -T="${WORK}/jira-ask-missing" -work_id="FEAT-014-jira-ask" -setup_feature "${T}" "${work_id}" -wf "${T}" resume "${work_id}" >/dev/null -out="$(SDLC_ROOT="${T}" wf "${T}" next)" -if grep -q 'Jira: missing' <<< "${out}" && grep -q 'Tracker follow-up:' <<< "${out}" \ - && grep -q 'Ask the user for the issue key' <<< "${out}"; then - ok "next asks when Jira missing" -else - bad "next should ask when Jira missing" -fi -"${START}" --target "${T}" --work-id "${work_id}" --phase plan >/dev/null -current="${T}/sdlc-spdd/.sdlc/sessions/current-session.md" -if grep -q '^- Jira: missing$' "${current}" \ - && grep -A20 '## Resume Prompt' "${current}" | grep -q 'Tracker link: Jira key is missing'; then - ok "session brief Resume Prompt asks when Jira missing" -else - bad "session brief should ask when Jira missing" -fi - -T="${WORK}/jira-ask-draft" -work_id="FEAT-015-jira-draft" -setup_feature "${T}" "${work_id}" -mkdir -p "${T}/sdlc-spdd/requirements/milestones" -cat > "${T}/sdlc-spdd/requirements/milestones/${work_id}.md" <<'EOF' -# Requirement: FEAT-015-jira-draft - -## Jira - -- Summary: draft without key yet -EOF -wf "${T}" resume "${work_id}" >/dev/null -out="$(SDLC_ROOT="${T}" wf "${T}" next)" -if grep -q 'Jira: draft' <<< "${out}" && grep -q 'Jira draft exists' <<< "${out}"; then - ok "next asks when Jira draft" -else - bad "next should ask when Jira draft" -fi -"${START}" --target "${T}" --work-id "${work_id}" --phase plan >/dev/null -current="${T}/sdlc-spdd/.sdlc/sessions/current-session.md" -if grep -A20 '## Resume Prompt' "${current}" | grep -q 'Jira draft exists'; then - ok "session brief asks when Jira draft" -else - bad "session brief should ask when Jira draft" -fi - -T="${WORK}/jira-ask-present" -work_id="FEAT-016-jira-present" -setup_feature "${T}" "${work_id}" -mkdir -p "${T}/sdlc-spdd/requirements/milestones" -cat > "${T}/sdlc-spdd/requirements/milestones/${work_id}.md" <<'EOF' -# Requirement: FEAT-016-jira-present - -## Jira - -- Key: ORCH-99 -EOF -wf "${T}" resume "${work_id}" >/dev/null -out="$(SDLC_ROOT="${T}" wf "${T}" next)" -if grep -q 'Jira: ORCH-99' <<< "${out}" && ! grep -q 'Tracker follow-up:' <<< "${out}"; then - ok "next shows key and skips ask when present" -else - bad "next should not ask when Jira key present" -fi -"${START}" --target "${T}" --work-id "${work_id}" --phase plan >/dev/null -current="${T}/sdlc-spdd/.sdlc/sessions/current-session.md" -if grep -q '^- Jira: ORCH-99$' "${current}" \ - && grep -A20 '## Resume Prompt' "${current}" | grep -q 'Jira: ORCH-99' \ - && ! grep -A20 '## Resume Prompt' "${current}" | grep -q 'Ask the user for the issue key'; then - ok "session brief records key without ask" -else - bad "session brief should record key without ask" -fi - -T="${WORK}/jira-ask-disabled" -work_id="FEAT-017-jira-off" -setup_feature "${T}" "${work_id}" -wf "${T}" resume "${work_id}" >/dev/null -out="$(SDLC_SESSION_ASK_JIRA=0 SDLC_ROOT="${T}" wf "${T}" next)" -if grep -q 'Jira: missing' <<< "${out}" && ! grep -q 'Tracker follow-up:' <<< "${out}"; then - ok "SDLC_SESSION_ASK_JIRA=0 suppresses ask" -else - bad "SDLC_SESSION_ASK_JIRA=0 should suppress ask" -fi -SDLC_SESSION_ASK_JIRA=0 "${START}" --target "${T}" --work-id "${work_id}" --phase plan >/dev/null -current="${T}/sdlc-spdd/.sdlc/sessions/current-session.md" -if ! grep -A20 '## Resume Prompt' "${current}" | grep -q 'Ask the user for the issue key'; then - ok "start respects SDLC_SESSION_ASK_JIRA=0" -else - bad "start should honor SDLC_SESSION_ASK_JIRA=0" -fi - -# --------------------------------------------------------------------------- -echo "== Test 22: code capture / complete without verify receipt refuse ==" -T="${WORK}/i1-receipt" -work_id="FEAT-018-i1" -setup_feature "${T}" "${work_id}" -mkdir -p "${T}/sdlc-spdd/scripts/lib" -cp "${REPO_ROOT}/scripts/sdlc.sh" "${T}/sdlc-spdd/scripts/sdlc.sh" -cp "${CAPTURE}" "${T}/sdlc-spdd/scripts/capture-session-memory.sh" -cp "${REPO_ROOT}/scripts/lib/"*.sh "${T}/sdlc-spdd/scripts/lib/" -chmod +x "${T}/sdlc-spdd/scripts/sdlc.sh" "${T}/sdlc-spdd/scripts/capture-session-memory.sh" -SDLC_ENGINE=shell SDLC_ROOT="${T}" "${T}/sdlc-spdd/scripts/sdlc.sh" resume "${work_id}" --phase code >/dev/null -if SDLC_ENGINE=shell SDLC_ROOT="${T}" "${T}/sdlc-spdd/scripts/sdlc.sh" \ - capture --phase code --summary "T01 complete" >/dev/null 2>&1; then - bad "code capture without receipt should refuse" -else - ok "code capture without receipt refuses" -fi -if SDLC_ENGINE=shell SDLC_ROOT="${T}" "${T}/sdlc-spdd/scripts/sdlc.sh" \ - complete --summary "T01 complete" >/dev/null 2>&1; then - bad "complete without receipt should refuse" -else - ok "complete without receipt refuses" -fi - -# --------------------------------------------------------------------------- -echo -echo "Results: ${pass} passed, ${fail} failed" -if [[ "${fail}" -gt 0 ]]; then - exit 1 -fi From 44250f111e4941ded12fc4d37e8c0ebe33c79362 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 14 Sep 2026 05:02:31 +0000 Subject: [PATCH 11/18] test: fix Python-only harness fixtures Co-authored-by: John Menke --- tests/research/test_ref001_sut.py | 2 +- tests/test-gate-review-validation.sh | 4 ++-- tests/test-integration-merge.sh | 8 ++++++++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/research/test_ref001_sut.py b/tests/research/test_ref001_sut.py index 8b41ed2b..f2b2275f 100644 --- a/tests/research/test_ref001_sut.py +++ b/tests/research/test_ref001_sut.py @@ -151,7 +151,7 @@ def test_python_parser_has_no_top_level_capture(self) -> None: with redirect_stderr(StringIO()), self.assertRaises(SystemExit): parser.parse_args(["capture", "--summary", "ok"]) text = SDLC_SH.read_text(encoding="utf-8") - self.assertIn("capture|complete|start|accept", text) + self.assertIn("capture|/sdlc-workflow-capture", text) if __name__ == "__main__": diff --git a/tests/test-gate-review-validation.sh b/tests/test-gate-review-validation.sh index dcd3e3b4..9dcc176d 100755 --- a/tests/test-gate-review-validation.sh +++ b/tests/test-gate-review-validation.sh @@ -77,7 +77,7 @@ if ! sdlc "${T}" skip api-test --reason "skip tests" >/dev/null; then else ok "skipped tests (api-test)" fi -if out="$(sdlc "${T}" gate review --work-id "${work_id}" 2>&1)"; then +if out="$(sdlc "${T}" gate --phase review --work-id "${work_id}" 2>&1)"; then bad "gate review must fail after dummy lesson + skip tests: ${out}" else if grep -q 'Validation receipt' <<< "${out}"; then @@ -101,7 +101,7 @@ if sdlc "${T}" capture --phase code --summary "T01 complete" \ else bad "code capture with receipt should succeed" fi -if out="$(sdlc "${T}" gate review --work-id "${work_id}" 2>&1)"; then +if out="$(sdlc "${T}" gate --phase review --work-id "${work_id}" 2>&1)"; then ok "gate review passes with Validation receipt" else bad "gate review should pass with receipt: ${out}" diff --git a/tests/test-integration-merge.sh b/tests/test-integration-merge.sh index f740079e..8d0149bf 100755 --- a/tests/test-integration-merge.sh +++ b/tests/test-integration-merge.sh @@ -3,6 +3,14 @@ set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +if [[ -x "${REPO_ROOT}/.venv/bin/python" ]]; then + export PYTHON="${REPO_ROOT}/.venv/bin/python" +elif command -v python3.12 >/dev/null 2>&1; then + export PYTHON="$(command -v python3.12)" +else + export PYTHON="$(command -v python3)" +fi +export PYTHONPATH="${REPO_ROOT}/engine/src${PYTHONPATH:+:${PYTHONPATH}}" SETUP="${REPO_ROOT}/scripts/setup-agent-prompts.sh" UPGRADE="${REPO_ROOT}/scripts/upgrade-project.sh" VALIDATE="${REPO_ROOT}/scripts/validate-command-adapters.sh" From 563b26cf84e30a2761a8d8d7efe59227f0f975eb Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 14 Sep 2026 05:02:55 +0000 Subject: [PATCH 12/18] test: use Python gate argument contract Co-authored-by: John Menke --- tests/test-gate-advisory-visible.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test-gate-advisory-visible.sh b/tests/test-gate-advisory-visible.sh index 782d9fa1..a85d8327 100755 --- a/tests/test-gate-advisory-visible.sh +++ b/tests/test-gate-advisory-visible.sh @@ -73,7 +73,7 @@ work_id="FEAT-014-advisory-visible" setup_feature "${T}" write_requirement "${T}" "${work_id}" write_canvas "${T}" "${work_id}" -if out="$(sdlc "${T}" gate architect --work-id "${work_id}" 2>&1)"; then +if out="$(sdlc "${T}" gate --phase architect --work-id "${work_id}" 2>&1)"; then if grep -q 'Architect review completed (advisory)' <<< "${out}" \ && grep -q 'Operations are task-sized (advisory)' <<< "${out}" \ && grep -q 'did not run' <<< "${out}"; then @@ -89,7 +89,7 @@ echo "== test_gate_architect_blocked_still_shows_advisory ==" T="${WORK}/architect-blocked" work_id="FEAT-014-advisory-blocked" setup_feature "${T}" -if out="$(sdlc "${T}" gate architect --work-id "${work_id}" 2>&1)"; then +if out="$(sdlc "${T}" gate --phase architect --work-id "${work_id}" 2>&1)"; then bad "gate architect must fail without canvas/requirement: ${out}" else if grep -q 'BLOCKED' <<< "${out}" \ @@ -106,7 +106,7 @@ T="${WORK}/analysis-ok" work_id="FEAT-014-no-advisory" setup_feature "${T}" write_requirement "${T}" "${work_id}" -if out="$(sdlc "${T}" gate analysis --work-id "${work_id}" 2>&1)"; then +if out="$(sdlc "${T}" gate --phase analysis --work-id "${work_id}" 2>&1)"; then if grep -q '(advisory)' <<< "${out}" || grep -q 'did not run' <<< "${out}"; then bad "gate analysis should not invent advisory rows: ${out}" else From 7fd25131bc01b0e2fb0c387beb531705b01eea3a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 14 Sep 2026 05:03:34 +0000 Subject: [PATCH 13/18] test: provide Python engine to live consumers Co-authored-by: John Menke --- tests/live-consumer/lib.sh | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/live-consumer/lib.sh b/tests/live-consumer/lib.sh index 82048598..95cc6bfc 100755 --- a/tests/live-consumer/lib.sh +++ b/tests/live-consumer/lib.sh @@ -7,6 +7,17 @@ SEED_DIR="${LIVE_CONSUMER_DIR}/seed" WORK_ID="${LIVE_WORK_ID:-FEAT-001-hello-live}" DONE_WORK_ID="${LIVE_DONE_WORK_ID:-FEAT-002-done-live}" SDLC_USER="${SDLC_USER:-live-matrix}" +if [[ -z "${PYTHON:-}" ]]; then + if [[ -x "${REPO_ROOT}/.venv/bin/python" ]]; then + PYTHON="${REPO_ROOT}/.venv/bin/python" + elif command -v python3.12 >/dev/null 2>&1; then + PYTHON="$(command -v python3.12)" + else + PYTHON="$(command -v python3)" + fi +fi +export PYTHON +export PYTHONPATH="${REPO_ROOT}/engine/src${PYTHONPATH:+:${PYTHONPATH}}" # Default: ephemeral mktemp. For Cursor UI reopen, set: # LIVE_CONSUMER_ROOT=/tmp/sdlc-spdd-live LIVE_CONSUMER_KEEP=1 From 844ed73b3c048d55049cbcdf27e31269f0229a27 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 14 Sep 2026 05:11:57 +0000 Subject: [PATCH 14/18] test: finish Python-only consumer migration Co-authored-by: John Menke --- tests/live-consumer/cursor-agent/run-persistence-test.mjs | 4 ++-- tests/live-consumer/cursor-agent/run-slash-matrix.mjs | 4 ++-- tests/test-commit-message-command.sh | 2 +- tests/test-sunset-command.sh | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/live-consumer/cursor-agent/run-persistence-test.mjs b/tests/live-consumer/cursor-agent/run-persistence-test.mjs index 0e1b0f3b..e4934db5 100755 --- a/tests/live-consumer/cursor-agent/run-persistence-test.mjs +++ b/tests/live-consumer/cursor-agent/run-persistence-test.mjs @@ -330,8 +330,8 @@ async function main() { else bad(`resumed agent status=${result.status}`); const ptr = sh( - path.join(liveScripts(ROOT), "sdlc-pointer.sh"), - ["get"], + path.join(liveScripts(ROOT), "sdlc.sh"), + ["pointer", "get"], { env: { SDLC_ROOT: ROOT } }, ); if ((ptr.stdout || "").trim() === WORK_ID) ok("pointer still claimed after resume"); diff --git a/tests/live-consumer/cursor-agent/run-slash-matrix.mjs b/tests/live-consumer/cursor-agent/run-slash-matrix.mjs index d9eb444f..321225a2 100755 --- a/tests/live-consumer/cursor-agent/run-slash-matrix.mjs +++ b/tests/live-consumer/cursor-agent/run-slash-matrix.mjs @@ -241,8 +241,8 @@ function runVerify(step) { function checkPointerClaimed() { const r = spawnSync( - path.join(liveScripts(ROOT), "sdlc-pointer.sh"), - ["get"], + path.join(liveScripts(ROOT), "sdlc.sh"), + ["pointer", "get"], { encoding: "utf8", env: { ...process.env, SDLC_ROOT: ROOT } }, ); return (r.stdout || "").trim() === WORK_ID; diff --git a/tests/test-commit-message-command.sh b/tests/test-commit-message-command.sh index 2540fac7..b67f5034 100755 --- a/tests/test-commit-message-command.sh +++ b/tests/test-commit-message-command.sh @@ -61,7 +61,7 @@ for path in "${CURSOR}" "${COPILOT}" "${CLAUDE}"; do assert_contains "${path}" "Paste-ready commit message" "paste-ready output (${path##*/})" assert_absent "${path}" "PR review comment" "not a PR review command (${path##*/})" done -assert_contains "${SDLC_SH}" 'commit-message)' "sdlc.sh routes commit-message to Python engine" +assert_contains "${SDLC_SH}" '_engine_args=("${cmd}" "$@")' "sdlc.sh routes commands to Python engine" echo "== Test 3: generator --check and adapter validation ==" if "${REPO_ROOT}/scripts/generate-command-adapters.sh" --check >/dev/null; then diff --git a/tests/test-sunset-command.sh b/tests/test-sunset-command.sh index 8b2e9f3e..e9cd9576 100755 --- a/tests/test-sunset-command.sh +++ b/tests/test-sunset-command.sh @@ -49,7 +49,7 @@ for path in "${CURSOR}" "${COPILOT}" "${CLAUDE}"; do assert_contains "${path}" "lessons.jsonl" "ledger destination (${path##*/})" assert_contains "${path}" "--apply" "apply stages snapshot (${path##*/})" done -assert_contains "${SDLC_SH}" 'sunset)' "sdlc.sh routes sunset to Python engine" +assert_contains "${SDLC_SH}" '_engine_args=("${cmd}" "$@")' "sdlc.sh routes commands to Python engine" echo "== Test 3: generator --check and adapter validation ==" if "${REPO_ROOT}/scripts/generate-command-adapters.sh" --check >/dev/null; then From 3c0da11cbdc779c9b57133f8ac3d908ddd482780 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 14 Sep 2026 05:24:34 +0000 Subject: [PATCH 15/18] refactor: extract shell bridge script resolution Keeps cmd_shell within the diff complexity gate by moving candidate enumeration and lookup into dedicated helpers. Co-authored-by: John Menke --- engine/src/sdlc_engine/cli_commands.py | 27 +++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/engine/src/sdlc_engine/cli_commands.py b/engine/src/sdlc_engine/cli_commands.py index 5e1ed23c..ef0853c6 100644 --- a/engine/src/sdlc_engine/cli_commands.py +++ b/engine/src/sdlc_engine/cli_commands.py @@ -476,22 +476,23 @@ def cmd_version(_: argparse.Namespace) -> int: return 0 +def _shell_script_candidates(root: Path, script: str) -> list[Path]: + names = [script] if script.endswith(".sh") else [script, f"{script}.sh"] + directories = [root / "scripts", root / "sdlc-spdd" / "scripts"] + return [directory / name for directory in directories for name in names] + + +def _resolve_shell_script(root: Path, script: str) -> Path | None: + for candidate in _shell_script_candidates(root, script): + if candidate.is_file(): + return candidate + return None + + def cmd_shell(args: argparse.Namespace) -> int: """Bridge to retained shell utilities in source or installed layouts.""" root = _project(args).root - script_names = [args.script] - if not args.script.endswith(".sh"): - script_names.append(f"{args.script}.sh") - script_dirs = [root / "scripts", root / "sdlc-spdd" / "scripts"] - script = next( - ( - directory / name - for directory in script_dirs - for name in script_names - if (directory / name).is_file() - ), - None, - ) + script = _resolve_shell_script(root, args.script) if script is None: print(f"shell bridge: script not found: {args.script}", file=sys.stderr) return 1 From f5fc1636e96df51e2d9ad23608761a86399dbc25 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 14 Sep 2026 05:27:31 +0000 Subject: [PATCH 16/18] test: resolve harness Python without a hardcoded repo venv CI installs the engine into the job interpreter and never creates a repo .venv, so harnesses pinning .venv/bin/python handed the dispatcher a path that does not exist. Share one resolver across the shell harnesses and make resolve_engine_python report an unrunnable interpreter instead of a truncated version. Co-authored-by: John Menke --- scripts/lib/python.sh | 12 ++++++----- sdlc-spdd/scripts/lib/python.sh | 12 ++++++----- tests/lib/engine-python.sh | 28 +++++++++++++++++++++++++ tests/live-consumer/lib.sh | 10 +++------ tests/test-gate-advisory-visible.sh | 5 ++++- tests/test-gate-review-validation.sh | 5 ++++- tests/test-integration-merge.sh | 11 ++++------ tests/test-scripts-lib.sh | 31 ++++++++++++++++++++++++++++ tests/test-verify-receipt.sh | 5 ++++- 9 files changed, 92 insertions(+), 27 deletions(-) create mode 100644 tests/lib/engine-python.sh diff --git a/scripts/lib/python.sh b/scripts/lib/python.sh index bd2496ec..3d3a310b 100644 --- a/scripts/lib/python.sh +++ b/scripts/lib/python.sh @@ -47,7 +47,7 @@ pick_bootstrap_python() { resolve_engine_python() { local root="${SDLC_ROOT:-${ROOT:-}}" - local major minor + local major minor version if [[ -n "${PYTHON:-}" ]]; then SDLC_PY="${PYTHON}" @@ -63,12 +63,14 @@ resolve_engine_python() { return 1 fi - read -r major minor <<<"$("${SDLC_PY}" -c 'import sys; print(sys.version_info.major, sys.version_info.minor)' 2>/dev/null)" || { - echo "error: cannot run ${SDLC_PY}" >&2 + version="$("${SDLC_PY}" -c 'import sys; print(sys.version_info.major, sys.version_info.minor)' 2>/dev/null)" + if [[ -z "${version}" ]]; then + echo "error: cannot run ${SDLC_PY} — sdlc-engine requires Python 3.12 (./scripts/setup-engine-venv.sh)" >&2 return 1 - } + fi + read -r major minor <<<"${version}" if (( major != 3 || minor != 12 )); then - echo "error: ${SDLC_PY} is 3.${minor} — sdlc-engine requires Python 3.12 (./scripts/setup-engine-venv.sh)" >&2 + echo "error: ${SDLC_PY} is ${major}.${minor} — sdlc-engine requires Python 3.12 (./scripts/setup-engine-venv.sh)" >&2 return 1 fi export SDLC_PY diff --git a/sdlc-spdd/scripts/lib/python.sh b/sdlc-spdd/scripts/lib/python.sh index bd2496ec..3d3a310b 100644 --- a/sdlc-spdd/scripts/lib/python.sh +++ b/sdlc-spdd/scripts/lib/python.sh @@ -47,7 +47,7 @@ pick_bootstrap_python() { resolve_engine_python() { local root="${SDLC_ROOT:-${ROOT:-}}" - local major minor + local major minor version if [[ -n "${PYTHON:-}" ]]; then SDLC_PY="${PYTHON}" @@ -63,12 +63,14 @@ resolve_engine_python() { return 1 fi - read -r major minor <<<"$("${SDLC_PY}" -c 'import sys; print(sys.version_info.major, sys.version_info.minor)' 2>/dev/null)" || { - echo "error: cannot run ${SDLC_PY}" >&2 + version="$("${SDLC_PY}" -c 'import sys; print(sys.version_info.major, sys.version_info.minor)' 2>/dev/null)" + if [[ -z "${version}" ]]; then + echo "error: cannot run ${SDLC_PY} — sdlc-engine requires Python 3.12 (./scripts/setup-engine-venv.sh)" >&2 return 1 - } + fi + read -r major minor <<<"${version}" if (( major != 3 || minor != 12 )); then - echo "error: ${SDLC_PY} is 3.${minor} — sdlc-engine requires Python 3.12 (./scripts/setup-engine-venv.sh)" >&2 + echo "error: ${SDLC_PY} is ${major}.${minor} — sdlc-engine requires Python 3.12 (./scripts/setup-engine-venv.sh)" >&2 return 1 fi export SDLC_PY diff --git a/tests/lib/engine-python.sh b/tests/lib/engine-python.sh new file mode 100644 index 00000000..c48e545b --- /dev/null +++ b/tests/lib/engine-python.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# Shared Python 3.12 resolution for shell harnesses. +# shellcheck shell=bash +# +# Harnesses drive the dispatcher against a temporary SDLC_ROOT, so the repo +# venv is never auto-discovered and each harness must pass PYTHON explicitly. +# CI installs the engine into the job interpreter and never creates a repo +# venv, so a hardcoded .venv path is not portable. + +_ENGINE_PYTHON_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=../../scripts/lib/python.sh +source "$(cd "${_ENGINE_PYTHON_LIB_DIR}/../.." && pwd)/scripts/lib/python.sh" + +sdlc_harness_python() { + local repo_root="${1:?repo root required}" + local venv_python="${repo_root}/.venv/bin/python" + if [[ -x "${venv_python}" ]] && _python_is_usable_312 "${venv_python}"; then + printf '%s\n' "${venv_python}" + return 0 + fi + local bootstrap + bootstrap="$(PYTHON='' pick_bootstrap_python)" + if [[ -z "${bootstrap}" ]]; then + echo "harness: Python 3.12 not found — run ./scripts/setup-engine-venv.sh" >&2 + return 1 + fi + printf '%s\n' "${bootstrap}" +} diff --git a/tests/live-consumer/lib.sh b/tests/live-consumer/lib.sh index 95cc6bfc..0c8760ec 100755 --- a/tests/live-consumer/lib.sh +++ b/tests/live-consumer/lib.sh @@ -8,13 +8,9 @@ WORK_ID="${LIVE_WORK_ID:-FEAT-001-hello-live}" DONE_WORK_ID="${LIVE_DONE_WORK_ID:-FEAT-002-done-live}" SDLC_USER="${SDLC_USER:-live-matrix}" if [[ -z "${PYTHON:-}" ]]; then - if [[ -x "${REPO_ROOT}/.venv/bin/python" ]]; then - PYTHON="${REPO_ROOT}/.venv/bin/python" - elif command -v python3.12 >/dev/null 2>&1; then - PYTHON="$(command -v python3.12)" - else - PYTHON="$(command -v python3)" - fi + # shellcheck source=../lib/engine-python.sh + source "${REPO_ROOT}/tests/lib/engine-python.sh" + PYTHON="$(sdlc_harness_python "${REPO_ROOT}")" fi export PYTHON export PYTHONPATH="${REPO_ROOT}/engine/src${PYTHONPATH:+:${PYTHONPATH}}" diff --git a/tests/test-gate-advisory-visible.sh b/tests/test-gate-advisory-visible.sh index a85d8327..e6ebce61 100755 --- a/tests/test-gate-advisory-visible.sh +++ b/tests/test-gate-advisory-visible.sh @@ -6,6 +6,9 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +# shellcheck source=lib/engine-python.sh +source "${SCRIPT_DIR}/lib/engine-python.sh" +HARNESS_PYTHON="$(sdlc_harness_python "${REPO_ROOT}")" CAPTURE="${REPO_ROOT}/scripts/capture-session-memory.sh" SDLC_SH="${REPO_ROOT}/scripts/sdlc.sh" @@ -62,7 +65,7 @@ sdlc() { local t="$1" shift SDLC_ROOT="${t}" \ - PYTHON="${REPO_ROOT}/.venv/bin/python" \ + PYTHON="${HARNESS_PYTHON}" \ PYTHONPATH="${REPO_ROOT}/engine/src" \ "${t}/sdlc-spdd/scripts/sdlc.sh" "$@" } diff --git a/tests/test-gate-review-validation.sh b/tests/test-gate-review-validation.sh index 9dcc176d..0455762a 100755 --- a/tests/test-gate-review-validation.sh +++ b/tests/test-gate-review-validation.sh @@ -6,6 +6,9 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +# shellcheck source=lib/engine-python.sh +source "${SCRIPT_DIR}/lib/engine-python.sh" +HARNESS_PYTHON="$(sdlc_harness_python "${REPO_ROOT}")" CAPTURE="${REPO_ROOT}/scripts/capture-session-memory.sh" SDLC_SH="${REPO_ROOT}/scripts/sdlc.sh" @@ -55,7 +58,7 @@ sdlc() { local t="$1" shift SDLC_ROOT="${t}" \ - PYTHON="${REPO_ROOT}/.venv/bin/python" \ + PYTHON="${HARNESS_PYTHON}" \ PYTHONPATH="${REPO_ROOT}/engine/src" \ "${t}/sdlc-spdd/scripts/sdlc.sh" "$@" } diff --git a/tests/test-integration-merge.sh b/tests/test-integration-merge.sh index 8d0149bf..7606e515 100755 --- a/tests/test-integration-merge.sh +++ b/tests/test-integration-merge.sh @@ -3,13 +3,10 @@ set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -if [[ -x "${REPO_ROOT}/.venv/bin/python" ]]; then - export PYTHON="${REPO_ROOT}/.venv/bin/python" -elif command -v python3.12 >/dev/null 2>&1; then - export PYTHON="$(command -v python3.12)" -else - export PYTHON="$(command -v python3)" -fi +# shellcheck source=lib/engine-python.sh +source "${REPO_ROOT}/tests/lib/engine-python.sh" +PYTHON="$(sdlc_harness_python "${REPO_ROOT}")" +export PYTHON export PYTHONPATH="${REPO_ROOT}/engine/src${PYTHONPATH:+:${PYTHONPATH}}" SETUP="${REPO_ROOT}/scripts/setup-agent-prompts.sh" UPGRADE="${REPO_ROOT}/scripts/upgrade-project.sh" diff --git a/tests/test-scripts-lib.sh b/tests/test-scripts-lib.sh index 2e274f21..7dfced8c 100755 --- a/tests/test-scripts-lib.sh +++ b/tests/test-scripts-lib.sh @@ -533,6 +533,37 @@ else ok "skip live python3.12 usable check (none on PATH)" fi +# An unrunnable interpreter must say so, not report a truncated version. +missing_py="${WORK}/absent/python" +resolve_out="$(PYTHON="${missing_py}" resolve_engine_python 2>&1 || true)" +assert_contains "${resolve_out}" "cannot run ${missing_py}" \ + "resolve_engine_python reports an unrunnable interpreter" +if grep -Fq -- " is 3. " <<< "${resolve_out}"; then + bad "resolve_engine_python must not print a truncated version" +else + ok "resolve_engine_python prints no truncated version" +fi +cat > "${WORK}/python3.11" <<'EOF' +#!/bin/sh +echo "3 11" +EOF +chmod +x "${WORK}/python3.11" +assert_contains "$(PYTHON="${WORK}/python3.11" resolve_engine_python 2>&1 || true)" \ + "is 3.11 —" "resolve_engine_python reports the full found version" + +# --------------------------------------------------------------------------- +echo "== tests/lib/engine-python.sh: harness interpreter resolution ==" +# shellcheck source=/dev/null +source "${REPO_ROOT}/tests/lib/engine-python.sh" +harness_root="${WORK}/harness-root" +mkdir -p "${harness_root}" +if command -v python3.12 >/dev/null 2>&1; then + assert_true "harness python resolves without a repo venv" \ + _python_is_usable_312 "$(sdlc_harness_python "${harness_root}")" +else + ok "skip harness python resolution (no python3.12 on PATH)" +fi + # --------------------------------------------------------------------------- echo "== verify-script-lib-duplicates.sh ==" if "${REPO_ROOT}/scripts/verify-script-lib-duplicates.sh" >/dev/null; then diff --git a/tests/test-verify-receipt.sh b/tests/test-verify-receipt.sh index 33442176..55c9c5d3 100755 --- a/tests/test-verify-receipt.sh +++ b/tests/test-verify-receipt.sh @@ -6,6 +6,9 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +# shellcheck source=lib/engine-python.sh +source "${SCRIPT_DIR}/lib/engine-python.sh" +HARNESS_PYTHON="$(sdlc_harness_python "${REPO_ROOT}")" CAPTURE="${REPO_ROOT}/scripts/capture-session-memory.sh" SDLC_SH="${REPO_ROOT}/scripts/sdlc.sh" @@ -38,7 +41,7 @@ sdlc() { local t="$1" shift SDLC_ROOT="${t}" \ - PYTHON="${REPO_ROOT}/.venv/bin/python" \ + PYTHON="${HARNESS_PYTHON}" \ PYTHONPATH="${REPO_ROOT}/engine/src" \ "${t}/sdlc-spdd/scripts/sdlc.sh" "$@" } From cb19e84e50eda9cf6e68e61ca90415cee7ef8b79 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 14 Sep 2026 05:49:57 +0000 Subject: [PATCH 17/18] docs: state one mandatory Python engine across current docs REF-003 T05. Replace dual-engine prose in operator, testing, engine, template, and research docs; drop the now-inert SDLC_ENGINE=python prefixes from command examples; describe the pytest coverage that replaced the retired twin harnesses. Route the last bare python3 calls in the retained session/capture utilities through SDLC_PY. Co-authored-by: John Menke --- .claude/commands/sdlc-spdd-commit-message.md | 2 +- .claude/commands/sdlc-spdd-sunset.md | 2 +- .cursor/commands/sdlc-spdd-commit-message.md | 2 +- .cursor/commands/sdlc-spdd-sunset.md | 2 +- .../sdlc-spdd-commit-message.prompt.md | 2 +- .github/prompts/sdlc-spdd-sunset.prompt.md | 2 +- CHANGELOG.md | 16 +++++ README.md | 2 +- STARTER-SPEC.md | 5 +- TESTING.md | 66 ++++++++----------- docs/README.md | 2 +- docs/adf-viewer.md | 6 +- docs/engine-v2.md | 45 ++++++------- docs/local-sqlite-index.md | 2 +- docs/maintaining-your-project.md | 2 +- docs/mcp-guide-for-agents.md | 4 +- docs/research/README.md | 2 +- engine/README.md | 12 ++-- scripts/capture-session-memory.sh | 4 +- scripts/start-agent-session.sh | 2 +- sdlc-spdd/docs/adf-viewer.md | 6 +- sdlc-spdd/docs/maintaining-your-project.md | 2 +- sdlc-spdd/docs/research/engine-sut.md | 25 ++++--- .../docs/research/evaluation-protocol.md | 8 +-- .../threats-to-validity-and-replication.md | 6 +- sdlc-spdd/harness/guide-dice.md | 2 +- sdlc-spdd/scripts/capture-session-memory.sh | 4 +- sdlc-spdd/scripts/start-agent-session.sh | 2 +- .../REF-003-retire-bash-workflow-dual-path.md | 35 +++++----- .../commands/lifecycle-commit-message.spec.md | 2 +- spec/commands/lifecycle-sunset.spec.md | 2 +- templates/agent-context/README.md | 6 +- templates/agent-context/harness/guide-dice.md | 2 +- .../commands/sdlc-spdd-commit-message.md | 2 +- templates/claude/commands/sdlc-spdd-sunset.md | 2 +- .../sdlc-spdd-commit-message.prompt.md | 2 +- .../prompts/sdlc-spdd-sunset.prompt.md | 2 +- templates/cursor/sdlc-spdd-commit-message.md | 2 +- templates/cursor/sdlc-spdd-sunset.md | 2 +- templates/requirements/milestones/README.md | 14 ++-- tests/research/test_ref001_sut.py | 2 +- 41 files changed, 158 insertions(+), 154 deletions(-) diff --git a/.claude/commands/sdlc-spdd-commit-message.md b/.claude/commands/sdlc-spdd-commit-message.md index 3ac4b607..8ee0e96c 100644 --- a/.claude/commands/sdlc-spdd-commit-message.md +++ b/.claude/commands/sdlc-spdd-commit-message.md @@ -10,7 +10,7 @@ Your job is to draft a commit message from the current local changes. Collect th 1. Parse optional arguments: a short user hint and an optional Work ID. Do not invent Work IDs. 2. If Work ID is omitted, try the active pointer via `./sdlc-spdd/scripts/sdlc.sh next` (or `./sdlc-spdd/scripts/sdlc.sh next` in the orchestrator repo) or `sdlc-spdd/.sdlc/sessions/current-session.md`. If still unknown, omit Work ID from the message unless the user hint includes one. -3. Collect the change set by running the Python engine (required — do not improvise with raw git when the engine is available): `./sdlc-spdd/scripts/sdlc.sh commit-message` with `--hint` / `--work-id` when known. In the orchestrator repo this always routes to `python -m sdlc_engine commit-message` even when `SDLC_ENGINE=shell`. +3. Collect the change set by running the Python engine (required — do not improvise with raw git when the engine is available): `./sdlc-spdd/scripts/sdlc.sh commit-message` with `--hint` / `--work-id` when known. In the orchestrator repo this always routes to `python -m sdlc_engine commit-message`. 4. Use the engine report as the source of truth for which files/diff to message (source is staged, else unstaged, else commits/diff since merge base). If the engine exits non-zero or reports nothing to commit, report that failure and stop. Do not invent a message. 5. Draft a paste-ready commit message: a concise subject line (imperative mood, ~72 chars); an optional body with why/what when the change needs more than the subject; incorporate the user hint when provided; include the Work ID in the subject or body when known (for example `FEAT-008: …` or a `Work-ID:` trailer). 6. Prefer one focused commit message for the current change set. Do not rewrite unrelated history. diff --git a/.claude/commands/sdlc-spdd-sunset.md b/.claude/commands/sdlc-spdd-sunset.md index 7ccdc890..54776aef 100644 --- a/.claude/commands/sdlc-spdd-sunset.md +++ b/.claude/commands/sdlc-spdd-sunset.md @@ -15,7 +15,7 @@ Your job is to close out a Work ID by collecting GitHub PR, GitHub issue, commit 1. Parse an optional Work ID. Do not invent Work IDs. 2. If Work ID is omitted, try the active pointer via `./sdlc-spdd/scripts/sdlc.sh next` (or `./sdlc-spdd/scripts/sdlc.sh next` in the orchestrator repo) or `sdlc-spdd/.sdlc/sessions/current-session.md`. If still unknown, stop and ask for a Work ID. -3. Collect tracker and git close-out state by running the Python engine (required — do not improvise with raw `gh`, Jira HTTP, or `git log` when the engine is available): `./sdlc-spdd/scripts/sdlc.sh sunset --work-id --apply`. In the orchestrator repo this always routes to `python -m sdlc_engine sunset` even when `SDLC_ENGINE=shell`. +3. Collect tracker and git close-out state by running the Python engine (required — do not improvise with raw `gh`, Jira HTTP, or `git log` when the engine is available): `./sdlc-spdd/scripts/sdlc.sh sunset --work-id --apply`. In the orchestrator repo this always routes to `python -m sdlc_engine sunset`. 4. Use the engine report as the source of truth for Jira key/status, GitHub issue and PR number/title/state/URL, and matching commits. If the engine exits non-zero, report that failure and stop. Do not invent issue, PR, commit, or Jira facts. 5. Treat missing remotes as warnings, not a hard stop, when the engine still produced a snapshot (for example `gh` is not installed, or Jira credentials are unset). Report every warning from the engine. 6. After a successful `--apply`, the engine has staged a `session` record (`source=sunset`) in `sdlc-spdd/.sdlc/staged/lessons.jsonl`. Recommend `/sdlc-spdd-accept` to promote it into `sdlc-spdd/spdd/memory/lessons.jsonl`. Do not edit the ledger by hand. diff --git a/.cursor/commands/sdlc-spdd-commit-message.md b/.cursor/commands/sdlc-spdd-commit-message.md index 3ac4b607..8ee0e96c 100644 --- a/.cursor/commands/sdlc-spdd-commit-message.md +++ b/.cursor/commands/sdlc-spdd-commit-message.md @@ -10,7 +10,7 @@ Your job is to draft a commit message from the current local changes. Collect th 1. Parse optional arguments: a short user hint and an optional Work ID. Do not invent Work IDs. 2. If Work ID is omitted, try the active pointer via `./sdlc-spdd/scripts/sdlc.sh next` (or `./sdlc-spdd/scripts/sdlc.sh next` in the orchestrator repo) or `sdlc-spdd/.sdlc/sessions/current-session.md`. If still unknown, omit Work ID from the message unless the user hint includes one. -3. Collect the change set by running the Python engine (required — do not improvise with raw git when the engine is available): `./sdlc-spdd/scripts/sdlc.sh commit-message` with `--hint` / `--work-id` when known. In the orchestrator repo this always routes to `python -m sdlc_engine commit-message` even when `SDLC_ENGINE=shell`. +3. Collect the change set by running the Python engine (required — do not improvise with raw git when the engine is available): `./sdlc-spdd/scripts/sdlc.sh commit-message` with `--hint` / `--work-id` when known. In the orchestrator repo this always routes to `python -m sdlc_engine commit-message`. 4. Use the engine report as the source of truth for which files/diff to message (source is staged, else unstaged, else commits/diff since merge base). If the engine exits non-zero or reports nothing to commit, report that failure and stop. Do not invent a message. 5. Draft a paste-ready commit message: a concise subject line (imperative mood, ~72 chars); an optional body with why/what when the change needs more than the subject; incorporate the user hint when provided; include the Work ID in the subject or body when known (for example `FEAT-008: …` or a `Work-ID:` trailer). 6. Prefer one focused commit message for the current change set. Do not rewrite unrelated history. diff --git a/.cursor/commands/sdlc-spdd-sunset.md b/.cursor/commands/sdlc-spdd-sunset.md index 66ab111e..20df42b2 100644 --- a/.cursor/commands/sdlc-spdd-sunset.md +++ b/.cursor/commands/sdlc-spdd-sunset.md @@ -10,7 +10,7 @@ Your job is to close out a Work ID by collecting GitHub PR, GitHub issue, commit 1. Parse an optional Work ID. Do not invent Work IDs. 2. If Work ID is omitted, try the active pointer via `./sdlc-spdd/scripts/sdlc.sh next` (or `./sdlc-spdd/scripts/sdlc.sh next` in the orchestrator repo) or `sdlc-spdd/.sdlc/sessions/current-session.md`. If still unknown, stop and ask for a Work ID. -3. Collect tracker and git close-out state by running the Python engine (required — do not improvise with raw `gh`, Jira HTTP, or `git log` when the engine is available): `./sdlc-spdd/scripts/sdlc.sh sunset --work-id --apply`. In the orchestrator repo this always routes to `python -m sdlc_engine sunset` even when `SDLC_ENGINE=shell`. +3. Collect tracker and git close-out state by running the Python engine (required — do not improvise with raw `gh`, Jira HTTP, or `git log` when the engine is available): `./sdlc-spdd/scripts/sdlc.sh sunset --work-id --apply`. In the orchestrator repo this always routes to `python -m sdlc_engine sunset`. 4. Use the engine report as the source of truth for Jira key/status, GitHub issue and PR number/title/state/URL, and matching commits. If the engine exits non-zero, report that failure and stop. Do not invent issue, PR, commit, or Jira facts. 5. Treat missing remotes as warnings, not a hard stop, when the engine still produced a snapshot (for example `gh` is not installed, or Jira credentials are unset). Report every warning from the engine. 6. After a successful `--apply`, the engine has staged a `session` record (`source=sunset`) in `sdlc-spdd/.sdlc/staged/lessons.jsonl`. Recommend `/sdlc-spdd-accept` to promote it into `sdlc-spdd/spdd/memory/lessons.jsonl`. Do not edit the ledger by hand. diff --git a/.github/prompts/sdlc-spdd-commit-message.prompt.md b/.github/prompts/sdlc-spdd-commit-message.prompt.md index df7068df..3f63d1dc 100644 --- a/.github/prompts/sdlc-spdd-commit-message.prompt.md +++ b/.github/prompts/sdlc-spdd-commit-message.prompt.md @@ -15,7 +15,7 @@ Draft a commit message from the current local changes. Collect the diff through 1. Parse optional arguments: a short user hint and an optional Work ID. Do not invent Work IDs. 2. If Work ID is omitted, try the active pointer via `./sdlc-spdd/scripts/sdlc.sh next` (or `./sdlc-spdd/scripts/sdlc.sh next` in the orchestrator repo) or `sdlc-spdd/.sdlc/sessions/current-session.md`. If still unknown, omit Work ID from the message unless the user hint includes one. -3. Collect the change set by running the Python engine (required — do not improvise with raw git when the engine is available): `./sdlc-spdd/scripts/sdlc.sh commit-message` with `--hint` / `--work-id` when known. In the orchestrator repo this always routes to `python -m sdlc_engine commit-message` even when `SDLC_ENGINE=shell`. +3. Collect the change set by running the Python engine (required — do not improvise with raw git when the engine is available): `./sdlc-spdd/scripts/sdlc.sh commit-message` with `--hint` / `--work-id` when known. In the orchestrator repo this always routes to `python -m sdlc_engine commit-message`. 4. Use the engine report as the source of truth for which files/diff to message (source is staged, else unstaged, else commits/diff since merge base). If the engine exits non-zero or reports nothing to commit, report that failure and stop. Do not invent a message. 5. Draft a paste-ready commit message: a concise subject line (imperative mood, ~72 chars); an optional body with why/what when the change needs more than the subject; incorporate the user hint when provided; include the Work ID in the subject or body when known (for example `FEAT-008: …` or a `Work-ID:` trailer). 6. Prefer one focused commit message for the current change set. Do not rewrite unrelated history. diff --git a/.github/prompts/sdlc-spdd-sunset.prompt.md b/.github/prompts/sdlc-spdd-sunset.prompt.md index c8b3837d..2fbc4c41 100644 --- a/.github/prompts/sdlc-spdd-sunset.prompt.md +++ b/.github/prompts/sdlc-spdd-sunset.prompt.md @@ -15,7 +15,7 @@ Close out a Work ID by collecting GitHub PR, GitHub issue, commit, and Jira stat 1. Parse an optional Work ID. Do not invent Work IDs. 2. If Work ID is omitted, try the active pointer via `./sdlc-spdd/scripts/sdlc.sh next` (or `./sdlc-spdd/scripts/sdlc.sh next` in the orchestrator repo) or `sdlc-spdd/.sdlc/sessions/current-session.md`. If still unknown, stop and ask for a Work ID. -3. Collect tracker and git close-out state by running the Python engine (required — do not improvise with raw `gh`, Jira HTTP, or `git log` when the engine is available): `./sdlc-spdd/scripts/sdlc.sh sunset --work-id --apply`. In the orchestrator repo this always routes to `python -m sdlc_engine sunset` even when `SDLC_ENGINE=shell`. +3. Collect tracker and git close-out state by running the Python engine (required — do not improvise with raw `gh`, Jira HTTP, or `git log` when the engine is available): `./sdlc-spdd/scripts/sdlc.sh sunset --work-id --apply`. In the orchestrator repo this always routes to `python -m sdlc_engine sunset`. 4. Use the engine report as the source of truth for Jira key/status, GitHub issue and PR number/title/state/URL, and matching commits. If the engine exits non-zero, report that failure and stop. Do not invent issue, PR, commit, or Jira facts. 5. Treat missing remotes as warnings, not a hard stop, when the engine still produced a snapshot (for example `gh` is not installed, or Jira credentials are unset). Report every warning from the engine. 6. After a successful `--apply`, the engine has staged a `session` record (`source=sunset`) in `sdlc-spdd/.sdlc/staged/lessons.jsonl`. Recommend `/sdlc-spdd-accept` to promote it into `sdlc-spdd/spdd/memory/lessons.jsonl`. Do not edit the ledger by hand. diff --git a/CHANGELOG.md b/CHANGELOG.md index 10fe6766..4585002b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,12 +37,28 @@ All notable changes to this project will be documented in this file. ### Removed +- REF-003: the bash workflow twin — `sdlc-workflow.sh`, `sdlc-team-registry.sh`, + and `sdlc-pointer.sh` — from `templates/agent-context/` and + `sdlc-spdd/scripts/` (3,100 shipped LOC). Init and upgrade no longer install + them, upgrade deletes the exact retired paths, and install verification + asserts their absence. Their shell harnesses + (`test-sdlc-workflow.sh`, `test-sdlc-pointer.sh`, `test-archive-work.sh`) + are replaced by `engine/tests_unit/` coverage (#321). - REF-002: pre-v3 consolidation, layout migration, TSV registry fallback, migration-only parsers and CLI verbs, duplicate-canvas synchronization, and executable root-layout fallbacks. Storage v3 is the only runtime layout. ### Changed +- REF-003: `scripts/sdlc.sh` is a thin dispatcher that requires Python 3.12 and + an importable `sdlc_engine`, with no bash fallback. `SDLC_ENGINE=shell` and + `SDLC_GATE_ENGINE=shell` now exit non-zero instead of selecting a second + implementation; `SDLC_ENGINE=python` stays accepted as a no-op. The retained + `start-agent-session.sh`, `capture-session-memory.sh`, and + `accept-lessons.sh` utilities call the engine, and `sdlc-engine shell` + resolves helpers in both orchestrator `scripts/` and installed + `sdlc-spdd/scripts/` layouts. Operator, testing, engine, and research docs + describe one mandatory engine (#321). - Milestone 3 (one flow on storage v3) opened from SPIKE-005: Python `sdlc-engine` is the only engine, storage v3 the only persistence model; pre-v3 compatibility and the bash workflow twin are removed, not diff --git a/README.md b/README.md index 518dee24..e9d42b93 100644 --- a/README.md +++ b/README.md @@ -225,7 +225,7 @@ Python engine (same surface, importable): ```bash python3 -m pip install -e './engine[dev,viewer]' -SDLC_ENGINE=python ./scripts/sdlc.sh next +./scripts/sdlc.sh next sdlc-engine context retrieve --work-id FEAT-001-order-status sdlc-engine context backends ``` diff --git a/STARTER-SPEC.md b/STARTER-SPEC.md index 525bb265..a2922683 100644 --- a/STARTER-SPEC.md +++ b/STARTER-SPEC.md @@ -268,10 +268,7 @@ Create the repo with this structure: │ ├── sync/ │ └── memory/ # lessons.jsonl + registry.jsonl ├── .sdlc/ # gitignored runtime (sessions, staged, sqlite) - ├── agent-context/ # workflow scripts in dogfood repo only - │ ├── sdlc-pointer.sh - │ ├── sdlc-workflow.sh - │ └── sdlc-team-registry.sh + ├── agent-context/ # dogfood repo only │ └── harness/ # dogfood mirror of harness/ │ ├── validation-rules.md │ └── quality-gates.md diff --git a/TESTING.md b/TESTING.md index 4f3aa503..e65393b9 100644 --- a/TESTING.md +++ b/TESTING.md @@ -16,10 +16,10 @@ what can be proven automatically, then run a short manual smoke for the rest. ## Engine test suites (3 packages) -**Milestone 2 system under test:** Python `WorkflowEngine.gate_check` -(`SDLC_ENGINE=auto` or `python`). See -[engine-sut.md](sdlc-spdd/docs/research/engine-sut.md). `SDLC_GATE_ENGINE=shell` -is a labeled fallback, not an evaluation condition. +**System under test:** Python `WorkflowEngine.gate_check`. See +[engine-sut.md](sdlc-spdd/docs/research/engine-sut.md). The Python engine is the +only lifecycle implementation; `SDLC_ENGINE=shell` and `SDLC_GATE_ENGINE=shell` +are rejected rather than selecting a second behavior (REF-003). | Suite | Path | Command | CI | |-------|------|---------|-----| @@ -101,7 +101,7 @@ Then: `./scripts/setup-engine-venv.sh --e2e` | Guide not up | `preflight e2e --guide` first | bare `curl …/sse` | | Stale background pytest | `unit --clean-stale` | start another full run | -Shell workflow harness (separate from engine pytest): `./tests/test-sdlc-workflow.sh` — run only when touching `scripts/sdlc.sh` or workflow gates. +Shell dispatcher harness (separate from engine pytest): `./tests/test-sdlc-engine-shim.sh` — run only when touching `scripts/sdlc.sh`. Workflow and gate behavior itself lives in `engine/tests_unit/`. **Suite 1** — fast, isolated: mocks, `tmp_path`, no browser, no network. (Three installer/console files still use a Flask `test_client`; TEST-004 moves them to Suite 2.) @@ -198,36 +198,26 @@ Run it locally before changing any install/upgrade script or command template. The CI workflow also runs `bash -n` over shell scripts before executing the regression harness. -### SDLC pointer harness +### Pointer, workflow, registry, and archive coverage -`./tests/test-sdlc-pointer.sh` exercises `templates/agent-context/sdlc-pointer.sh`: +REF-003 retired the bash twins (`sdlc-pointer.sh`, `sdlc-workflow.sh`, +`sdlc-team-registry.sh`) and the shell harnesses that exercised them +(`test-sdlc-pointer.sh`, `test-sdlc-workflow.sh`, `test-archive-work.sh`). +The same behavior is now covered in Suite 1: -- CLI round-trip (`set`/`get`/`reset`) -- Guarded run (`run_against_pointer`) refusal on mismatch -- `SDLC_POINTER_OVERRIDE` bootstrap -- Integration with `start-agent-session.sh` pointer auto-set -- Install path copies the script to target projects +- `engine/tests_unit/test_pointer.py` — pointer round-trip, guarded run refusal + on mismatch, `start-agent-session.sh` auto-set +- `engine/tests_unit/test_workflow.py`, `test_workflow_gates.py` — phase/gate + tracking, `next`/`advance`/`skip`/`shelf`/`resume`/`sync`, guarded `capture` +- `engine/tests_unit/test_registry_archive.py` — team `claim`/`release`, stale + TTL, branch/PR/Jira notes in `spdd/memory/registry.jsonl`, Jira Key auto-link + on claim, and `archive` / `archive --all` (refuses In Progress without + `--force`, leaves `requirements/milestones/.md` in place, clears a + matching pointer, marks registry status `archived`) +- `engine/tests_unit/test_cli_shell.py` — the retained `sdlc-engine shell` + bridge resolves utilities in both orchestrator and installed layouts -### SDLC workflow + team registry harness - -`./tests/test-sdlc-workflow.sh` exercises `templates/agent-context/sdlc-workflow.sh` and team registry: - -- Phase/gate tracking, `next`/`advance`/`skip`/`shelf`/`resume`/`sync` -- `sdlc.sh` wrapper delegation -- Guarded `capture` (pointer must match) -- Team `claim`/`release`, stale TTL, branch/PR/Jira notes in `spdd/memory/registry.jsonl` -- Jira Key auto-link from `requirements/milestones/.md` on claim - -### Archive completed/cancelled work harness - -`./tests/test-archive-work.sh` exercises `sdlc.sh archive` / `archive --all`: - -- Refuses In Progress work unless `--force` -- **Deletes** Complete/Cancelled canvases, analysis/review/sync artifacts, and matching session briefs from the working tree (git history retains them; storage v3 has no `spdd/*/archive/` folders) -- Leaves `requirements/milestones/.md` in place -- Clears the local pointer when it matches the archived Work ID -- Marks `spdd/memory/registry.jsonl` status `archived` (and `sync-team` can mark `cancelled` without removing files) -- `list-work` ignores removed Work IDs (no committed archive directories) +Dispatcher delegation stays in `./tests/test-sdlc-engine-shim.sh`. ### Shared scripts/lib harness @@ -298,7 +288,7 @@ source .venv/bin/activate ./scripts/run-test-suites.sh integration ./scripts/run-test-suites.sh e2e # Playwright + GitHub (needs gh auth) ./scripts/run-test-suites.sh e2e --guide # + Guide + Neo4j stack -SDLC_ENGINE=python ./scripts/sdlc.sh version +./scripts/sdlc.sh version ./tests/test-sdlc-engine-shim.sh ``` @@ -353,10 +343,10 @@ Issue sync confidence: | CI | Same, with `issues: write` for create/close cleanup | `test-github-issue-sync` job in `test-sdlc-engine.yml` | ```bash -SDLC_ENGINE=python ./scripts/sdlc.sh sync-links -SDLC_ENGINE=python ./scripts/sdlc.sh sync-links --repair -SDLC_ENGINE=python ./scripts/sdlc.sh sync-roadmap --dry-run -SDLC_ENGINE=python ./scripts/sdlc.sh issues draft --system github +./scripts/sdlc.sh sync-links +./scripts/sdlc.sh sync-links --repair +./scripts/sdlc.sh sync-roadmap --dry-run +./scripts/sdlc.sh issues draft --system github ./scripts/sdlc.sh local start --name scratch --intent "offline explore" ./scripts/sdlc.sh local promote --type feature --name "Documented title" --dry-run @@ -373,7 +363,7 @@ SDLC_ENGINE=python ./scripts/sdlc.sh issues draft --system github `./tests/test-canvas-readiness.sh` exercises `validate-reasons-canvas.sh` readiness normalization and a smoke path for `capture-session-memory.sh` staging records into `.sdlc/staged/lessons.jsonl` (storage v3). Workflow capture integration -is covered by `./tests/test-sdlc-workflow.sh` (Test 7) and the live-consumer +is covered by `engine/tests_unit/test_workflow.py` and the live-consumer matrix scenarios 03 and 08. ### Index SPDD analysis harness diff --git a/docs/README.md b/docs/README.md index 8169aa42..22d89797 100644 --- a/docs/README.md +++ b/docs/README.md @@ -56,7 +56,7 @@ Jira link & sync / ADF viewer), [ADF templates + Vue3 console](adf-template-libr **Workflow CLI reference** (pointer, phase tracking, team registry): [agent-context/README.md](../agent-context/README.md#sdlc-pointer-current-choretask) — installed in target projects at `sdlc-spdd/scripts/` with docs under `docs/sdlc-spdd/`. -**Python engine:** [SDLC Engine](engine-v2.md) — reusable `sdlc_engine` package; `SDLC_ENGINE=auto|python|shell` on `scripts/sdlc.sh`. +**Python engine:** [SDLC Engine](engine-v2.md) — reusable `sdlc_engine` package; the only lifecycle implementation behind `scripts/sdlc.sh`. **Local GUIs (experimental ops console + ADF Viewer):** [Ops console and ADF Viewer](ops-console.md). diff --git a/docs/adf-viewer.md b/docs/adf-viewer.md index 56484167..7d2f3503 100644 --- a/docs/adf-viewer.md +++ b/docs/adf-viewer.md @@ -28,10 +28,8 @@ Flask is an optional extra so the core engine stays stdlib-only. ## Run ```bash -# Preferred wrappers +# Preferred wrapper ./scripts/sdlc.sh viewer --port 5050 -# or -SDLC_ENGINE=python ./scripts/sdlc.sh viewer --port 5050 # Direct python3 -m sdlc_engine.viewer --root . --port 5050 @@ -67,7 +65,7 @@ The ops console **ADF** tab can seed a new Work ID from a selected `adf/*.adf.js 2. Enter **Work ID** and title → **Init work (dry run)** previews; **Init work** creates requirement, canvas (Source System: ADF), and registry claim. ```bash -SDLC_ENGINE=python ./scripts/sdlc.sh work init-from-adf \ +./scripts/sdlc.sh work init-from-adf \ --root /path/to/target --adf adf/ORCH-77.adf.json \ --work-id FEAT-013-from-adf --title "Title from ADF" ``` diff --git a/docs/engine-v2.md b/docs/engine-v2.md index 61ef2436..ffd78596 100644 --- a/docs/engine-v2.md +++ b/docs/engine-v2.md @@ -6,14 +6,15 @@ stage-then-accept, projections), issue sync, local sessions, and the ops console ![Engine components](diagrams/03-component-engine.svg) -Shell scripts remain an install/upgrade entry point. **REF-001:** the default -`sdlc.sh` engine is `auto` (Python when importable). Python -`WorkflowEngine.gate_check` is the Milestone 2 system under test. Opt into -legacy bash workflow CLI with `SDLC_ENGINE=shell`; gates still call Python -unless `SDLC_GATE_ENGINE=shell`. `sdlc.sh capture`/`start`/`accept` stay on -the shell path (Python has `local capture` / `context accept`). On-disk -formats are shared: `.sdlc/`, -`spdd/memory/lessons.jsonl`, `spdd/memory/registry.jsonl`, canvas paths. +Shell scripts remain an install/upgrade entry point and host a few retained +utilities, but they no longer implement lifecycle behavior. **REF-003:** +`scripts/sdlc.sh` is a thin dispatcher that requires Python 3.12 and an +importable `sdlc_engine`; it fails with a setup hint rather than falling back. +`SDLC_ENGINE=shell` and `SDLC_GATE_ENGINE=shell` are rejected, and +`SDLC_ENGINE=python` is accepted only as a no-op. Python +`WorkflowEngine.gate_check` is the system under test. On-disk formats are +`.sdlc/`, `spdd/memory/lessons.jsonl`, `spdd/memory/registry.jsonl`, and +canvas paths. ## Layout @@ -62,11 +63,9 @@ engine/ # From orchestrator checkout (no install) PYTHONPATH=engine/src python3 -m sdlc_engine next --root . -# Opt into the Python engine via the existing wrapper (default remains shell) -SDLC_ENGINE=python ./scripts/sdlc.sh next -SDLC_ENGINE=python ./scripts/sdlc.sh claim FEAT-001-demo -SDLC_ENGINE=auto ./scripts/sdlc.sh next # python if importable, else shell -./scripts/sdlc.sh next # bash default +# Through the dispatcher (always the Python engine) +./scripts/sdlc.sh next +./scripts/sdlc.sh claim FEAT-001-demo # Editable install python3 -m pip install -e './engine[dev]' @@ -90,16 +89,16 @@ sdlc-engine archive --all --dry-run ## Milestone / Jira / GitHub sync ```bash -SDLC_ENGINE=python ./scripts/sdlc.sh links -SDLC_ENGINE=python ./scripts/sdlc.sh sync-links [--repair] -SDLC_ENGINE=python ./scripts/sdlc.sh sync-roadmap +./scripts/sdlc.sh links +./scripts/sdlc.sh sync-links [--repair] +./scripts/sdlc.sh sync-roadmap -SDLC_ENGINE=python ./scripts/sdlc.sh issues draft --system jira -SDLC_ENGINE=python ./scripts/sdlc.sh issues push --system jira --apply -SDLC_ENGINE=python ./scripts/sdlc.sh issues pull --system github --apply +./scripts/sdlc.sh issues draft --system jira +./scripts/sdlc.sh issues push --system jira --apply +./scripts/sdlc.sh issues pull --system github --apply -SDLC_ENGINE=python ./scripts/sdlc.sh issues upload-adf --issue-key PROJ-123 --file adf/PROJ-123.adf.json --apply -SDLC_ENGINE=python ./scripts/sdlc.sh issues download-adf PROJ-123 --apply +./scripts/sdlc.sh issues upload-adf --issue-key PROJ-123 --file adf/PROJ-123.adf.json --apply +./scripts/sdlc.sh issues download-adf PROJ-123 --apply ``` Jira Cloud descriptions use **ADF**. See [jira-runbook.md](jira-runbook.md#description-formatting-adf) @@ -111,8 +110,6 @@ Collect the change set the user is about to commit (staged → unstaged → comm since merge base) so `/sdlc-spdd-commit-message` can draft a message from a stable engine report. **Generate only** — never creates a commit. -Always routed to the Python engine (even when `SDLC_ENGINE=shell`): - ```bash ./scripts/sdlc.sh commit-message ./scripts/sdlc.sh commit-message --hint "wire engine report" --work-id FEAT-008-commit-message-command @@ -129,8 +126,6 @@ a `session` record (`source=sunset`) into the lesson ledger. Used by `/sdlc-spdd-sunset`. Remote pulls are best-effort (missing `gh` or Jira credentials become warnings). -Always routed to the Python engine (even when `SDLC_ENGINE=shell`): - ```bash ./scripts/sdlc.sh sunset --work-id FEAT-001-example ./scripts/sdlc.sh sunset --work-id FEAT-001-example --apply diff --git a/docs/local-sqlite-index.md b/docs/local-sqlite-index.md index 5221fbee..804c8862 100644 --- a/docs/local-sqlite-index.md +++ b/docs/local-sqlite-index.md @@ -32,7 +32,7 @@ When enabled, `capture` / `accept` keep the cache in sync automatically, and ## Commands -Always routed to the Python engine (even when `SDLC_ENGINE=shell`): +Routed to the Python engine, like every other `sdlc.sh` command: ```bash ./scripts/sdlc.sh db rebuild diff --git a/docs/maintaining-your-project.md b/docs/maintaining-your-project.md index 92305896..befa50d5 100644 --- a/docs/maintaining-your-project.md +++ b/docs/maintaining-your-project.md @@ -116,7 +116,7 @@ Keep the canvas Metadata current: For Jira updates: - SDLC_ENGINE=python ./scripts/sdlc.sh issues push --dry-run + ./scripts/sdlc.sh issues push --dry-run See [Jira runbook](jira-runbook.md) and [Issue sync and branching](issue-sync-and-branching.md). diff --git a/docs/mcp-guide-for-agents.md b/docs/mcp-guide-for-agents.md index 03dfd4a9..90898640 100644 --- a/docs/mcp-guide-for-agents.md +++ b/docs/mcp-guide-for-agents.md @@ -36,10 +36,10 @@ Same payloads via HTTP — use in agent prompts or terminal: ./scripts/guide/query-guide.sh --text --work-id FEAT-001-order-status-api # JSON (scripting) -SDLC_ENGINE=python ./scripts/sdlc.sh context guide-query --work-id FEAT-001-order-status-api +./scripts/sdlc.sh context guide-query --work-id FEAT-001-order-status-api # Explicit MCP tool call -SDLC_ENGINE=python ./scripts/sdlc.sh context mcp-call \ +./scripts/sdlc.sh context mcp-call \ --tool spdd_getLesson \ --json '{"id":"pitfall:FEAT-001:engine:retro"}' ``` diff --git a/docs/research/README.md b/docs/research/README.md index a65d497a..fc8b33a7 100644 --- a/docs/research/README.md +++ b/docs/research/README.md @@ -12,7 +12,7 @@ This folder is **not** a paper. It holds investigation notes that support either | [Evaluation protocol (TEST-001)](../../sdlc-spdd/docs/research/evaluation-protocol.md) | Method eval protocol (no study data) | How to run slices: gold, raters, baselines, stop rules | | [TEST-002 first slice log](../../tests/eval/test-002-hello/SLICE-LOG.md) | n=1 hello gold, protocol incomplete | C-DRIFT symbol-proxy 0 vs 0.333; C-PORT not reported; do not treat as evidence the method works | | [Dogfood ledger policy (CHORE-003)](../../sdlc-spdd/docs/research/dogfood-ledger-policy.md) | Archive vs lessons.jsonl | Seeded decision/pitfall/pattern; retrieve is non-vacuous; not RQ4 usefulness | -| [Engine SUT (REF-001)](../../sdlc-spdd/docs/research/engine-sut.md) | Named gate semantics | Python `WorkflowEngine.gate_check`; `SDLC_GATE_ENGINE=shell` is not an eval condition | +| [Engine SUT (REF-001)](../../sdlc-spdd/docs/research/engine-sut.md) | Named gate semantics | Python `WorkflowEngine.gate_check`; `SDLC_GATE_ENGINE=shell` is rejected, not an eval condition | | [Threats and replication (DOC-003)](../../sdlc-spdd/docs/research/threats-to-validity-and-replication.md) | Threats + **referee one-shot** | `./sdlc-spdd/docs/research/prove-academic-review.sh` (no Docker/Neo4j) | | [Retrieve algorithm (FEAT-017)](../../sdlc-spdd/docs/research/retrieve-algorithm.md) | Lexical keyword-list vs title-body | C-CONTEXT relevance proxy; DICE still unmeasured | | [Capture metrics queries (FEAT-015)](../../sdlc-spdd/docs/research/capture-metrics-queries.md) | `context metrics` construct queries | C-COMPLY / C-CONTEXT / C-REWORK / C-MEMORY | diff --git a/engine/README.md b/engine/README.md index 2e637b2d..90001b97 100644 --- a/engine/README.md +++ b/engine/README.md @@ -33,13 +33,11 @@ Or without activating the venv: .venv/bin/python -m sdlc_engine next --root . ``` -Prefer the engine from the existing wrapper (shell remains the default): +Or through the `sdlc.sh` dispatcher, which always runs this engine: ```bash -SDLC_ENGINE=python ./scripts/sdlc.sh next -SDLC_ENGINE=auto ./scripts/sdlc.sh next # python if importable, else shell +./scripts/sdlc.sh next -# Local/offline sessions + SQLite index always use the Python engine (even with SDLC_ENGINE=shell) ./scripts/sdlc.sh local start --name scratch --intent "explore without a FEAT" ./scripts/sdlc.sh local promote --type feature --name "Documented title" ./scripts/sdlc.sh db rebuild @@ -86,8 +84,10 @@ python3 -m pip install -e './engine[dev,viewer]' ## Compatibility - File formats stay identical (`.sdlc/`, `spdd/memory/registry.jsonl`, canvas paths). -- Shell `sdlc.sh` can delegate to this engine (`SDLC_ENGINE=auto|python|shell`). -- Target projects can keep using bash until they opt into the engine. +- Shell `sdlc.sh` is a thin dispatcher into this engine; it requires Python 3.12 + and an importable `sdlc_engine` and has no bash fallback (REF-003). +- `SDLC_ENGINE=python` is accepted as a no-op; `SDLC_ENGINE=shell` and + `SDLC_GATE_ENGINE=shell` are rejected. ## Tests diff --git a/scripts/capture-session-memory.sh b/scripts/capture-session-memory.sh index c47234ba..66a98867 100755 --- a/scripts/capture-session-memory.sh +++ b/scripts/capture-session-memory.sh @@ -382,7 +382,7 @@ METRICS_JSON="$( METRIC_CONTEXT_FILES="${METRIC_CONTEXT_FILES}" \ METRIC_VALIDATE_CYCLES="${METRIC_VALIDATE_CYCLES}" \ METRIC_REVIEW_CYCLES="${METRIC_REVIEW_CYCLES}" \ - python3 - <<'PY' + "${SDLC_PY}" - <<'PY' import json, os m = {} r = os.environ.get("METRIC_READINESS", "").strip() @@ -413,7 +413,7 @@ if [[ -n "${VERIFY_COMMAND}" ]]; then VERIFY_COMMAND="${VERIFY_COMMAND}" \ VERIFY_EXIT="${VERIFY_EXIT}" \ VERIFY_RESULT="${VERIFY_RESULT}" \ - python3 - <<'PY' + "${SDLC_PY}" - <<'PY' import json, os print(json.dumps({ "command": os.environ["VERIFY_COMMAND"], diff --git a/scripts/start-agent-session.sh b/scripts/start-agent-session.sh index cb3149fe..01a4c69b 100755 --- a/scripts/start-agent-session.sh +++ b/scripts/start-agent-session.sh @@ -364,7 +364,7 @@ digest_md="" if [[ -n "${WORK_ID}" ]]; then _milestone_path="${HOME}/requirements/milestones/${WORK_ID}.md" _analysis_path="${HOME}/spdd/analysis/${WORK_ID}-analysis.md" - digest_md="$(python3 - < --dry-run + ./scripts/sdlc.sh issues push --dry-run See [Jira runbook](jira-runbook.md) and [Issue sync and branching](issue-sync-and-branching.md). diff --git a/sdlc-spdd/docs/research/engine-sut.md b/sdlc-spdd/docs/research/engine-sut.md index ba40c59e..cd3ed6fd 100644 --- a/sdlc-spdd/docs/research/engine-sut.md +++ b/sdlc-spdd/docs/research/engine-sut.md @@ -11,7 +11,7 @@ This names the **system under test** for Milestone 2 evaluation. It is not a cla ```bash ./scripts/sdlc.sh gate --phase code --work-id -# default SDLC_ENGINE=auto → Python when sdlc_engine is importable +# the dispatcher requires sdlc_engine; there is no other gate implementation sdlc-engine gate --phase code --work-id ``` @@ -19,19 +19,26 @@ FEAT-014 semantic minima and FEAT-016 review/`Files:` checks live here. Shell `v ## Defaults +REF-003 removed the second implementation, so there is no engine to select. The +variables survive only as rejected or inert inputs. + | Variable | Default | Meaning | |----------|---------|---------| -| `SDLC_ENGINE` | `auto` | Prefer Python for the `sdlc.sh` CLI when importable | -| `SDLC_ENGINE=python` | — | Require Python; fail if missing | -| `SDLC_ENGINE=shell` | — | Bash workflow CLI (`next`/`claim`/…). **Gates still call Python** when importable | -| `SDLC_GATE_ENGINE=shell` | unset | Force the bash substring fallback. **Not the SUT.** Used by `tests/test-sdlc-workflow.sh` | +| unset | — | `sdlc.sh` runs the Python engine; missing Python 3.12 or `sdlc_engine` fails with a setup hint | +| `SDLC_ENGINE=python` | — | Accepted as a no-op; it already names the only engine | +| `SDLC_ENGINE=shell` | — | **Rejected** (non-zero, "no longer supported"); it cannot select a bash workflow CLI | +| `SDLC_GATE_ENGINE=shell` | unset | **Rejected** (non-zero); the bash substring fallback is gone, so it is not an evaluation condition | + +`sdlc.sh capture`, `start`, and `accept` are retained shell utilities that call +the Python engine; they no longer host lifecycle semantics. Python equivalents +are `local capture` and `context accept`. -`sdlc.sh capture`, `start`, and `accept` stay on the shell path under `auto`. Python equivalents are `local capture` and `context accept`. +Before REF-003 the default was `SDLC_ENGINE=auto` (Python when importable) and +`SDLC_GATE_ENGINE=shell` forced a pre-FEAT-014 `ready for coding` grep with no +`Files:` mapping. Slice logs recorded before REF-003 must still name the frozen +engine; a shell-fallback gate pass was never C-COMPLY. ## What is not the SUT -- `SDLC_GATE_ENGINE=shell` fallback (pre-FEAT-014 `ready for coding` grep; no `Files:` mapping) - Adapter-text parity (`validate-command-adapters.sh`) - Live-consumer matrix (Cursor-oriented; not C-PORT) - -Until you freeze `SDLC_ENGINE=auto` or `python` in the slice log, do not treat a shell-fallback gate pass as C-COMPLY. diff --git a/sdlc-spdd/docs/research/evaluation-protocol.md b/sdlc-spdd/docs/research/evaluation-protocol.md index 90f41abd..8fa0748d 100644 --- a/sdlc-spdd/docs/research/evaluation-protocol.md +++ b/sdlc-spdd/docs/research/evaluation-protocol.md @@ -15,11 +15,11 @@ Guide is **fork-only**. This protocol does not depend on an Embabel upstream mer ## 1. Object of evaluation -**Method (SUT after REF-001):** Planning artifacts + REASONS Canvas + phase commands/gates + ledger retrieval, as implemented by Python `WorkflowEngine.gate_check` (`SDLC_ENGINE=auto` or `python`) plus spec-generated assistant adapters. See [engine-sut.md](engine-sut.md). +**Method (SUT after REF-001):** Planning artifacts + REASONS Canvas + phase commands/gates + ledger retrieval, as implemented by Python `WorkflowEngine.gate_check` plus spec-generated assistant adapters. See [engine-sut.md](engine-sut.md). **Not the object:** CLI unit tests, adapter-text parity, or SWE-bench agent competence. Those stay in `TESTING.md`. -Treat **`SDLC_GATE_ENGINE=shell`** as a labeled harness fallback, **not** a second evaluation condition. Default `SDLC_ENGINE=auto` uses Python `gate_check` when importable, including when `SDLC_ENGINE=shell` but the package can be imported. +Since REF-003 there is no second engine to confound a condition: `sdlc.sh` always runs Python `gate_check`, and **`SDLC_GATE_ENGINE=shell`** is rejected rather than selecting a fallback. --- @@ -32,7 +32,7 @@ Every RQ that compares “the method” uses these four process conditions on th | **unstructured chat** | Solve the gold task in one assistant thread. No canvas, no phase commands, no ledger retrieve. | None (chat only) | None | | **canvas-only** | Write/follow a REASONS canvas (Fowler-like). Do not run SDLC phase commands or `gate_check`. | Canvas file | None | | **lifecycle-only** | Use analysis→…→sync commands / briefs (SDLC Agents-like). No REASONS canvas. | Planning files / briefs | Phases | -| **full SDLC-SPDD** | Canvas + phase commands + optional `./scripts/sdlc.sh gate --phase …` (default `SDLC_ENGINE=auto` → Python `gate_check`) + `context retrieve` when the task is a follow-on | Canvas + phase artifacts | Phases + gates + retrieve | +| **full SDLC-SPDD** | Canvas + phase commands + optional `./scripts/sdlc.sh gate --phase …` (always Python `gate_check`) + `context retrieve` when the task is a follow-on | Canvas + phase artifacts | Phases + gates + retrieve | Do not invent a fifth “prompt file dump” condition in TEST-002’s first slice. @@ -159,7 +159,7 @@ Record ContextLoad on `record.metrics.context_files` (`sdlc-engine context metri | Gold task + canvas commit SHA | Assistant sampling | Assistant name | | Model id / version string the vendor exposes | Temperature if the product exposes it; else note “UI default” | Model id | | Operator instruction sheet per condition | Natural language in the thread | Date, n sessions | -| `SDLC_ENGINE=auto` or `python` when gates are used (`SDLC_GATE_ENGINE=shell` forbidden) | Network / product drift | Engine SHA | +| Python `gate_check` when gates are used (no engine selection exists to record) | Network / product drift | Engine SHA | **n:** first TEST-002 slice uses **n ≥ 3** independent runs per condition that is actually collected (not n=1 dressed as a study). If budget allows only n=1, the log must say **protocol incomplete**, not “result.” diff --git a/sdlc-spdd/docs/research/threats-to-validity-and-replication.md b/sdlc-spdd/docs/research/threats-to-validity-and-replication.md index 4fcede93..b98194e8 100644 --- a/sdlc-spdd/docs/research/threats-to-validity-and-replication.md +++ b/sdlc-spdd/docs/research/threats-to-validity-and-replication.md @@ -68,7 +68,7 @@ SPIKE-004’s earlier “file/regex/exact-keyword proxies” row is **partially | Threat | Status after P2 | |--------|-----------------| -| Dual engine (shell `sdlc.sh` vs Python `gate_check`) | Retired as a default confound (REF-001). The research SUT is Python `gate_check` (`SDLC_ENGINE=auto` or `python`). `SDLC_GATE_ENGINE=shell` is not an evaluation condition. | +| Dual engine (shell `sdlc.sh` vs Python `gate_check`) | Retired as a default confound (REF-001), then removed outright (REF-003): the bash twin is deleted and `SDLC_ENGINE=shell` / `SDLC_GATE_ENGINE=shell` are rejected. The research SUT is Python `gate_check`. | | `--force` / skip | Still lets an operator complete a session that is not process-compliant. Log `--force` as invalid for C-COMPLY scoring. | | Chat can ignore the canvas | The method is advised process + optional CLI gates, not a causal sandbox. | | Operator is often the author | Hawthorne / demand characteristics. TEST-002 must say if the operator wrote the method. | @@ -115,7 +115,7 @@ Assistant sampling **cannot be automated away**. TEST-001 §6 (Nondeterminism pl | Gold task + canvas commit SHA | Assistant sampling | Assistant name | | Model id / version string the vendor exposes | Temperature if the product exposes it; else “UI default” | Model id | | Operator instruction sheet per condition | Natural language in the thread | Date, n sessions | -| `SDLC_ENGINE=auto` or `python` when gates are used (`SDLC_GATE_ENGINE=shell` forbidden) | Network / product drift | Engine SHA | +| Python `gate_check` when gates are used (no engine selection exists to record) | Network / product drift | Engine SHA | **n ≥ 3** per collected condition, or the slice is **protocol incomplete**. Do not pool across model versions. This Work ID does not run those sessions. @@ -132,7 +132,7 @@ The table below is the extra freeze if someone later runs a TEST-002 *slice* |------|------------|--------------------| | Engine commit | `git rev-parse HEAD` | this repo | | Engine version | `python3 -c "import sdlc_engine; print(sdlc_engine.__version__)"` | currently `2.0.0a6` at DOC-003 write time; re-print at run time | -| Gate SUT | default `SDLC_ENGINE=auto` (or `python`); never `SDLC_GATE_ENGINE=shell` | [engine-sut.md](engine-sut.md); `sdlc-engine gate --phase code --work-id ` | +| Gate SUT | Python `gate_check`; `SDLC_GATE_ENGINE=shell` is rejected, not a condition | [engine-sut.md](engine-sut.md); `sdlc-engine gate --phase code --work-id ` | | Capture metrics | structured fields, not body grep | `sdlc-engine context metrics --construct C-REWORK` ([capture-metrics-queries.md](capture-metrics-queries.md)) | | Construct spec | file SHA | `sdlc-spdd/docs/research/research-questions-and-constructs.md` | | Evaluation protocol | file SHA | `sdlc-spdd/docs/research/evaluation-protocol.md` | diff --git a/sdlc-spdd/harness/guide-dice.md b/sdlc-spdd/harness/guide-dice.md index 7f909657..87f2197a 100644 --- a/sdlc-spdd/harness/guide-dice.md +++ b/sdlc-spdd/harness/guide-dice.md @@ -39,7 +39,7 @@ When native MCP is unavailable, agents **must** use the same tools via CLI: ```bash ./scripts/guide/query-guide.sh --text --work-id ./scripts/guide/query-guide.sh --area engine/tests -SDLC_ENGINE=python ./scripts/sdlc.sh context mcp-call --tool spdd_getLesson --json '{"id":""}' +./scripts/sdlc.sh context mcp-call --tool spdd_getLesson --json '{"id":""}' ``` MCP config snippet: `./scripts/guide/mcp-config-snippet.sh --cursor` diff --git a/sdlc-spdd/scripts/capture-session-memory.sh b/sdlc-spdd/scripts/capture-session-memory.sh index c47234ba..66a98867 100755 --- a/sdlc-spdd/scripts/capture-session-memory.sh +++ b/sdlc-spdd/scripts/capture-session-memory.sh @@ -382,7 +382,7 @@ METRICS_JSON="$( METRIC_CONTEXT_FILES="${METRIC_CONTEXT_FILES}" \ METRIC_VALIDATE_CYCLES="${METRIC_VALIDATE_CYCLES}" \ METRIC_REVIEW_CYCLES="${METRIC_REVIEW_CYCLES}" \ - python3 - <<'PY' + "${SDLC_PY}" - <<'PY' import json, os m = {} r = os.environ.get("METRIC_READINESS", "").strip() @@ -413,7 +413,7 @@ if [[ -n "${VERIFY_COMMAND}" ]]; then VERIFY_COMMAND="${VERIFY_COMMAND}" \ VERIFY_EXIT="${VERIFY_EXIT}" \ VERIFY_RESULT="${VERIFY_RESULT}" \ - python3 - <<'PY' + "${SDLC_PY}" - <<'PY' import json, os print(json.dumps({ "command": os.environ["VERIFY_COMMAND"], diff --git a/sdlc-spdd/scripts/start-agent-session.sh b/sdlc-spdd/scripts/start-agent-session.sh index cb3149fe..01a4c69b 100755 --- a/sdlc-spdd/scripts/start-agent-session.sh +++ b/sdlc-spdd/scripts/start-agent-session.sh @@ -364,7 +364,7 @@ digest_md="" if [[ -n "${WORK_ID}" ]]; then _milestone_path="${HOME}/requirements/milestones/${WORK_ID}.md" _analysis_path="${HOME}/spdd/analysis/${WORK_ID}-analysis.md" - digest_md="$(python3 - </sdlc-spdd/scripts/` -- Current shell harness inventory: 28 `tests/test-*.sh` files +- Shell harness inventory: 28 `tests/test-*.sh` files at REF-003 start; 25 + after `test-sdlc-workflow.sh`, `test-sdlc-pointer.sh`, and + `test-archive-work.sh` move to pytest ## A - Approach @@ -169,7 +172,7 @@ already means the only supported engine. `SDLC_ENGINE=shell` and any ### T04 - Migrate the shell-harness and CI matrix -- Status: Pending +- Status: Complete - Description: Rewrite affected workflow, pointer, gate, archive, integration, and live-consumer harnesses to exercise Python-only behavior; remove only tests/workflows made obsolete by equivalent pytest coverage. Reconcile the @@ -182,7 +185,7 @@ already means the only supported engine. `SDLC_ENGINE=shell` and any ### T05 - Synchronize the one-engine documentation contract -- Status: Pending +- Status: Complete - Description: Update active operator, testing, engine, template, and shipped docs to state the mandatory Python contract; preserve historical audit records; update changelog and REF-003 lifecycle status. @@ -245,8 +248,8 @@ already means the only supported engine. `SDLC_ENGINE=shell` and any - [x] T02 retained utilities have no twin dependency - [x] T03 twin files are absent from source and installs - [x] T03 shipped LOC reduction is at least 3,000 -- [ ] T04 current shell/pytest matrix is green -- [ ] T05 current docs state one engine +- [x] T04 current shell/pytest matrix is green +- [x] T05 current docs state one engine - [ ] T06 review, retro, sync, and dependency docs are complete - [ ] Full validation suite is green diff --git a/spec/commands/lifecycle-commit-message.spec.md b/spec/commands/lifecycle-commit-message.spec.md index a191f34b..ef0cfd83 100644 --- a/spec/commands/lifecycle-commit-message.spec.md +++ b/spec/commands/lifecycle-commit-message.spec.md @@ -36,7 +36,7 @@ Your job is to draft a commit message from the current local changes. Collect th 1. Parse optional arguments: a short user hint and an optional Work ID. Do not invent Work IDs. 2. If Work ID is omitted, try the active pointer via `./scripts/sdlc-spdd/sdlc.sh next` (or `./scripts/sdlc.sh next` in the orchestrator repo) or `.sdlc/sessions/current-session.md`. If still unknown, omit Work ID from the message unless the user hint includes one. -3. Collect the change set by running the Python engine (required — do not improvise with raw git when the engine is available): `./scripts/sdlc.sh commit-message` with `--hint` / `--work-id` when known. In the orchestrator repo this always routes to `python -m sdlc_engine commit-message` even when `SDLC_ENGINE=shell`. +3. Collect the change set by running the Python engine (required — do not improvise with raw git when the engine is available): `./scripts/sdlc.sh commit-message` with `--hint` / `--work-id` when known. In the orchestrator repo this always routes to `python -m sdlc_engine commit-message`. 4. Use the engine report as the source of truth for which files/diff to message (source is staged, else unstaged, else commits/diff since merge base). If the engine exits non-zero or reports nothing to commit, report that failure and stop. Do not invent a message. 5. Draft a paste-ready commit message: a concise subject line (imperative mood, ~72 chars); an optional body with why/what when the change needs more than the subject; incorporate the user hint when provided; include the Work ID in the subject or body when known (for example `FEAT-008: …` or a `Work-ID:` trailer). 6. Prefer one focused commit message for the current change set. Do not rewrite unrelated history. diff --git a/spec/commands/lifecycle-sunset.spec.md b/spec/commands/lifecycle-sunset.spec.md index 03b992d2..37270413 100644 --- a/spec/commands/lifecycle-sunset.spec.md +++ b/spec/commands/lifecycle-sunset.spec.md @@ -38,7 +38,7 @@ Your job is to close out a Work ID by collecting GitHub PR, GitHub issue, commit 1. Parse an optional Work ID. Do not invent Work IDs. 2. If Work ID is omitted, try the active pointer via `./scripts/sdlc.sh next` (or `./scripts/sdlc.sh next` in the orchestrator repo) or `.sdlc/sessions/current-session.md`. If still unknown, stop and ask for a Work ID. -3. Collect tracker and git close-out state by running the Python engine (required — do not improvise with raw `gh`, Jira HTTP, or `git log` when the engine is available): `./scripts/sdlc.sh sunset --work-id --apply`. In the orchestrator repo this always routes to `python -m sdlc_engine sunset` even when `SDLC_ENGINE=shell`. +3. Collect tracker and git close-out state by running the Python engine (required — do not improvise with raw `gh`, Jira HTTP, or `git log` when the engine is available): `./scripts/sdlc.sh sunset --work-id --apply`. In the orchestrator repo this always routes to `python -m sdlc_engine sunset`. 4. Use the engine report as the source of truth for Jira key/status, GitHub issue and PR number/title/state/URL, and matching commits. If the engine exits non-zero, report that failure and stop. Do not invent issue, PR, commit, or Jira facts. 5. Treat missing remotes as warnings, not a hard stop, when the engine still produced a snapshot (for example `gh` is not installed, or Jira credentials are unset). Report every warning from the engine. 6. After a successful `--apply`, the engine has staged a `session` record (`source=sunset`) in `.sdlc/staged/lessons.jsonl`. Recommend `/sdlc-spdd-accept` to promote it into `spdd/memory/lessons.jsonl`. Do not edit the ledger by hand. diff --git a/templates/agent-context/README.md b/templates/agent-context/README.md index 6a3776b9..52ca7c5a 100644 --- a/templates/agent-context/README.md +++ b/templates/agent-context/README.md @@ -9,7 +9,7 @@ orchestrator repo (storage v3 dogfood layout). |------|------| | `harness/` | `phase-index.md`, quality gates, validation rules | | `harness/skills/` | Phase-matching and `#SkillName` skills — resolve with `resolve-agent-context.sh` | -| `sdlc-pointer.sh`, `sdlc-workflow.sh`, `sdlc-team-registry.sh` | Workflow CLI (also installed under `scripts/` in targets) | +| `scripts/sdlc.sh` (in targets) | Thin dispatcher into the Python `sdlc_engine` lifecycle CLI | Committed memory lives under **`spdd/memory/`** (`lessons.jsonl`, `registry.jsonl`). Hot session brief: **`.sdlc/sessions/current-session.md`** (gitignored). @@ -50,8 +50,8 @@ contracts (`spdd/canvas/`, analysis, reviews) are the audit trail; run ./scripts/sdlc.sh capture --summary "finished T02" ``` -Set `SDLC_ENGINE=python` (or `auto`) to use the Python engine — see -[docs/engine-v2.md](../docs/engine-v2.md). +`sdlc.sh` always runs the Python engine; there is no shell engine to select — +see [docs/engine-v2.md](../docs/engine-v2.md). ## Team registry diff --git a/templates/agent-context/harness/guide-dice.md b/templates/agent-context/harness/guide-dice.md index 7f909657..87f2197a 100644 --- a/templates/agent-context/harness/guide-dice.md +++ b/templates/agent-context/harness/guide-dice.md @@ -39,7 +39,7 @@ When native MCP is unavailable, agents **must** use the same tools via CLI: ```bash ./scripts/guide/query-guide.sh --text --work-id ./scripts/guide/query-guide.sh --area engine/tests -SDLC_ENGINE=python ./scripts/sdlc.sh context mcp-call --tool spdd_getLesson --json '{"id":""}' +./scripts/sdlc.sh context mcp-call --tool spdd_getLesson --json '{"id":""}' ``` MCP config snippet: `./scripts/guide/mcp-config-snippet.sh --cursor` diff --git a/templates/claude/commands/sdlc-spdd-commit-message.md b/templates/claude/commands/sdlc-spdd-commit-message.md index 8d225272..504bd473 100644 --- a/templates/claude/commands/sdlc-spdd-commit-message.md +++ b/templates/claude/commands/sdlc-spdd-commit-message.md @@ -10,7 +10,7 @@ Your job is to draft a commit message from the current local changes. Collect th 1. Parse optional arguments: a short user hint and an optional Work ID. Do not invent Work IDs. 2. If Work ID is omitted, try the active pointer via `./scripts/sdlc-spdd/sdlc.sh next` (or `./scripts/sdlc.sh next` in the orchestrator repo) or `.sdlc/sessions/current-session.md`. If still unknown, omit Work ID from the message unless the user hint includes one. -3. Collect the change set by running the Python engine (required — do not improvise with raw git when the engine is available): `./scripts/sdlc.sh commit-message` with `--hint` / `--work-id` when known. In the orchestrator repo this always routes to `python -m sdlc_engine commit-message` even when `SDLC_ENGINE=shell`. +3. Collect the change set by running the Python engine (required — do not improvise with raw git when the engine is available): `./scripts/sdlc.sh commit-message` with `--hint` / `--work-id` when known. In the orchestrator repo this always routes to `python -m sdlc_engine commit-message`. 4. Use the engine report as the source of truth for which files/diff to message (source is staged, else unstaged, else commits/diff since merge base). If the engine exits non-zero or reports nothing to commit, report that failure and stop. Do not invent a message. 5. Draft a paste-ready commit message: a concise subject line (imperative mood, ~72 chars); an optional body with why/what when the change needs more than the subject; incorporate the user hint when provided; include the Work ID in the subject or body when known (for example `FEAT-008: …` or a `Work-ID:` trailer). 6. Prefer one focused commit message for the current change set. Do not rewrite unrelated history. diff --git a/templates/claude/commands/sdlc-spdd-sunset.md b/templates/claude/commands/sdlc-spdd-sunset.md index 777009e9..0c1c20ae 100644 --- a/templates/claude/commands/sdlc-spdd-sunset.md +++ b/templates/claude/commands/sdlc-spdd-sunset.md @@ -15,7 +15,7 @@ Your job is to close out a Work ID by collecting GitHub PR, GitHub issue, commit 1. Parse an optional Work ID. Do not invent Work IDs. 2. If Work ID is omitted, try the active pointer via `./scripts/sdlc.sh next` (or `./scripts/sdlc.sh next` in the orchestrator repo) or `.sdlc/sessions/current-session.md`. If still unknown, stop and ask for a Work ID. -3. Collect tracker and git close-out state by running the Python engine (required — do not improvise with raw `gh`, Jira HTTP, or `git log` when the engine is available): `./scripts/sdlc.sh sunset --work-id --apply`. In the orchestrator repo this always routes to `python -m sdlc_engine sunset` even when `SDLC_ENGINE=shell`. +3. Collect tracker and git close-out state by running the Python engine (required — do not improvise with raw `gh`, Jira HTTP, or `git log` when the engine is available): `./scripts/sdlc.sh sunset --work-id --apply`. In the orchestrator repo this always routes to `python -m sdlc_engine sunset`. 4. Use the engine report as the source of truth for Jira key/status, GitHub issue and PR number/title/state/URL, and matching commits. If the engine exits non-zero, report that failure and stop. Do not invent issue, PR, commit, or Jira facts. 5. Treat missing remotes as warnings, not a hard stop, when the engine still produced a snapshot (for example `gh` is not installed, or Jira credentials are unset). Report every warning from the engine. 6. After a successful `--apply`, the engine has staged a `session` record (`source=sunset`) in `.sdlc/staged/lessons.jsonl`. Recommend `/sdlc-spdd-accept` to promote it into `spdd/memory/lessons.jsonl`. Do not edit the ledger by hand. diff --git a/templates/copilot/prompts/sdlc-spdd-commit-message.prompt.md b/templates/copilot/prompts/sdlc-spdd-commit-message.prompt.md index 9afe73dc..3802aa30 100644 --- a/templates/copilot/prompts/sdlc-spdd-commit-message.prompt.md +++ b/templates/copilot/prompts/sdlc-spdd-commit-message.prompt.md @@ -15,7 +15,7 @@ Draft a commit message from the current local changes. Collect the diff through 1. Parse optional arguments: a short user hint and an optional Work ID. Do not invent Work IDs. 2. If Work ID is omitted, try the active pointer via `./scripts/sdlc-spdd/sdlc.sh next` (or `./scripts/sdlc.sh next` in the orchestrator repo) or `.sdlc/sessions/current-session.md`. If still unknown, omit Work ID from the message unless the user hint includes one. -3. Collect the change set by running the Python engine (required — do not improvise with raw git when the engine is available): `./scripts/sdlc.sh commit-message` with `--hint` / `--work-id` when known. In the orchestrator repo this always routes to `python -m sdlc_engine commit-message` even when `SDLC_ENGINE=shell`. +3. Collect the change set by running the Python engine (required — do not improvise with raw git when the engine is available): `./scripts/sdlc.sh commit-message` with `--hint` / `--work-id` when known. In the orchestrator repo this always routes to `python -m sdlc_engine commit-message`. 4. Use the engine report as the source of truth for which files/diff to message (source is staged, else unstaged, else commits/diff since merge base). If the engine exits non-zero or reports nothing to commit, report that failure and stop. Do not invent a message. 5. Draft a paste-ready commit message: a concise subject line (imperative mood, ~72 chars); an optional body with why/what when the change needs more than the subject; incorporate the user hint when provided; include the Work ID in the subject or body when known (for example `FEAT-008: …` or a `Work-ID:` trailer). 6. Prefer one focused commit message for the current change set. Do not rewrite unrelated history. diff --git a/templates/copilot/prompts/sdlc-spdd-sunset.prompt.md b/templates/copilot/prompts/sdlc-spdd-sunset.prompt.md index 61a02a36..c11ad84f 100644 --- a/templates/copilot/prompts/sdlc-spdd-sunset.prompt.md +++ b/templates/copilot/prompts/sdlc-spdd-sunset.prompt.md @@ -15,7 +15,7 @@ Close out a Work ID by collecting GitHub PR, GitHub issue, commit, and Jira stat 1. Parse an optional Work ID. Do not invent Work IDs. 2. If Work ID is omitted, try the active pointer via `./scripts/sdlc.sh next` (or `./scripts/sdlc.sh next` in the orchestrator repo) or `.sdlc/sessions/current-session.md`. If still unknown, stop and ask for a Work ID. -3. Collect tracker and git close-out state by running the Python engine (required — do not improvise with raw `gh`, Jira HTTP, or `git log` when the engine is available): `./scripts/sdlc.sh sunset --work-id --apply`. In the orchestrator repo this always routes to `python -m sdlc_engine sunset` even when `SDLC_ENGINE=shell`. +3. Collect tracker and git close-out state by running the Python engine (required — do not improvise with raw `gh`, Jira HTTP, or `git log` when the engine is available): `./scripts/sdlc.sh sunset --work-id --apply`. In the orchestrator repo this always routes to `python -m sdlc_engine sunset`. 4. Use the engine report as the source of truth for Jira key/status, GitHub issue and PR number/title/state/URL, and matching commits. If the engine exits non-zero, report that failure and stop. Do not invent issue, PR, commit, or Jira facts. 5. Treat missing remotes as warnings, not a hard stop, when the engine still produced a snapshot (for example `gh` is not installed, or Jira credentials are unset). Report every warning from the engine. 6. After a successful `--apply`, the engine has staged a `session` record (`source=sunset`) in `.sdlc/staged/lessons.jsonl`. Recommend `/sdlc-spdd-accept` to promote it into `spdd/memory/lessons.jsonl`. Do not edit the ledger by hand. diff --git a/templates/cursor/sdlc-spdd-commit-message.md b/templates/cursor/sdlc-spdd-commit-message.md index 8d225272..504bd473 100644 --- a/templates/cursor/sdlc-spdd-commit-message.md +++ b/templates/cursor/sdlc-spdd-commit-message.md @@ -10,7 +10,7 @@ Your job is to draft a commit message from the current local changes. Collect th 1. Parse optional arguments: a short user hint and an optional Work ID. Do not invent Work IDs. 2. If Work ID is omitted, try the active pointer via `./scripts/sdlc-spdd/sdlc.sh next` (or `./scripts/sdlc.sh next` in the orchestrator repo) or `.sdlc/sessions/current-session.md`. If still unknown, omit Work ID from the message unless the user hint includes one. -3. Collect the change set by running the Python engine (required — do not improvise with raw git when the engine is available): `./scripts/sdlc.sh commit-message` with `--hint` / `--work-id` when known. In the orchestrator repo this always routes to `python -m sdlc_engine commit-message` even when `SDLC_ENGINE=shell`. +3. Collect the change set by running the Python engine (required — do not improvise with raw git when the engine is available): `./scripts/sdlc.sh commit-message` with `--hint` / `--work-id` when known. In the orchestrator repo this always routes to `python -m sdlc_engine commit-message`. 4. Use the engine report as the source of truth for which files/diff to message (source is staged, else unstaged, else commits/diff since merge base). If the engine exits non-zero or reports nothing to commit, report that failure and stop. Do not invent a message. 5. Draft a paste-ready commit message: a concise subject line (imperative mood, ~72 chars); an optional body with why/what when the change needs more than the subject; incorporate the user hint when provided; include the Work ID in the subject or body when known (for example `FEAT-008: …` or a `Work-ID:` trailer). 6. Prefer one focused commit message for the current change set. Do not rewrite unrelated history. diff --git a/templates/cursor/sdlc-spdd-sunset.md b/templates/cursor/sdlc-spdd-sunset.md index edf8faf3..8c4874cb 100644 --- a/templates/cursor/sdlc-spdd-sunset.md +++ b/templates/cursor/sdlc-spdd-sunset.md @@ -10,7 +10,7 @@ Your job is to close out a Work ID by collecting GitHub PR, GitHub issue, commit 1. Parse an optional Work ID. Do not invent Work IDs. 2. If Work ID is omitted, try the active pointer via `./scripts/sdlc.sh next` (or `./scripts/sdlc.sh next` in the orchestrator repo) or `.sdlc/sessions/current-session.md`. If still unknown, stop and ask for a Work ID. -3. Collect tracker and git close-out state by running the Python engine (required — do not improvise with raw `gh`, Jira HTTP, or `git log` when the engine is available): `./scripts/sdlc.sh sunset --work-id --apply`. In the orchestrator repo this always routes to `python -m sdlc_engine sunset` even when `SDLC_ENGINE=shell`. +3. Collect tracker and git close-out state by running the Python engine (required — do not improvise with raw `gh`, Jira HTTP, or `git log` when the engine is available): `./scripts/sdlc.sh sunset --work-id --apply`. In the orchestrator repo this always routes to `python -m sdlc_engine sunset`. 4. Use the engine report as the source of truth for Jira key/status, GitHub issue and PR number/title/state/URL, and matching commits. If the engine exits non-zero, report that failure and stop. Do not invent issue, PR, commit, or Jira facts. 5. Treat missing remotes as warnings, not a hard stop, when the engine still produced a snapshot (for example `gh` is not installed, or Jira credentials are unset). Report every warning from the engine. 6. After a successful `--apply`, the engine has staged a `session` record (`source=sunset`) in `.sdlc/staged/lessons.jsonl`. Recommend `/sdlc-spdd-accept` to promote it into `spdd/memory/lessons.jsonl`. Do not edit the ledger by hand. diff --git a/templates/requirements/milestones/README.md b/templates/requirements/milestones/README.md index 0f632bc7..14139b3e 100644 --- a/templates/requirements/milestones/README.md +++ b/templates/requirements/milestones/README.md @@ -48,17 +48,17 @@ Each milestone requirement file stores Jira syntax: - **Before create** — fill Summary, Description, acceptance criteria, labels, components - **After create** — set `- Key: ABC-123` and commit -- **On claim** — `./scripts/sdlc.sh claim ` (or `SDLC_ENGINE=python`) auto-reads the Key into the team registry +- **On claim** — `./scripts/sdlc.sh claim ` auto-reads the Key into the team registry `jira:` note token (disable with `SDLC_TEAM_AUTO_JIRA=0`) Engine helpers (v2): ```bash -SDLC_ENGINE=python ./scripts/sdlc.sh issues draft --system jira -SDLC_ENGINE=python ./scripts/sdlc.sh issues draft --system jira --format adf # Cloud payload preview -SDLC_ENGINE=python ./scripts/sdlc.sh issues push --system jira # dry-run -SDLC_ENGINE=python ./scripts/sdlc.sh issues push --system jira --apply # ADF on Jira Cloud -SDLC_ENGINE=python ./scripts/sdlc.sh sync-links --repair +./scripts/sdlc.sh issues draft --system jira +./scripts/sdlc.sh issues draft --system jira --format adf # Cloud payload preview +./scripts/sdlc.sh issues push --system jira # dry-run +./scripts/sdlc.sh issues push --system jira --apply # ADF on Jira Cloud +./scripts/sdlc.sh sync-links --repair ``` Jira Cloud needs ADF for descriptions — the engine converts this markdown @@ -82,7 +82,7 @@ Optional `## GitHub` section for teams that track delivery in GitHub Issues: After create, set `Number` / `URL`. Claim auto-links `github:#N` (disable with `SDLC_TEAM_AUTO_GITHUB=0`). ```bash -SDLC_ENGINE=python ./scripts/sdlc.sh issues push --system github --apply # uses gh CLI +./scripts/sdlc.sh issues push --system github --apply # uses gh CLI ``` diff --git a/tests/research/test_ref001_sut.py b/tests/research/test_ref001_sut.py index f2b2275f..acf07f35 100644 --- a/tests/research/test_ref001_sut.py +++ b/tests/research/test_ref001_sut.py @@ -67,8 +67,8 @@ def test_sut_doc_names_python_gate_check(self) -> None: text = SUT_DOC.read_text(encoding="utf-8") blob = text.lower() self.assertIn("workflowengine.gate_check", blob) - self.assertIn("sdlc_engine=auto", blob) self.assertIn("sdlc_gate_engine=shell", blob) + self.assertIn("rejected", blob) self.assertIn("not the sut", blob) def test_testing_and_roadmap_name_sut(self) -> None: From 7ce96743af82cf7029b70845891274f28b2cc94c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 14 Sep 2026 06:06:11 +0000 Subject: [PATCH 18/18] docs: close out REF-003 lifecycle records REF-003 T06. Add the review and sync artifacts, mark every acceptance criterion and operation complete against a recorded run, reconcile the stale shell-harness counts (30 -> 28 -> 25), and mark REF-003 Complete in the requirement, Milestone 3, and the roadmap. Accept the four staged lessons. Co-authored-by: John Menke --- sdlc-spdd/ROADMAP.md | 2 +- .../milestones/milestone-3/MILESTONE-3.md | 4 +- .../REF-003-retire-bash-workflow-dual-path.md | 15 ++- .../REF-003-retire-bash-workflow-dual-path.md | 19 +-- sdlc-spdd/spdd/memory/context-index.md | 3 + sdlc-spdd/spdd/memory/lessons.jsonl | 4 + sdlc-spdd/spdd/memory/registry.jsonl | 3 + ...3-retire-bash-workflow-dual-path-review.md | 119 ++++++++++++++++++ ...003-retire-bash-workflow-dual-path-sync.md | 99 +++++++++++++++ 9 files changed, 251 insertions(+), 17 deletions(-) create mode 100644 sdlc-spdd/spdd/reviews/REF-003-retire-bash-workflow-dual-path-review.md create mode 100644 sdlc-spdd/spdd/sync/REF-003-retire-bash-workflow-dual-path-sync.md diff --git a/sdlc-spdd/ROADMAP.md b/sdlc-spdd/ROADMAP.md index 2e7d0aad..d5852674 100644 --- a/sdlc-spdd/ROADMAP.md +++ b/sdlc-spdd/ROADMAP.md @@ -223,7 +223,7 @@ Milestone 1 feature track is Complete on the integration branch. | CHORE-004-lint-and-complexity-gates-real | make it right | Complete (Milestone 3 P0, #308) | | CHORE-006-dogfood-adapters-and-quick-spec | make it right | Complete (Milestone 3 P0, #308 + #320) | | REF-002-purge-pre-v3-compat | make it right | Complete (Milestone 3 P0, #314; 25/25 merge checks) | -| REF-003-retire-bash-workflow-dual-path | make it right | Milestone 3 P0 | +| REF-003-retire-bash-workflow-dual-path | make it right | Complete (Milestone 3 P0, #321; 25/25 PR checks) | | REF-004…REF-011, CHORE-005, TEST-004 | make it right | Milestone 3 P1/P2 | | DOC-004…DOC-007, CHORE-007…CHORE-010 | make it right (docs) | Milestone 4 | diff --git a/sdlc-spdd/requirements/milestones/milestone-3/MILESTONE-3.md b/sdlc-spdd/requirements/milestones/milestone-3/MILESTONE-3.md index b854a293..aa51f85b 100644 --- a/sdlc-spdd/requirements/milestones/milestone-3/MILESTONE-3.md +++ b/sdlc-spdd/requirements/milestones/milestone-3/MILESTONE-3.md @@ -28,7 +28,7 @@ P0 — one flow (do first, in order): - [x] CHORE-004-lint-and-complexity-gates-real — gates run on the real diff (#308) - [x] CHORE-006-dogfood-adapters-and-quick-spec — dogfood packs and `quick` regenerate from canonical specs (#308, #320) - [x] REF-002-purge-pre-v3-compat — one data model; archive deletes; no migrations (#314) -- [ ] REF-003-retire-bash-workflow-dual-path — one engine; no shell twin +- [x] REF-003-retire-bash-workflow-dual-path — one engine; no shell twin P1 — make the one flow maintainable: @@ -55,7 +55,7 @@ P2 — finish the consolidation: | CHORE-004-lint-and-complexity-gates-real | P0 | M | [requirement](CHORE-004-lint-and-complexity-gates-real.md) | Complete | Lint, complexity, shellcheck gates on the real diff | | CHORE-006-dogfood-adapters-and-quick-spec | P0 | S | [requirement](CHORE-006-dogfood-adapters-and-quick-spec.md) | Complete | Dogfood parity in #308; canonical quick spec in #320 | | REF-002-purge-pre-v3-compat | P0 | L | [requirement](REF-002-purge-pre-v3-compat.md) | Complete | Storage v3 is the only supported layout (#314) | -| REF-003-retire-bash-workflow-dual-path | P0 | L | [requirement](REF-003-retire-bash-workflow-dual-path.md) | To Do | Retire the bash workflow twin; Python is the only flow | +| REF-003-retire-bash-workflow-dual-path | P0 | L | [requirement](REF-003-retire-bash-workflow-dual-path.md) | Complete | Bash workflow twin retired; Python is the only flow (#321) | | REF-004-split-installer-blueprints | P1 | M | [requirement](REF-004-split-installer-blueprints.md) | To Do | Split `installer/app.py` `create_app` into blueprints | | REF-005-cli-command-modules | P1 | M | [requirement](REF-005-cli-command-modules.md) | To Do | One module per CLI command; result objects, not prints | | REF-006-storage-records-and-atomic-appends | P1 | M | [requirement](REF-006-storage-records-and-atomic-appends.md) | To Do | Typed storage records; atomic, locked JSONL appends | diff --git a/sdlc-spdd/requirements/milestones/milestone-3/REF-003-retire-bash-workflow-dual-path.md b/sdlc-spdd/requirements/milestones/milestone-3/REF-003-retire-bash-workflow-dual-path.md index 03565a8b..ed33b6ca 100644 --- a/sdlc-spdd/requirements/milestones/milestone-3/REF-003-retire-bash-workflow-dual-path.md +++ b/sdlc-spdd/requirements/milestones/milestone-3/REF-003-retire-bash-workflow-dual-path.md @@ -4,7 +4,7 @@ jira_key: "" github_number: "" jira_epic: "" jira_type: "Refactor" -jira_status: "To Do" +jira_status: "Done" jira_assignee: "" jira_due_date: "" jira_sprint: "" @@ -23,7 +23,7 @@ related: **Work ID:** REF-003-retire-bash-workflow-dual-path **Milestone:** Milestone 3 — One flow on storage v3 -**Status:** To Do +**Status:** Done (PR #321) **Priority / size:** P0 / L **Date:** 2026-09-13 **Beck stage:** make it right (one engine, one persistence model, aligned docs/tests) @@ -58,10 +58,13 @@ sdlc-workflow.sh (2 089 LOC) and sdlc-team-registry.sh (981 LOC) reimplement wor ## Acceptance Criteria -- [ ] templates/agent-context contains no sdlc-workflow.sh / sdlc-team-registry.sh / sdlc-pointer.sh -- [ ] `SDLC_ENGINE=shell ./scripts/sdlc.sh next` exits non-zero with a clear message -- [ ] All 30 bash harnesses either pass against the Python-only dispatcher or are removed with a pytest replacement -- [ ] Shipped script LOC drops by at least 3 000 +- [x] templates/agent-context contains no sdlc-workflow.sh / sdlc-team-registry.sh / sdlc-pointer.sh +- [x] `SDLC_ENGINE=shell ./scripts/sdlc.sh next` exits non-zero with a clear message +- [x] All bash harnesses either pass against the Python-only dispatcher or are removed with a pytest + replacement. The count of 30 predates REF-002; the inventory was 28 when REF-003 began and is 25 + after `test-sdlc-workflow.sh`, `test-sdlc-pointer.sh`, and `test-archive-work.sh` moved to pytest. + 24 of 25 pass locally; `test-guide-stack-live.sh` needs a live Guide + Neo4j stack. +- [x] Shipped script LOC drops by at least 3 000 (3 100 template LOC; 6 200 with dogfood copies) ## Non-Goals diff --git a/sdlc-spdd/spdd/canvas/REF-003-retire-bash-workflow-dual-path.md b/sdlc-spdd/spdd/canvas/REF-003-retire-bash-workflow-dual-path.md index cface1f8..d91bcc4a 100644 --- a/sdlc-spdd/spdd/canvas/REF-003-retire-bash-workflow-dual-path.md +++ b/sdlc-spdd/spdd/canvas/REF-003-retire-bash-workflow-dual-path.md @@ -4,8 +4,8 @@ - Work ID: REF-003-retire-bash-workflow-dual-path - Work Type: Refactor -- Status: In Progress -- Readiness: Ready For Coding +- Status: Complete +- Readiness: Complete - Created: 2026-09-14 - Updated: 2026-09-14 - Milestone: milestone-3 @@ -197,7 +197,7 @@ already means the only supported engine. `SDLC_ENGINE=shell` and any ### T06 - Full validation, review, retro, and sync -- Status: Pending +- Status: Complete - Description: Run the complete local unit/integration/shell/live-consumer and documentation validation matrix, perform scope/review checks, capture and accept lessons, synchronize milestone/roadmap/dependencies, and release the @@ -250,15 +250,18 @@ already means the only supported engine. `SDLC_ENGINE=shell` and any - [x] T03 shipped LOC reduction is at least 3,000 - [x] T04 current shell/pytest matrix is green - [x] T05 current docs state one engine -- [ ] T06 review, retro, sync, and dependency docs are complete -- [ ] Full validation suite is green +- [x] T06 review, retro, sync, and dependency docs are complete +- [x] Full validation suite is green ## Sync Notes Begins from PR #320 merge commit `5ea5c8f`; all four post-merge `main` -workflows passed before REF-003 coding began. +workflows passed before REF-003 coding began. Delivered in PR #321 with 25/25 +checks green. Review: +`sdlc-spdd/spdd/reviews/REF-003-retire-bash-workflow-dual-path-review.md`. +Sync: `sdlc-spdd/spdd/sync/REF-003-retire-bash-workflow-dual-path-sync.md`. ## Final Status -- Readiness: Ready For Coding -- Status: In Progress +- Readiness: Complete +- Status: Complete diff --git a/sdlc-spdd/spdd/memory/context-index.md b/sdlc-spdd/spdd/memory/context-index.md index 7404805f..ca8f20b2 100644 --- a/sdlc-spdd/spdd/memory/context-index.md +++ b/sdlc-spdd/spdd/memory/context-index.md @@ -4,6 +4,9 @@ | Area | Kind | Work ID | Phase | Timestamp | Source | Entry | |------|------|---------|-------|-----------|--------|-------| +| tests/test- | pitfall | REF-003-retire-bash-workflow-dual-path | code | 2026-09-14T06:00:59Z | capture | Pitfall: REF-003-retire-bash-workflow-dual-path | +| tests/test- | pattern | REF-003-retire-bash-workflow-dual-path | code | 2026-09-14T06:00:59Z | capture | Pattern: REF-003-retire-bash-workflow-dual-path | +| tests/test- | decision | REF-003-retire-bash-workflow-dual-path | code | 2026-09-14T06:00:59Z | capture | Decision: REF-003-retire-bash-workflow-dual-path | | adapters | pitfall | CHORE-006-dogfood-adapters-and-quick-spec | retro | 2026-09-14T04:37:23Z | capture | Pitfall: CHORE-006-dogfood-adapters-and-quick-spec | | adapters | pattern | CHORE-006-dogfood-adapters-and-quick-spec | retro | 2026-09-14T04:37:23Z | capture | Pattern: CHORE-006-dogfood-adapters-and-quick-spec | | scripts/lib | pitfall | REF-002-purge-pre-v3-compat | retro | 2026-09-14T04:21:58Z | capture | Pitfall: REF-002-purge-pre-v3-compat | diff --git a/sdlc-spdd/spdd/memory/lessons.jsonl b/sdlc-spdd/spdd/memory/lessons.jsonl index e2ed6cae..02594bc6 100644 --- a/sdlc-spdd/spdd/memory/lessons.jsonl +++ b/sdlc-spdd/spdd/memory/lessons.jsonl @@ -12,3 +12,7 @@ {"id": "session:CHORE-006-dogfood-adapters-and-quick-spec:adapters:capture", "kind": "session", "work_id": "CHORE-006-dogfood-adapters-and-quick-spec", "area": "adapters", "phase": "retro", "ts": "2026-09-14T04:37:23Z", "title": "CHORE-006 approved: quick is now generated from a canonical spec with byte-stable outputs.", "body": "Phase: retro\nSummary: CHORE-006 approved: quick is now generated from a canonical spec with byte-stable outputs.\nValidation: 367 command-spec and 785 adapter-install checks pass; generator, template/installed validators, scope, canvas, and requirements pass.\nVerify: command=./tests/test-command-specs.sh; exit=0; result=pass\nNext: /sdlc-spdd-sync CHORE-006-dogfood-adapters-and-quick-spec\nAreas: adapters command-specs testing\nMetrics: readiness=Reviewed;review-result=pass;rework=0;validate-cycles=1;review-cycles=1", "source": "capture", "keywords": ["command-specs", "testing"], "commit": "0af1d60", "schema": 3, "metrics": {"readiness": "Reviewed", "review_result": "pass", "rework": 0, "validate_cycles": 1, "review_cycles": 1}, "verify": {"command": "./tests/test-command-specs.sh", "exit": 0, "result": "pass"}} {"id": "pitfall:CHORE-006-dogfood-adapters-and-quick-spec:adapters:capture", "kind": "pitfall", "work_id": "CHORE-006-dogfood-adapters-and-quick-spec", "area": "adapters", "phase": "retro", "ts": "2026-09-14T04:37:23Z", "title": "Pitfall: CHORE-006-dogfood-adapters-and-quick-spec", "body": "One-way spec-to-template inventory checks do not detect orphan templates that have no spec; require reverse inventory or an explicit contract assertion for exceptional commands.", "source": "capture", "keywords": ["command-specs", "testing"], "commit": "0af1d60", "schema": 1} {"id": "pattern:CHORE-006-dogfood-adapters-and-quick-spec:adapters:capture", "kind": "pattern", "work_id": "CHORE-006-dogfood-adapters-and-quick-spec", "area": "adapters", "phase": "retro", "ts": "2026-09-14T04:37:23Z", "title": "Pattern: CHORE-006-dogfood-adapters-and-quick-spec", "body": "Move an existing hand-maintained adapter under generation by first encoding its current behavior, then require a zero generated diff before accepting source-of-truth ownership.", "source": "capture", "keywords": ["command-specs", "testing"], "commit": "0af1d60", "schema": 1} +{"id": "session:REF-003-retire-bash-workflow-dual-path:tests/test-:capture", "kind": "session", "work_id": "REF-003-retire-bash-workflow-dual-path", "area": "tests/test-", "phase": "code", "ts": "2026-09-14T06:00:59Z", "title": "T04/T05/T06: fixed PR #321 CI, migrated the harness matrix, and synced the one-engine docs", "body": "Phase: code\nSummary: T04/T05/T06: fixed PR #321 CI, migrated the harness matrix, and synced the one-engine docs\nValidation: Not recorded\nVerify: command=./scripts/run-test-suites.sh unit && ./scripts/run-test-suites.sh integration && 24/25 tests/test-*.sh; exit=0; result=pass\nNext: Write REF-003 review and sync artifacts, then accept staged lessons\nAreas: tests/test- src/sdlc_engine tests/live-consumer scripts/sdlc- tests/workflows tests/research scripts/lib scripts/tests tests/lib tests engine/src/sdlc_engine docs", "source": "capture", "keywords": ["src/sdlc_engine", "tests/live-consumer", "scripts/sdlc-", "tests/workflows", "tests/research", "scripts/lib", "scripts/tests", "tests/lib", "tests", "engine/src/sdlc_engine", "docs"], "commit": "cb19e84", "schema": 3, "verify": {"command": "./scripts/run-test-suites.sh unit && ./scripts/run-test-suites.sh integration && 24/25 tests/test-*.sh", "exit": 0, "result": "pass"}} +{"id": "decision:REF-003-retire-bash-workflow-dual-path:tests/test-:capture", "kind": "decision", "work_id": "REF-003-retire-bash-workflow-dual-path", "area": "tests/test-", "phase": "code", "ts": "2026-09-14T06:00:59Z", "title": "Decision: REF-003-retire-bash-workflow-dual-path", "body": "Shell harnesses resolve their interpreter through one shared tests/lib/engine-python.sh helper instead of each pinning the repo .venv, because CI installs the engine into the job interpreter and never creates a repo venv.", "source": "capture", "keywords": ["src/sdlc_engine", "tests/live-consumer", "scripts/sdlc-", "tests/workflows", "tests/research", "scripts/lib", "scripts/tests", "tests/lib", "tests", "engine/src/sdlc_engine", "docs"], "commit": "cb19e84", "schema": 1} +{"id": "pitfall:REF-003-retire-bash-workflow-dual-path:tests/test-:capture", "kind": "pitfall", "work_id": "REF-003-retire-bash-workflow-dual-path", "area": "tests/test-", "phase": "code", "ts": "2026-09-14T06:00:59Z", "title": "Pitfall: REF-003-retire-bash-workflow-dual-path", "body": "resolve_engine_python read the interpreter version into 'read -r major minor' via a here-string; when the interpreter could not run at all the here-string still fed read one empty line, so read returned 0 and the guard printed a truncated 'is 3.' version instead of reporting an unrunnable interpreter. Detect the empty version string before parsing.", "source": "capture", "keywords": ["src/sdlc_engine", "tests/live-consumer", "scripts/sdlc-", "tests/workflows", "tests/research", "scripts/lib", "scripts/tests", "tests/lib", "tests", "engine/src/sdlc_engine", "docs"], "commit": "cb19e84", "schema": 1} +{"id": "pattern:REF-003-retire-bash-workflow-dual-path:tests/test-:capture", "kind": "pattern", "work_id": "REF-003-retire-bash-workflow-dual-path", "area": "tests/test-", "phase": "code", "ts": "2026-09-14T06:00:59Z", "title": "Pattern: REF-003-retire-bash-workflow-dual-path", "body": "The diff complexity gate fails on any CCN rise in a touched function, so growth belongs in a new extracted helper (_shell_script_candidates/_resolve_shell_script) rather than inline in the existing function. The gate reads git HEAD, not the working tree, so it must be re-run after committing.", "source": "capture", "keywords": ["src/sdlc_engine", "tests/live-consumer", "scripts/sdlc-", "tests/workflows", "tests/research", "scripts/lib", "scripts/tests", "tests/lib", "tests", "engine/src/sdlc_engine", "docs"], "commit": "cb19e84", "schema": 1} diff --git a/sdlc-spdd/spdd/memory/registry.jsonl b/sdlc-spdd/spdd/memory/registry.jsonl index 53a7d4e9..2a788bed 100644 --- a/sdlc-spdd/spdd/memory/registry.jsonl +++ b/sdlc-spdd/spdd/memory/registry.jsonl @@ -47,3 +47,6 @@ {"event": "release", "work_id": "CHORE-006-dogfood-adapters-and-quick-spec", "status": "shelved", "phase": "retro", "operation": "", "owner": "Cursor Agent", "note": "CHORE-006 complete in PR #320; review, retro, and sync recorded", "ts": "2026-09-14T04:38:32Z"} {"event": "refresh", "work_id": "CHORE-006-dogfood-adapters-and-quick-spec", "status": "done", "phase": "retro", "operation": "", "owner": "Cursor Agent", "note": "canvas Final Status: Complete", "ts": "2026-09-14T04:38:32Z"} {"event": "claim", "work_id": "REF-003-retire-bash-workflow-dual-path", "status": "active", "phase": "init", "operation": "", "owner": "Cursor Agent", "note": "", "ts": "2026-09-14T04:42:43Z"} +{"event": "claim", "work_id": "REF-003-retire-bash-workflow-dual-path", "status": "active", "phase": "code", "operation": "T06", "owner": "Cursor Agent", "note": "", "ts": "2026-09-14T05:50:26Z"} +{"event": "release", "work_id": "REF-003-retire-bash-workflow-dual-path", "status": "shelved", "phase": "code", "operation": "", "owner": "Cursor Agent", "note": "REF-003 complete; PR #321", "ts": "2026-09-14T06:04:35Z"} +{"event": "refresh", "work_id": "REF-003-retire-bash-workflow-dual-path", "status": "done", "phase": "code", "operation": "", "owner": "Cursor Agent", "note": "canvas Final Status: Complete", "ts": "2026-09-14T06:05:00Z"} diff --git a/sdlc-spdd/spdd/reviews/REF-003-retire-bash-workflow-dual-path-review.md b/sdlc-spdd/spdd/reviews/REF-003-retire-bash-workflow-dual-path-review.md new file mode 100644 index 00000000..9945d0fc --- /dev/null +++ b/sdlc-spdd/spdd/reviews/REF-003-retire-bash-workflow-dual-path-review.md @@ -0,0 +1,119 @@ +# Review: REF-003-retire-bash-workflow-dual-path + +**Work ID:** REF-003-retire-bash-workflow-dual-path +**Date:** 2026-09-14 +**Result:** Approved With Notes +**Pull Request:** https://github.com/jmjava/sdlc-spdd-orchestrator/pull/321 +**Reviewed Commit:** `cb19e84e50eda9cf6e68e61ca90415cee7ef8b79` + +## Summary + +PR #321 implements the approved Python-only lifecycle contract. The bash +workflow, gate, pointer, and team-registry twins are deleted from the shipped +templates and the dogfood copies, `scripts/sdlc.sh` is a thin dispatcher that +requires Python 3.12 and an importable `sdlc_engine`, and the retained +session/capture/accept utilities call the engine rather than reimplementing it. +All six operations T01–T06 are complete and every acceptance criterion is +verified against a run, not inferred. + +Readiness was **Ready For Coding** when implementation began; coding did not +precede the architecture gate. + +## Proof + +| Check | Result | +|-------|--------| +| PR CI | 25/25 check runs successful at `cb19e84` | +| Suite 1 — unit | 288 passed | +| Suite 2 — integration | 151 passed, coverage 91.58% (gate 90%) | +| Research suite | 87 passed | +| Shell harnesses | 24/25 passed; `test-guide-stack-live.sh` skipped (needs a live Guide + Neo4j stack, outside this PR's CI) | +| Live-consumer shell matrix | 113 passed, 0 failed, 1 skipped | +| Installed-target smoke | 45/45 install checks; claim/pointer/next/status/gate/capture/accept exercised against a fresh target | +| ShellCheck | `bash -n` clean; `-S error` clean over `scripts`, `templates/agent-context`, `tests`, `.cursor` | +| Complexity gate | PASS against `origin/main` | +| Canvas / requirements / adapter / diagram validators | PASS | +| Shipped LOC removed | 3,100 template LOC (6,200 including dogfood copies); target was 3,000 | + +## Acceptance Criteria + +Every criterion was executed; the transcript is in +`/opt/cursor/artifacts/ref003-acceptance.log`. + +- Retired twins absent from `templates/agent-context/` and `sdlc-spdd/scripts/`. +- `SDLC_ENGINE=shell` exits 2 with the Python-only message. +- `SDLC_GATE_ENGINE=shell` exits 2 with the same single-engine contract. +- A missing interpreter (`PYTHON=/nonexistent/python`) and a wrong-version + interpreter (3.11 stub) both exit 1 with the setup hint and no bash fallback. +- No touched utility invokes bare `python3` for engine behavior; all engine + calls resolve `SDLC_PY`. +- `cmd_shell` resolves helpers in orchestrator `scripts/` and installed + `sdlc-spdd/scripts/`, proven by unit tests and a live installed-target call. +- Fresh install omits the twins, upgrade backs up then deletes pre-existing + copies, and install verification asserts their absence. +- Current README, testing, engine, template, and research docs describe one + mandatory Python engine. + +## Safeguards + +Each canvas safeguard was checked against the diff or a run. + +| Safeguard | Verdict | +|-----------|---------| +| Exact-path deletion only under framework-owned template/install locations | Holds. Upgrade deletes the three named files under `/sdlc-spdd/scripts/` only; no globs or directory sweeps. | +| Upgrade never deletes unrelated user scripts | Holds. A hand-placed `sdlc-workflow.sh` was backed up into `.sdlc-spdd-upgrade-backups/` before removal, and no other file in `scripts/` was touched. | +| Missing Python fails loudly with setup guidance | Holds, and improved. A missing interpreter and a 3.11 interpreter both exit non-zero naming the interpreter and `./scripts/setup-engine-venv.sh`. The truncated `is 3.` message that hid this failure in CI is fixed. | +| Shell-engine requests fail; they never silently switch behavior | Holds. `SDLC_ENGINE=shell` and `SDLC_GATE_ENGINE=shell` exit 2 in both the orchestrator checkout and a fresh installed target. | +| Retained utility output and persisted records receive focused regression coverage before twin deletion | Holds. The verify-receipt, gate-review, gate-advisory, and canvas-readiness harnesses plus `engine/tests_unit/` cover capture staging, receipt refusal, and gate rows. | +| Installed-target smoke proves source-checkout assumptions did not leak | Holds. A fresh target passed 45/45 install checks and ran the full lifecycle, including `sdlc-engine shell` resolving a helper under `sdlc-spdd/scripts/`. | +| Full current harness inventory is measured and run, not inferred from stale requirement prose | Holds. The inventory was counted (25) and run (24 pass, 1 live-stack skip); the stale 30 and 28 are reconciled in the requirement and canvas. | +| Never contribute Guide work upstream to `embabel/guide` | Holds. No Guide or upstream change is in this diff. | + +## Findings + +1. **Defect found and fixed during review — complexity gate.** Extending + `cmd_shell` with installed-target lookup raised its CCN from 4 to 6, and the + gate fails on any rise in a touched function. Candidate enumeration and + lookup moved into `_shell_script_candidates` and `_resolve_shell_script`. +2. **Defect found and fixed during review — harness Python resolution.** Three + migrated harnesses passed `PYTHON="${REPO_ROOT}/.venv/bin/python"` to the + dispatcher, but CI installs the engine into the job interpreter and never + creates a repo venv, so `test-sdlc-workflow` failed. Resolution now goes + through one shared `tests/lib/engine-python.sh` helper, which + `test-integration-merge.sh` and `tests/live-consumer/lib.sh` also use + instead of their own copies of the same fallback chain. +3. **Defect found and fixed during review — misleading version error.** + `resolve_engine_python` parsed the interpreter version with + `read -r major minor <<<"$(...)"`. When the interpreter could not run, the + here-string still supplied one empty line, so `read` returned 0 and the + guard printed `is 3.` with an empty minor. It now detects the empty version + string and reports an unrunnable interpreter, and prints the full + `major.minor` when the version is simply wrong. Covered in + `tests/test-scripts-lib.sh`. +4. **Scope addition, reconciled.** T05 extended past the planned doc set to the + research docs (`engine-sut.md`, `evaluation-protocol.md`, + `threats-to-validity-and-replication.md`). Their "Defaults" and + replication-freeze tables still described `SDLC_ENGINE=auto` and a + `SDLC_GATE_ENGINE=shell` fallback as live knobs, which is no longer true. + `tests/research/test_ref001_sut.py` pinned the `sdlc_engine=auto` prose, so + it now asserts the rejection contract instead. +5. **Scope addition, reconciled.** AC5 was not met when review began: + `start-agent-session.sh` and `capture-session-memory.sh` still shaped + engine records through bare `python3` heredocs. Both already resolve + `SDLC_PY` earlier and exit on failure, so the three call sites now use it. +6. **Inventory reconciled.** The requirement's pre-REF-002 count of 30 shell + harnesses and the canvas's 28 are both stale; retiring the three twin + harnesses to pytest leaves 25. The canvas records all three numbers. + +## Required Changes + +None. Findings 1–3 are fixed in this PR; 4–6 are recorded in the canvas and +requirement. + +## Not Covered + +- `test-guide-stack-live.sh` needs a live Guide + Neo4j stack and was not run. + It does not exercise the dispatcher contract this Work ID changed. +- Manual chat smoke in Cursor/Copilot/Claude Code was not performed; the + command adapters changed only in generated prose (removing "even when + `SDLC_ENGINE=shell`"), and adapter parity plus spec generation are green. diff --git a/sdlc-spdd/spdd/sync/REF-003-retire-bash-workflow-dual-path-sync.md b/sdlc-spdd/spdd/sync/REF-003-retire-bash-workflow-dual-path-sync.md new file mode 100644 index 00000000..33f166d5 --- /dev/null +++ b/sdlc-spdd/spdd/sync/REF-003-retire-bash-workflow-dual-path-sync.md @@ -0,0 +1,99 @@ +# Sync: REF-003-retire-bash-workflow-dual-path + +**Work ID:** REF-003-retire-bash-workflow-dual-path +**Date:** 2026-09-14 +**Readiness After Sync:** Complete +**Status After Sync:** Complete +**Pull Request:** https://github.com/jmjava/sdlc-spdd-orchestrator/pull/321 + +## What Changed + +- The bash workflow twin — `sdlc-workflow.sh`, `sdlc-team-registry.sh`, and + `sdlc-pointer.sh` — is deleted from `templates/agent-context/` and + `sdlc-spdd/scripts/` (3,100 shipped LOC; 6,200 with dogfood copies). +- `scripts/sdlc.sh` and its dogfood copy are thin dispatchers that require + Python 3.12 and an importable `sdlc_engine`. `SDLC_ENGINE=shell` and + `SDLC_GATE_ENGINE=shell` exit non-zero; `SDLC_ENGINE=python` is a no-op. +- `start-agent-session.sh`, `capture-session-memory.sh`, and + `accept-lessons.sh` call the engine through `SDLC_PY` and no longer source a + twin or shell out to bare `python3` for engine behavior. +- `sdlc-engine shell` resolves retained utilities in both orchestrator + `scripts/` and installed `sdlc-spdd/scripts/` layouts. +- Init and upgrade stop shipping the twins, upgrade backs up and deletes the + exact retired paths, and install verification asserts their absence. +- `test-sdlc-workflow.sh`, `test-sdlc-pointer.sh`, and `test-archive-work.sh` + are replaced by `engine/tests_unit/` coverage; their CI workflows are gone. +- Operator, testing, engine, template, and research docs describe one + mandatory Python engine. + +## What Drifted + +- Extending `cmd_shell` for installed targets raised its CCN from 4 to 6 and + failed the diff complexity gate, which rejects any rise in a touched + function. +- Three migrated harnesses pinned `${REPO_ROOT}/.venv/bin/python`. CI installs + the engine into the job interpreter and never creates a repo venv, so + `test-sdlc-workflow` failed on a path that did not exist. +- `resolve_engine_python` reported that failure as `is 3.` because its + here-string parse could not distinguish an unrunnable interpreter from an + empty version string. +- T05 was scoped to operator docs, but the research docs and + `tests/research/test_ref001_sut.py` still pinned `SDLC_ENGINE=auto` and a + live `SDLC_GATE_ENGINE=shell` fallback. +- AC5 was unmet at review time: two retained utilities still shaped engine + records with bare `python3` heredocs. +- The requirement's harness count of 30 predates REF-002 and the canvas's 28 + predates this Work ID's deletions. + +## What Was Reconciled + +- `_shell_script_candidates` and `_resolve_shell_script` carry the bridge + logic, returning `cmd_shell` below its base complexity. +- `tests/lib/engine-python.sh` is the single harness interpreter resolver; + `test-integration-merge.sh` and `tests/live-consumer/lib.sh` use it instead + of their own copies of the same fallback chain. +- `resolve_engine_python` reports an unrunnable interpreter distinctly and + prints the full `major.minor` for a wrong version, with coverage in + `tests/test-scripts-lib.sh`. +- Research docs and the SUT test state the rejection contract. +- The three remaining `python3` heredocs use the already-resolved `SDLC_PY`. +- The canvas records all three harness counts (30 → 28 → 25) and the + requirement explains the reconciliation. +- Requirement, canvas, Milestone 3, and `ROADMAP.md` mark REF-003 Complete. + +## Review and Validation + +- Review result: **Approved With Notes**. +- PR CI: **25/25 successful checks** at `cb19e84`. +- Suite 1 unit 288 passed; Suite 2 integration 151 passed at 91.58% coverage; + research 87 passed. +- Shell harnesses 24/25 passed; `test-guide-stack-live.sh` skipped (live Guide + + Neo4j stack). +- Live-consumer shell matrix: 113 passed, 0 failed, 1 skipped. +- Installed-target smoke: 45/45 install checks, plus claim, pointer, next, + status, gate, capture refusal, capture with receipt, accept, and the shell + bridge. +- ShellCheck `-S error` and `bash -n` clean; complexity gate PASS; canvas, + requirements, adapter, and diagram validators PASS. + +## Dependencies + +- Depends on REF-002-purge-pre-v3-compat (Complete). +- Unblocks REF-010-pythonize-session-and-capture: the retained session and + capture utilities are now thin Python-engine clients, which is the starting + point REF-010 assumed. +- REF-004, REF-005, and REF-006 no longer need shell parity. + +## Accepted Lessons + +- `session:REF-003-retire-bash-workflow-dual-path:tests/test-:capture` +- `decision:REF-003-retire-bash-workflow-dual-path:tests/test-:capture` +- `pitfall:REF-003-retire-bash-workflow-dual-path:tests/test-:capture` +- `pattern:REF-003-retire-bash-workflow-dual-path:tests/test-:capture` + +## Evidence + +- `/opt/cursor/artifacts/ref003-acceptance.log` +- `/opt/cursor/artifacts/ref003-full-validation.log` +- `/opt/cursor/artifacts/ref003-installed-target-smoke.log` +- `/opt/cursor/artifacts/live-consumer-matrix.log`