From c7f3ffb0e8cacb816777ff70f77de8a78a486255 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 13 Sep 2026 23:26:23 +0000 Subject: [PATCH 01/22] =?UTF-8?q?Open=20Milestones=203=E2=80=935=20from=20?= =?UTF-8?q?the=20SPIKE-005=20architectural=20review.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One engine (Python sdlc-engine), one persistence model (storage v3), aligned code/tests/docs. Adds the measured-baseline analysis, Milestone 3 (one flow), Milestone 4 (docs truth), Milestone 5 (make-it-fast outline), 24 requirement stubs, task lists including the Obsidian monthly goals, ROADMAP pointers, and marks Milestone 2 complete. Registry: claim SPIKE-005. Co-authored-by: John Menke --- sdlc-spdd/ROADMAP.md | 45 +++- .../milestones/milestone-2/_milestone.yml | 4 +- .../BUG-001-db-query-undefined-names.md | 99 ++++++++ ...HORE-004-lint-and-complexity-gates-real.md | 100 ++++++++ .../CHORE-005-ci-reusable-workflows.md | 97 ++++++++ ...ORE-006-dogfood-adapters-and-quick-spec.md | 98 ++++++++ .../milestones/milestone-3/MILESTONE-3.md | 90 +++++++ .../REF-002-purge-pre-v3-compat.md | 104 ++++++++ .../REF-003-retire-bash-workflow-dual-path.md | 104 ++++++++ .../REF-004-split-installer-blueprints.md | 99 ++++++++ .../REF-005-cli-command-modules.md | 99 ++++++++ ...-006-storage-records-and-atomic-appends.md | 98 ++++++++ .../REF-007-single-guide-transport.md | 98 ++++++++ .../REF-008-console-viewer-hardening.md | 99 ++++++++ .../REF-009-adf-codec-and-viewer-assets.md | 95 ++++++++ .../REF-010-pythonize-session-and-capture.md | 96 ++++++++ .../REF-011-split-issue-sync-adapters.md | 94 +++++++ .../SPIKE-005-architecture-review.md | 109 +++++++++ ...ST-004-hermetic-unit-suite-and-fixtures.md | 100 ++++++++ .../milestones/milestone-3/_milestone.yml | 7 + .../milestone-4/CHORE-007-doc-links-gate.md | 97 ++++++++ .../CHORE-008-single-docs-authoring-root.md | 96 ++++++++ .../CHORE-009-release-version-hygiene.md | 96 ++++++++ .../CHORE-010-registry-reconciliation.md | 97 ++++++++ .../DOC-004-install-path-vocabulary.md | 98 ++++++++ .../DOC-005-grounding-path-map-codegen.md | 97 ++++++++ ...n-decisions-v3-and-starter-spec-archive.md | 95 ++++++++ .../DOC-007-glossary-and-engine-cli-guide.md | 94 +++++++ .../milestones/milestone-4/MILESTONE-4.md | 66 +++++ .../milestones/milestone-4/_milestone.yml | 7 + .../milestones/milestone-5/MILESTONE-5.md | 23 ++ .../milestones/milestone-5/_milestone.yml | 7 + sdlc-spdd/session-notes/2026-09-13.md | 28 +++ .../SPIKE-005-architecture-review-analysis.md | 230 ++++++++++++++++++ sdlc-spdd/spdd/memory/registry.jsonl | 14 ++ sdlc-spdd/spdd/tasks/2026-10-monthly-goals.md | 124 ++++++++++ sdlc-spdd/spdd/tasks/milestone-3-one-flow.md | 66 +++++ 37 files changed, 3065 insertions(+), 5 deletions(-) create mode 100644 sdlc-spdd/requirements/milestones/milestone-3/BUG-001-db-query-undefined-names.md create mode 100644 sdlc-spdd/requirements/milestones/milestone-3/CHORE-004-lint-and-complexity-gates-real.md create mode 100644 sdlc-spdd/requirements/milestones/milestone-3/CHORE-005-ci-reusable-workflows.md create mode 100644 sdlc-spdd/requirements/milestones/milestone-3/CHORE-006-dogfood-adapters-and-quick-spec.md create mode 100644 sdlc-spdd/requirements/milestones/milestone-3/MILESTONE-3.md create mode 100644 sdlc-spdd/requirements/milestones/milestone-3/REF-002-purge-pre-v3-compat.md create mode 100644 sdlc-spdd/requirements/milestones/milestone-3/REF-003-retire-bash-workflow-dual-path.md create mode 100644 sdlc-spdd/requirements/milestones/milestone-3/REF-004-split-installer-blueprints.md create mode 100644 sdlc-spdd/requirements/milestones/milestone-3/REF-005-cli-command-modules.md create mode 100644 sdlc-spdd/requirements/milestones/milestone-3/REF-006-storage-records-and-atomic-appends.md create mode 100644 sdlc-spdd/requirements/milestones/milestone-3/REF-007-single-guide-transport.md create mode 100644 sdlc-spdd/requirements/milestones/milestone-3/REF-008-console-viewer-hardening.md create mode 100644 sdlc-spdd/requirements/milestones/milestone-3/REF-009-adf-codec-and-viewer-assets.md create mode 100644 sdlc-spdd/requirements/milestones/milestone-3/REF-010-pythonize-session-and-capture.md create mode 100644 sdlc-spdd/requirements/milestones/milestone-3/REF-011-split-issue-sync-adapters.md create mode 100644 sdlc-spdd/requirements/milestones/milestone-3/SPIKE-005-architecture-review.md create mode 100644 sdlc-spdd/requirements/milestones/milestone-3/TEST-004-hermetic-unit-suite-and-fixtures.md create mode 100644 sdlc-spdd/requirements/milestones/milestone-3/_milestone.yml create mode 100644 sdlc-spdd/requirements/milestones/milestone-4/CHORE-007-doc-links-gate.md create mode 100644 sdlc-spdd/requirements/milestones/milestone-4/CHORE-008-single-docs-authoring-root.md create mode 100644 sdlc-spdd/requirements/milestones/milestone-4/CHORE-009-release-version-hygiene.md create mode 100644 sdlc-spdd/requirements/milestones/milestone-4/CHORE-010-registry-reconciliation.md create mode 100644 sdlc-spdd/requirements/milestones/milestone-4/DOC-004-install-path-vocabulary.md create mode 100644 sdlc-spdd/requirements/milestones/milestone-4/DOC-005-grounding-path-map-codegen.md create mode 100644 sdlc-spdd/requirements/milestones/milestone-4/DOC-006-design-decisions-v3-and-starter-spec-archive.md create mode 100644 sdlc-spdd/requirements/milestones/milestone-4/DOC-007-glossary-and-engine-cli-guide.md create mode 100644 sdlc-spdd/requirements/milestones/milestone-4/MILESTONE-4.md create mode 100644 sdlc-spdd/requirements/milestones/milestone-4/_milestone.yml create mode 100644 sdlc-spdd/requirements/milestones/milestone-5/MILESTONE-5.md create mode 100644 sdlc-spdd/requirements/milestones/milestone-5/_milestone.yml create mode 100644 sdlc-spdd/session-notes/2026-09-13.md create mode 100644 sdlc-spdd/spdd/analysis/SPIKE-005-architecture-review-analysis.md create mode 100644 sdlc-spdd/spdd/tasks/2026-10-monthly-goals.md create mode 100644 sdlc-spdd/spdd/tasks/milestone-3-one-flow.md diff --git a/sdlc-spdd/ROADMAP.md b/sdlc-spdd/ROADMAP.md index 9ca53d4f..aacc714a 100644 --- a/sdlc-spdd/ROADMAP.md +++ b/sdlc-spdd/ROADMAP.md @@ -17,7 +17,7 @@ whole framework through the stages in order. | Stage | State | Focus | |-------|-------|-------| | **Make it work** | mostly done | MVP delivered — three assistant adapters, capture, indexes, session briefs, and validation CI all function end to end. | -| **Make it right** | **complete** | Refactors landed (FEAT-001→003, FEAT-009→012) and archived. Active: **storage v3** on integration branch. | +| **Make it right** | **active (Milestones 3–4)** | Milestone 1 refactors and Milestone 2 research hardening landed. Now: **one engine, one persistence model** (Milestone 3) and **docs truth** (Milestone 4). | | **Make it fast** | **spikes (shelved)** | Measurement landed (FEAT-004/005). SPIKE-001/002 archived pending Guide MCP; act on metrics when resumed. | Planning guidance: @@ -73,7 +73,7 @@ Milestone 1 requirement stubs and canvases were removed from the working tree; use git history. Storage v3 (`docs/storage-v3.md`), `harness/skills`, `.sdlc/` runtime, and test-suite restructure remain the engineering baseline. -## Milestone 2 — Academic contribution bar (planned) +## Milestone 2 — Academic contribution bar (complete) See [requirements/milestones/milestone-2/MILESTONE-2.md](requirements/milestones/milestone-2/MILESTONE-2.md) and the [iteration task list](spdd/tasks/milestone-2-academic-hardening.md). @@ -90,6 +90,36 @@ seeded); REF-001 Complete (Python `gate_check` is the named SUT); TEST-003 Complete (T01–T05: three storage modes, context-select, unreachable Guide fails `context parity`; mocked Guide in default CI). Journal n≥3 remains TEST-001's stop rule, not a new SPIKE. +## Milestone 3 — One flow on storage v3 (active) + +See [requirements/milestones/milestone-3/MILESTONE-3.md](requirements/milestones/milestone-3/MILESTONE-3.md) +and the [task list](spdd/tasks/milestone-3-one-flow.md). Opened by the +[SPIKE-005 architectural review](spdd/analysis/SPIKE-005-architecture-review-analysis.md). + +**Stage:** make it right. Owner decision (2026-09-13): **no competing versions**. +Python `sdlc-engine` is the only engine; storage v3 is the only persistence model; +pre-v3 compatibility (legacy `agent-context/` layouts, `work-registry.tsv`, feature +mirrors, `spdd/*/archive/`, migration shims) and the bash workflow twin are removed, +not maintained. Losing pre-v3 context is accepted. + +P0: BUG-001, CHORE-004, CHORE-006, REF-002 (purge pre-v3), REF-003 (retire bash twin). +P1: REF-004→008, CHORE-005, TEST-004. P2: REF-009→011. + +## Milestone 4 — Documentation truth and release hygiene (active, parallel) + +See [requirements/milestones/milestone-4/MILESTONE-4.md](requirements/milestones/milestone-4/MILESTONE-4.md). +Docs describe the layout `init` creates and the one flow of Milestone 3; generated +grounding; zero broken links in CI; one authoring root; current ADRs; version and +registry hygiene. P0: DOC-004, DOC-005, CHORE-007. P1: CHORE-008, DOC-006, CHORE-009, +CHORE-010. P2: DOC-007. + +## Milestone 5 — Make it fast (outline) + +See [requirements/milestones/milestone-5/MILESTONE-5.md](requirements/milestones/milestone-5/MILESTONE-5.md). +Not opened until Milestone 3 P0/P1 and Milestone 4 P0 are complete. + +Month-scoped view of all three: [spdd/tasks/2026-10-monthly-goals.md](spdd/tasks/2026-10-monthly-goals.md). + ## Post-MVP backlog Each item names the Beck stage it serves (see [Delivery posture](#delivery-posture-kent-beck-make-it-work--make-it-right--make-it-fast)). Near-term make-it-right refactors and make-it-fast measurement (FEAT-004/005) are largely complete; remaining make-it-fast work is spikes and acting on metrics. @@ -188,13 +218,22 @@ Milestone 1 feature track is Complete on the integration branch. | CHORE-003-dogfood-ledger | make it right | Complete (P2) | | REF-001-engine-single-source | make it right | Complete (P2) | | TEST-003-cretrieve-roundtrip | make it right | Complete (T01–T05) | +| SPIKE-005-architecture-review | make it right (program) | In Progress — opened Milestones 3–5 | +| BUG-001-db-query-undefined-names | make it right | Milestone 3 P0 | +| CHORE-004-lint-and-complexity-gates-real | make it right | Milestone 3 P0 | +| CHORE-006-dogfood-adapters-and-quick-spec | make it right | Milestone 3 P0 | +| REF-002-purge-pre-v3-compat | make it right | Milestone 3 P0 | +| REF-003-retire-bash-workflow-dual-path | make it right | Milestone 3 P0 | +| 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 | Deferred / residual (not Work IDs yet): | Item | Notes | |------|-------| -| Readability pass | Consistent structure/naming across code/docs (milestone residual) | +| Readability pass | Absorbed into Milestone 3 (REF-004/005) and Milestone 4 (CHORE-008) | | Dual milestone root stub | Root `milestone-1.md` + subdirectory both exist; prefer subdir | +| `spdd --metrics` | Superseded by `sdlc-engine context metrics` (FEAT-015); row above is historical | `spdd --metrics` is no longer a nameless residual — it is **FEAT-015-first-class-metrics** under Milestone 2. diff --git a/sdlc-spdd/requirements/milestones/milestone-2/_milestone.yml b/sdlc-spdd/requirements/milestones/milestone-2/_milestone.yml index cf64e8dc..b1f122a2 100644 --- a/sdlc-spdd/requirements/milestones/milestone-2/_milestone.yml +++ b/sdlc-spdd/requirements/milestones/milestone-2/_milestone.yml @@ -1,7 +1,7 @@ name: "Milestone 2 — Academic contribution bar" number: 2 start_date: "2026-09-06" -end_date: "" -status: "planned" +end_date: "2026-09-07" +status: "complete" related_epics: [] notes: "Raise sdlc-spdd-orchestrator from hobby/engineering research to a defensible empirical SE / AI-assisted-process contribution. Driven by SPIKE-004 journal-style review. Does not replace product work (ADF/Vue3)." diff --git a/sdlc-spdd/requirements/milestones/milestone-3/BUG-001-db-query-undefined-names.md b/sdlc-spdd/requirements/milestones/milestone-3/BUG-001-db-query-undefined-names.md new file mode 100644 index 00000000..ca328da8 --- /dev/null +++ b/sdlc-spdd/requirements/milestones/milestone-3/BUG-001-db-query-undefined-names.md @@ -0,0 +1,99 @@ +--- +work_id: "BUG-001-db-query-undefined-names" +jira_key: "" +github_number: "" +jira_epic: "" +jira_type: "Bug" +jira_status: "To Do" +jira_assignee: "" +jira_due_date: "" +jira_sprint: "" +milestone: "milestone-3" +priority: "P0" +size: "S" +blocks: [] +depends_on: + - "SPIKE-005-architecture-review" +related: + - "CHORE-004-lint-and-complexity-gates-real" +--- + +# BUG-001: Fix undefined names in db_query.py (export_sql NameError) + +**Work ID:** BUG-001-db-query-undefined-names +**Milestone:** Milestone 3 — One flow on storage v3 +**Status:** To Do +**Priority / size:** P0 / S +**Date:** 2026-09-13 +**Beck stage:** make it right (one engine, one persistence model, aligned docs/tests) + +## Related Work + +| Relationship | Work ID | Status | Notes | +|--------------|---------|--------|-------| +| Blocks | (none) | — | — | +| Depends On | `SPIKE-005-architecture-review` | In Progress | See milestone-3 | +| Related | `CHORE-004-lint-and-complexity-gates-real` | To Do | See milestone-3 | + +## User / Business Goal + +`sdlc-engine db export` crashes with NameError because db_query.py references NODE_*/REL_* constants and _utc_now without importing them; CI never ran pyflakes' undefined-name check. + +## Scope + +### IN SCOPE + +- Import the db_schema constants and timeutil.utc_now in db_query.py +- Regression tests: export_sql round-trip and context_linked_to_section on a seeded index +- Decide whether context_linked_to_section (no callers) stays; if kept, it must be tested + +### NOT IN SCOPE + +- Widening the lint gate (CHORE-004) +- SQLite schema changes + +## Acceptance Criteria + +- [ ] `ruff check --select F821 engine/src` is clean +- [ ] Unit test calls LocalIndex.export_sql on a temp project and asserts the dump header +- [ ] Unit test exercises context_linked_to_section or the method is removed + +## Non-Goals + +- Expanding product surface (ADF, Vue3, Jira UX) under this Work ID +- Upstream PRs to embabel/guide + +## Jira + +Create the issue manually in Jira UI, then set **Key** (and matching `jira_key` frontmatter) and commit. + +- Key: TBD +- Issue type: Bug +- Summary: Fix undefined names in db_query.py (export_sql NameError) +- Labels: sdlc-spdd, milestone-3, one-flow +- Components: framework + +### Description + +`sdlc-engine db export` crashes with NameError because db_query.py references NODE_*/REL_* constants and _utc_now without importing them; CI never ran pyflakes' undefined-name check. + +### Acceptance criteria (Given/When/Then) + +- Given One flow on storage v3 is the active program, When this Work ID is complete, Then the acceptance criteria above are checked and the analysis/canvas (if any) match implementation. + +## GitHub + +Create the issue manually in GitHub UI, then set **Number** (and matching `github_number` frontmatter when used) and commit. + +- Number: TBD +- Title: Fix undefined names in db_query.py (export_sql NameError) +- Labels: sdlc-spdd, milestone-3, one-flow +- URL: + +### Description + +See this requirement and `spdd/analysis/SPIKE-005-architecture-review-analysis.md`. + +## Next Step + + /sdlc-spdd-analysis @sdlc-spdd/requirements/milestones/milestone-3/BUG-001-db-query-undefined-names.md diff --git a/sdlc-spdd/requirements/milestones/milestone-3/CHORE-004-lint-and-complexity-gates-real.md b/sdlc-spdd/requirements/milestones/milestone-3/CHORE-004-lint-and-complexity-gates-real.md new file mode 100644 index 00000000..c51892ad --- /dev/null +++ b/sdlc-spdd/requirements/milestones/milestone-3/CHORE-004-lint-and-complexity-gates-real.md @@ -0,0 +1,100 @@ +--- +work_id: "CHORE-004-lint-and-complexity-gates-real" +jira_key: "" +github_number: "" +jira_epic: "" +jira_type: "Chore" +jira_status: "To Do" +jira_assignee: "" +jira_due_date: "" +jira_sprint: "" +milestone: "milestone-3" +priority: "P0" +size: "M" +blocks: [] +depends_on: + - "SPIKE-005-architecture-review" +related: + - "BUG-001-db-query-undefined-names" +--- + +# CHORE-004: Make lint, complexity, and shellcheck gates run on the real diff + +**Work ID:** CHORE-004-lint-and-complexity-gates-real +**Milestone:** Milestone 3 — One flow on storage v3 +**Status:** To Do +**Priority / size:** P0 / M +**Date:** 2026-09-13 +**Beck stage:** make it right (one engine, one persistence model, aligned docs/tests) + +## Related Work + +| Relationship | Work ID | Status | Notes | +|--------------|---------|--------|-------| +| Blocks | (none) | — | — | +| Depends On | `SPIKE-005-architecture-review` | In Progress | See milestone-3 | +| Related | `BUG-001-db-query-undefined-names` | To Do | See milestone-3 | + +## User / Business Goal + +CI only checks F401/F811 and only proves the complexity checker on a synthetic repo; shellcheck never runs. Gates must fail on real regressions. + +## Scope + +### IN SCOPE + +- pyproject ruff select = full pyflakes (F) plus E9; fix fallout (F821, F541) +- Run scripts/check-complexity.py --base origin/main on PR diffs in test-sdlc-engine.yml (fetch-depth 0) +- Shellcheck workflow over scripts/, scripts/lib/, templates/agent-context/, tests/*.sh; fix current errors (SC2066, SC1087, SC2218, SC2144) +- Update test_quality_gates.py to assert the new rule set + +### NOT IN SCOPE + +- Fixing all 190 broad-rule findings (C901 etc.) — those fall to the REF items +- Adopting a formatter + +## Acceptance Criteria + +- [ ] CI ruff step uses the pyproject rule set; tree is clean +- [ ] A PR adding a CCN 11 function fails the complexity step (proved once, recorded in TESTING.md) +- [ ] shellcheck -S error passes in CI on all shell paths + +## Non-Goals + +- Expanding product surface (ADF, Vue3, Jira UX) under this Work ID +- Upstream PRs to embabel/guide + +## Jira + +Create the issue manually in Jira UI, then set **Key** (and matching `jira_key` frontmatter) and commit. + +- Key: TBD +- Issue type: Chore +- Summary: Make lint, complexity, and shellcheck gates run on the real diff +- Labels: sdlc-spdd, milestone-3, one-flow +- Components: framework + +### Description + +CI only checks F401/F811 and only proves the complexity checker on a synthetic repo; shellcheck never runs. Gates must fail on real regressions. + +### Acceptance criteria (Given/When/Then) + +- Given One flow on storage v3 is the active program, When this Work ID is complete, Then the acceptance criteria above are checked and the analysis/canvas (if any) match implementation. + +## GitHub + +Create the issue manually in GitHub UI, then set **Number** (and matching `github_number` frontmatter when used) and commit. + +- Number: TBD +- Title: Make lint, complexity, and shellcheck gates run on the real diff +- Labels: sdlc-spdd, milestone-3, one-flow +- URL: + +### Description + +See this requirement and `spdd/analysis/SPIKE-005-architecture-review-analysis.md`. + +## Next Step + + /sdlc-spdd-analysis @sdlc-spdd/requirements/milestones/milestone-3/CHORE-004-lint-and-complexity-gates-real.md diff --git a/sdlc-spdd/requirements/milestones/milestone-3/CHORE-005-ci-reusable-workflows.md b/sdlc-spdd/requirements/milestones/milestone-3/CHORE-005-ci-reusable-workflows.md new file mode 100644 index 00000000..d1623470 --- /dev/null +++ b/sdlc-spdd/requirements/milestones/milestone-3/CHORE-005-ci-reusable-workflows.md @@ -0,0 +1,97 @@ +--- +work_id: "CHORE-005-ci-reusable-workflows" +jira_key: "" +github_number: "" +jira_epic: "" +jira_type: "Chore" +jira_status: "To Do" +jira_assignee: "" +jira_due_date: "" +jira_sprint: "" +milestone: "milestone-3" +priority: "P1" +size: "M" +blocks: [] +depends_on: + - "CHORE-004-lint-and-complexity-gates-real" +related: + - "TEST-004-hermetic-unit-suite-and-fixtures" +--- + +# CHORE-005: Consolidate 26 workflows into ~9 with a reusable setup workflow + +**Work ID:** CHORE-005-ci-reusable-workflows +**Milestone:** Milestone 3 — One flow on storage v3 +**Status:** To Do +**Priority / size:** P1 / M +**Date:** 2026-09-13 +**Beck stage:** make it right (one engine, one persistence model, aligned docs/tests) + +## Related Work + +| Relationship | Work ID | Status | Notes | +|--------------|---------|--------|-------| +| Blocks | (none) | — | — | +| Depends On | `CHORE-004-lint-and-complexity-gates-real` | To Do | See milestone-3 | +| Related | `TEST-004-hermetic-unit-suite-and-fixtures` | To Do | See milestone-3 | + +## User / Business Goal + +Setup steps are copy-pasted ~30 times; validators overlap; installer coverage runs twice; Playwright push paths are narrower than PR paths. + +## Scope + +### IN SCOPE + +- workflow_call setup (checkout, python 3.12, engine install, optional node/playwright) +- ci-engine, ci-shell-harness, ci-adapters, ci-docs-boundary, ci-research-p0, ci-guide-experimental, ci-live-consumer, pages +- Align e2e push/PR path filters; drop duplicate installer cov job; merge validate-command-adapters into spec-generation + +### NOT IN SCOPE + +- Changing what is tested + +## Acceptance Criteria + +- [ ] ≤ 10 workflow files; one reusable setup +- [ ] Every job that ran before still runs (mapping table in TESTING.md) + +## Non-Goals + +- Expanding product surface (ADF, Vue3, Jira UX) under this Work ID +- Upstream PRs to embabel/guide + +## Jira + +Create the issue manually in Jira UI, then set **Key** (and matching `jira_key` frontmatter) and commit. + +- Key: TBD +- Issue type: Chore +- Summary: Consolidate 26 workflows into ~9 with a reusable setup workflow +- Labels: sdlc-spdd, milestone-3, one-flow +- Components: framework + +### Description + +Setup steps are copy-pasted ~30 times; validators overlap; installer coverage runs twice; Playwright push paths are narrower than PR paths. + +### Acceptance criteria (Given/When/Then) + +- Given One flow on storage v3 is the active program, When this Work ID is complete, Then the acceptance criteria above are checked and the analysis/canvas (if any) match implementation. + +## GitHub + +Create the issue manually in GitHub UI, then set **Number** (and matching `github_number` frontmatter when used) and commit. + +- Number: TBD +- Title: Consolidate 26 workflows into ~9 with a reusable setup workflow +- Labels: sdlc-spdd, milestone-3, one-flow +- URL: + +### Description + +See this requirement and `spdd/analysis/SPIKE-005-architecture-review-analysis.md`. + +## Next Step + + /sdlc-spdd-analysis @sdlc-spdd/requirements/milestones/milestone-3/CHORE-005-ci-reusable-workflows.md diff --git a/sdlc-spdd/requirements/milestones/milestone-3/CHORE-006-dogfood-adapters-and-quick-spec.md b/sdlc-spdd/requirements/milestones/milestone-3/CHORE-006-dogfood-adapters-and-quick-spec.md new file mode 100644 index 00000000..6c382177 --- /dev/null +++ b/sdlc-spdd/requirements/milestones/milestone-3/CHORE-006-dogfood-adapters-and-quick-spec.md @@ -0,0 +1,98 @@ +--- +work_id: "CHORE-006-dogfood-adapters-and-quick-spec" +jira_key: "" +github_number: "" +jira_epic: "" +jira_type: "Chore" +jira_status: "To Do" +jira_assignee: "" +jira_due_date: "" +jira_sprint: "" +milestone: "milestone-3" +priority: "P0" +size: "S" +blocks: [] +depends_on: + - "SPIKE-005-architecture-review" +related: + - "DOC-005-grounding-path-map-codegen" +--- + +# CHORE-006: Regenerate dogfood adapters and give /sdlc-spdd-quick a spec + +**Work ID:** CHORE-006-dogfood-adapters-and-quick-spec +**Milestone:** Milestone 3 — One flow on storage v3 +**Status:** To Do +**Priority / size:** P0 / S +**Date:** 2026-09-13 +**Beck stage:** make it right (one engine, one persistence model, aligned docs/tests) + +## Related Work + +| Relationship | Work ID | Status | Notes | +|--------------|---------|--------|-------| +| Blocks | (none) | — | — | +| Depends On | `SPIKE-005-architecture-review` | In Progress | See milestone-3 | +| Related | `DOC-005-grounding-path-map-codegen` | To Do | See milestone-4 | + +## User / Business Goal + +This repo's installed command packs (.cursor/commands, .claude/commands, .github/prompts) lag templates — Kasana I1 steps and DIF steps are missing — and /sdlc-spdd-quick has no spec so the generator is not its source of truth. + +## Scope + +### IN SCOPE + +- Regenerate dogfood packs through the same install path-rewrite used for targets +- Add spec/commands/lifecycle-quick.spec.md and generate all three adapters from it +- Extend validate-sdlc-spdd-adapters to compare dogfood against path-rewritten templates so content lag fails CI + +### NOT IN SCOPE + +- Changing command semantics + +## Acceptance Criteria + +- [ ] Path-rewritten templates diff clean against .cursor/commands, .claude/commands, .github/prompts +- [ ] generate-command-adapters.sh --check covers quick +- [ ] CI fails when a dogfood pack is stale + +## Non-Goals + +- Expanding product surface (ADF, Vue3, Jira UX) under this Work ID +- Upstream PRs to embabel/guide + +## Jira + +Create the issue manually in Jira UI, then set **Key** (and matching `jira_key` frontmatter) and commit. + +- Key: TBD +- Issue type: Chore +- Summary: Regenerate dogfood adapters and give /sdlc-spdd-quick a spec +- Labels: sdlc-spdd, milestone-3, one-flow +- Components: framework + +### Description + +This repo's installed command packs (.cursor/commands, .claude/commands, .github/prompts) lag templates — Kasana I1 steps and DIF steps are missing — and /sdlc-spdd-quick has no spec so the generator is not its source of truth. + +### Acceptance criteria (Given/When/Then) + +- Given One flow on storage v3 is the active program, When this Work ID is complete, Then the acceptance criteria above are checked and the analysis/canvas (if any) match implementation. + +## GitHub + +Create the issue manually in GitHub UI, then set **Number** (and matching `github_number` frontmatter when used) and commit. + +- Number: TBD +- Title: Regenerate dogfood adapters and give /sdlc-spdd-quick a spec +- Labels: sdlc-spdd, milestone-3, one-flow +- URL: + +### Description + +See this requirement and `spdd/analysis/SPIKE-005-architecture-review-analysis.md`. + +## Next Step + + /sdlc-spdd-analysis @sdlc-spdd/requirements/milestones/milestone-3/CHORE-006-dogfood-adapters-and-quick-spec.md diff --git a/sdlc-spdd/requirements/milestones/milestone-3/MILESTONE-3.md b/sdlc-spdd/requirements/milestones/milestone-3/MILESTONE-3.md new file mode 100644 index 00000000..ba8f0b97 --- /dev/null +++ b/sdlc-spdd/requirements/milestones/milestone-3/MILESTONE-3.md @@ -0,0 +1,90 @@ +# Milestone 3 — One flow on storage v3 + +## Goal + +There is exactly one engine (Python `sdlc-engine`), one persistence model (storage v3: committed `spdd/memory/lessons.jsonl` + `registry.jsonl`, gitignored `.sdlc/` runtime, single `sdlc-spdd/` home), and code, tests, and docs that all describe that one flow. Competing implementations (bash workflow twin, `SDLC_ENGINE=shell`), pre-v3 compatibility (legacy `agent-context/` layouts, `work-registry.tsv`, feature mirrors, `spdd/*/archive/` folders, migration shims) are removed, not maintained. Losing pre-v3 context is an accepted cost (owner decision, 2026-09-13). + +**Stage:** make it right. No new capability; the framework does the same job with one implementation, real quality gates, and honest test layers. + +Analysis that opened this milestone: `spdd/analysis/SPIKE-005-architecture-review-analysis.md`. + +## Outcome (definition of done) + +- `SDLC_ENGINE` and `SDLC_GATE_ENGINE` no longer exist; `sdlc.sh` is a thin dispatcher into the Python engine and errors clearly when it is missing. +- `templates/agent-context/` ships no workflow/registry/pointer bash; shipped script LOC drops by ≥ 3 000. +- No code path reads or writes pre-v3 layouts; `archive` deletes (git history is the record); no `spdd/*/archive/` directories. +- Lint (full pyflakes), complexity (on the PR diff), and shellcheck gates run in CI and fail on regressions. +- No function in `engine/src` exceeds CCN 15; `installer/app.py` and `cli_*` are split into units under 80 NLOC. +- Unit suite is hermetic (no Flask, no subprocess to bash); every module has a focused test file; `TESTING.md` numbers are generated from the tree. +- ≤ 10 GitHub workflows with one reusable setup. +- Local web surfaces have a written threat model; `--lan` requires a token. + +## Scope + +P0 — one flow (do first, in order): + +- [x] SPIKE-005-architecture-review — review + this program (canvas pending) +- [ ] BUG-001-db-query-undefined-names — live `NameError` in `db export` +- [ ] CHORE-004-lint-and-complexity-gates-real — gates run on the real diff +- [ ] CHORE-006-dogfood-adapters-and-quick-spec — dogfood packs regenerate from spec +- [ ] REF-002-purge-pre-v3-compat — one data model; archive deletes; no migrations +- [ ] REF-003-retire-bash-workflow-dual-path — one engine; no shell twin + +P1 — make the one flow maintainable: + +- [ ] REF-004-split-installer-blueprints +- [ ] REF-005-cli-command-modules +- [ ] REF-006-storage-records-and-atomic-appends +- [ ] REF-007-single-guide-transport +- [ ] REF-008-console-viewer-hardening +- [ ] CHORE-005-ci-reusable-workflows +- [ ] TEST-004-hermetic-unit-suite-and-fixtures + +P2 — finish the consolidation: + +- [ ] REF-009-adf-codec-and-viewer-assets +- [ ] REF-010-pythonize-session-and-capture +- [ ] REF-011-split-issue-sync-adapters + +## Linked Work + +| Work ID | Pri | Size | Requirement | Status | Notes | +|---------|-----|------|-------------|--------|-------| +| SPIKE-005-architecture-review | P0 | M | [requirement](SPIKE-005-architecture-review.md) | In Progress | Full architectural review and Milestone 3/4/5 program | +| BUG-001-db-query-undefined-names | P0 | S | [requirement](BUG-001-db-query-undefined-names.md) | To Do | Fix undefined names in db_query.py (export_sql NameError) | +| CHORE-004-lint-and-complexity-gates-real | P0 | M | [requirement](CHORE-004-lint-and-complexity-gates-real.md) | To Do | 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) | To Do | Regenerate dogfood adapters; spec for `/sdlc-spdd-quick` | +| REF-002-purge-pre-v3-compat | P0 | L | [requirement](REF-002-purge-pre-v3-compat.md) | To Do | Purge pre-v3 persistence compatibility; one data model | +| 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-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 | +| REF-007-single-guide-transport | P1 | S | [requirement](REF-007-single-guide-transport.md) | To Do | `GuideClient` is the only Guide HTTP path | +| REF-008-console-viewer-hardening | P1 | M | [requirement](REF-008-console-viewer-hardening.md) | To Do | Threat model and hardening for console and viewer | +| CHORE-005-ci-reusable-workflows | P1 | M | [requirement](CHORE-005-ci-reusable-workflows.md) | To Do | 26 workflows → ~9 with a reusable setup | +| TEST-004-hermetic-unit-suite-and-fixtures | P1 | M | [requirement](TEST-004-hermetic-unit-suite-and-fixtures.md) | To Do | Hermetic unit suite; shared installed-target fixture | +| REF-009-adf-codec-and-viewer-assets | P2 | M | [requirement](REF-009-adf-codec-and-viewer-assets.md) | To Do | One ADF codec; viewer UI as assets or Vue tab | +| REF-010-pythonize-session-and-capture | P2 | L | [requirement](REF-010-pythonize-session-and-capture.md) | To Do | Session, capture, resolve, create-work in Python | +| REF-011-split-issue-sync-adapters | P2 | M | [requirement](REF-011-split-issue-sync-adapters.md) | To Do | Split `IssueSyncService` into Jira and GitHub adapters | + +## Iteration order + +1. BUG-001, CHORE-004, CHORE-006 — small, independent, test-first; they make every later PR safer. +2. REF-002 then REF-003 — the purge first (fewer code paths to retire), then the engine twin. Each lands as a sequence of one-operation PRs; the acceptance greps are the stop rule. +3. REF-004, REF-005, REF-006 in parallel once REF-003 is in (they no longer need shell parity). +4. REF-007, REF-008, CHORE-005, TEST-004. +5. P2 items. + +## Stop rules + +- A PR that adds a second implementation of anything already in Python is rejected. +- A PR that keeps a legacy path "for safety" must name the user who needs it; otherwise the path is deleted. +- Docs change in the same PR as the behavior they describe (`TESTING.md`, `docs/storage-v3.md`, `docs/engine-v2.md`). + +## SDLC-SPDD Flow + +For each work item: `/sdlc-spdd-analysis` on its requirement → `/sdlc-spdd-plan` → `/sdlc-spdd-architect` until Ready For Coding → one operation at a time with `/sdlc-spdd-code` → review, capture, accept, retro. + +## Session Updates + +2026-09-13 — Milestone opened from SPIKE-005. Owner decision: one engine, one persistence model; pre-v3 context may be lost. First P0 fixes started in the SPIKE-005 branch. diff --git a/sdlc-spdd/requirements/milestones/milestone-3/REF-002-purge-pre-v3-compat.md b/sdlc-spdd/requirements/milestones/milestone-3/REF-002-purge-pre-v3-compat.md new file mode 100644 index 00000000..f613ac68 --- /dev/null +++ b/sdlc-spdd/requirements/milestones/milestone-3/REF-002-purge-pre-v3-compat.md @@ -0,0 +1,104 @@ +--- +work_id: "REF-002-purge-pre-v3-compat" +jira_key: "" +github_number: "" +jira_epic: "" +jira_type: "Refactor" +jira_status: "To Do" +jira_assignee: "" +jira_due_date: "" +jira_sprint: "" +milestone: "milestone-3" +priority: "P0" +size: "L" +blocks: + - "REF-006-storage-records-and-atomic-appends" +depends_on: + - "SPIKE-005-architecture-review" +related: + - "REF-003-retire-bash-workflow-dual-path" +--- + +# REF-002: Purge pre-v3 persistence compatibility; one data model + +**Work ID:** REF-002-purge-pre-v3-compat +**Milestone:** Milestone 3 — One flow on storage v3 +**Status:** To Do +**Priority / size:** P0 / L +**Date:** 2026-09-13 +**Beck stage:** make it right (one engine, one persistence model, aligned docs/tests) + +## Related Work + +| Relationship | Work ID | Status | Notes | +|--------------|---------|--------|-------| +| Blocks | `REF-006-storage-records-and-atomic-appends` | To Do | See milestone-3 | +| Depends On | `SPIKE-005-architecture-review` | In Progress | See milestone-3 | +| Related | `REF-003-retire-bash-workflow-dual-path` | To Do | See milestone-3 | + +## User / Business Goal + +Storage v3 (ledger + registry + .sdlc/ runtime under sdlc-spdd/) is the only persistence model. Legacy layouts, migrations, and archive folders make upgrade harder and fork behavior. Losing pre-v3 context is accepted. + +## Scope + +### IN SCOPE + +- archive deletes artifacts (git history is the record); remove spdd/*/archive/ moves and agent-context/sessions sweeps +- Remove root-layout and agent-context/harness fallbacks in project.py; sdlc-spdd/ is the home +- Remove legacy TSV registry reading in registry.py; registry.jsonl only +- Remove storage_migrate legacy parsers, agent_context_upgrade, context_model legacy markdown paths, installer/rollback legacy-layout archive +- upgrade-project.sh / verify-project-install.sh / framework-install.sh stop consolidating legacy sprawl; they refuse with a clear message and pointer to a fresh init +- Tests and docs (storage-v3.md, TESTING.md, README) describe only v3 + +### NOT IN SCOPE + +- Changing the v3 record schema (REF-006) +- Retiring the bash workflow twin (REF-003) + +## Acceptance Criteria + +- [ ] `grep -r 'agent-context\|work-registry' engine/src scripts templates` returns only the install-source folder name templates/agent-context/ +- [ ] archive_work removes canvas/analysis/review/sync/session/state files; no archive/ directories are created; unit + bash tests assert the same contract +- [ ] storage migrate, legacy-layout-archive, and rollback-of-legacy code paths are deleted with their tests +- [ ] TESTING.md and docs/storage-v3.md have no legacy-layout sections + +## Non-Goals + +- Expanding product surface (ADF, Vue3, Jira UX) under this Work ID +- Upstream PRs to embabel/guide + +## Jira + +Create the issue manually in Jira UI, then set **Key** (and matching `jira_key` frontmatter) and commit. + +- Key: TBD +- Issue type: Refactor +- Summary: Purge pre-v3 persistence compatibility; one data model +- Labels: sdlc-spdd, milestone-3, one-flow +- Components: framework + +### Description + +Storage v3 (ledger + registry + .sdlc/ runtime under sdlc-spdd/) is the only persistence model. Legacy layouts, migrations, and archive folders make upgrade harder and fork behavior. Losing pre-v3 context is accepted. + +### Acceptance criteria (Given/When/Then) + +- Given One flow on storage v3 is the active program, When this Work ID is complete, Then the acceptance criteria above are checked and the analysis/canvas (if any) match implementation. + +## GitHub + +Create the issue manually in GitHub UI, then set **Number** (and matching `github_number` frontmatter when used) and commit. + +- Number: TBD +- Title: Purge pre-v3 persistence compatibility; one data model +- Labels: sdlc-spdd, milestone-3, one-flow +- URL: + +### Description + +See this requirement and `spdd/analysis/SPIKE-005-architecture-review-analysis.md`. + +## Next Step + + /sdlc-spdd-analysis @sdlc-spdd/requirements/milestones/milestone-3/REF-002-purge-pre-v3-compat.md 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 new file mode 100644 index 00000000..f0b92624 --- /dev/null +++ b/sdlc-spdd/requirements/milestones/milestone-3/REF-003-retire-bash-workflow-dual-path.md @@ -0,0 +1,104 @@ +--- +work_id: "REF-003-retire-bash-workflow-dual-path" +jira_key: "" +github_number: "" +jira_epic: "" +jira_type: "Refactor" +jira_status: "To Do" +jira_assignee: "" +jira_due_date: "" +jira_sprint: "" +milestone: "milestone-3" +priority: "P0" +size: "L" +blocks: + - "REF-010-pythonize-session-and-capture" +depends_on: + - "REF-002-purge-pre-v3-compat" +related: + - "REF-001-engine-single-source" +--- + +# REF-003: Retire the bash workflow twin; Python engine is the only flow + +**Work ID:** REF-003-retire-bash-workflow-dual-path +**Milestone:** Milestone 3 — One flow on storage v3 +**Status:** To Do +**Priority / size:** P0 / L +**Date:** 2026-09-13 +**Beck stage:** make it right (one engine, one persistence model, aligned docs/tests) + +## Related Work + +| Relationship | Work ID | Status | Notes | +|--------------|---------|--------|-------| +| Blocks | `REF-010-pythonize-session-and-capture` | To Do | See milestone-3 | +| Depends On | `REF-002-purge-pre-v3-compat` | To Do | See milestone-3 | +| Related | `REF-001-engine-single-source` | Complete | See milestone-2 | + +## User / Business Goal + +sdlc-workflow.sh (2 089 LOC) and sdlc-team-registry.sh (981 LOC) reimplement workflow, registry, pointer, and gate. Two engines mean two behaviors. Keep bash only for install/upgrade packaging and as a thin dispatcher. + +## Scope + +### IN SCOPE + +- Delete SDLC_ENGINE=shell and SDLC_GATE_ENGINE=shell; sdlc.sh requires the Python engine and errors with the install hint +- Delete workflow/registry/pointer/gate bash implementations from templates/agent-context; keep sdlc.sh dispatcher and install scripts +- Gate path uses resolve_engine_python (SDLC_PY), never bare python3 +- cmd_shell bridges sdlc-spdd/scripts in installed targets +- Bash tests that exercised shell verbs move to pytest or are deleted; live-consumer matrix runs Python-only +- Docs (engine-v2.md, TESTING.md, README) state one engine + +### NOT IN SCOPE + +- Pythonizing session/capture scripts (REF-010) +- Removing install/upgrade shell + +## 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 + +## Non-Goals + +- Expanding product surface (ADF, Vue3, Jira UX) under this Work ID +- Upstream PRs to embabel/guide + +## Jira + +Create the issue manually in Jira UI, then set **Key** (and matching `jira_key` frontmatter) and commit. + +- Key: TBD +- Issue type: Refactor +- Summary: Retire the bash workflow twin; Python engine is the only flow +- Labels: sdlc-spdd, milestone-3, one-flow +- Components: framework + +### Description + +sdlc-workflow.sh (2 089 LOC) and sdlc-team-registry.sh (981 LOC) reimplement workflow, registry, pointer, and gate. Two engines mean two behaviors. Keep bash only for install/upgrade packaging and as a thin dispatcher. + +### Acceptance criteria (Given/When/Then) + +- Given One flow on storage v3 is the active program, When this Work ID is complete, Then the acceptance criteria above are checked and the analysis/canvas (if any) match implementation. + +## GitHub + +Create the issue manually in GitHub UI, then set **Number** (and matching `github_number` frontmatter when used) and commit. + +- Number: TBD +- Title: Retire the bash workflow twin; Python engine is the only flow +- Labels: sdlc-spdd, milestone-3, one-flow +- URL: + +### Description + +See this requirement and `spdd/analysis/SPIKE-005-architecture-review-analysis.md`. + +## Next Step + + /sdlc-spdd-analysis @sdlc-spdd/requirements/milestones/milestone-3/REF-003-retire-bash-workflow-dual-path.md diff --git a/sdlc-spdd/requirements/milestones/milestone-3/REF-004-split-installer-blueprints.md b/sdlc-spdd/requirements/milestones/milestone-3/REF-004-split-installer-blueprints.md new file mode 100644 index 00000000..be36e3eb --- /dev/null +++ b/sdlc-spdd/requirements/milestones/milestone-3/REF-004-split-installer-blueprints.md @@ -0,0 +1,99 @@ +--- +work_id: "REF-004-split-installer-blueprints" +jira_key: "" +github_number: "" +jira_epic: "" +jira_type: "Refactor" +jira_status: "To Do" +jira_assignee: "" +jira_due_date: "" +jira_sprint: "" +milestone: "milestone-3" +priority: "P1" +size: "M" +blocks: [] +depends_on: + - "CHORE-004-lint-and-complexity-gates-real" +related: + - "REF-008-console-viewer-hardening" +--- + +# REF-004: Split installer/app.py create_app into Flask blueprints + +**Work ID:** REF-004-split-installer-blueprints +**Milestone:** Milestone 3 — One flow on storage v3 +**Status:** To Do +**Priority / size:** P1 / M +**Date:** 2026-09-13 +**Beck stage:** make it right (one engine, one persistence model, aligned docs/tests) + +## Related Work + +| Relationship | Work ID | Status | Notes | +|--------------|---------|--------|-------| +| Blocks | (none) | — | — | +| Depends On | `CHORE-004-lint-and-complexity-gates-real` | To Do | See milestone-3 | +| Related | `REF-008-console-viewer-hardening` | To Do | See milestone-3 | + +## User / Business Goal + +create_app is one 1 056-line nested function holding every /api/* route; routes cannot be tested or reviewed in isolation. + +## Scope + +### IN SCOPE + +- One blueprint per console tab (install, persistence, sqlite, guide, issues, adf, templates, dashboard, rollback) +- Shared request/response helpers; SystemExit on missing Flask becomes an ImportError with a hint +- Remove no-op wrapper aliases at installer/app.py:88–119 + +### NOT IN SCOPE + +- Changing API payloads the Vue3 console depends on +- Viewer app (REF-009) + +## Acceptance Criteria + +- [ ] No function in installer/ exceeds 80 NLOC or CCN 10 (lizard) +- [ ] Integration suite and Vue3 Playwright pass unchanged +- [ ] installer coverage gate stays ≥ 90 % + +## Non-Goals + +- Expanding product surface (ADF, Vue3, Jira UX) under this Work ID +- Upstream PRs to embabel/guide + +## Jira + +Create the issue manually in Jira UI, then set **Key** (and matching `jira_key` frontmatter) and commit. + +- Key: TBD +- Issue type: Refactor +- Summary: Split installer/app.py create_app into Flask blueprints +- Labels: sdlc-spdd, milestone-3, one-flow +- Components: framework + +### Description + +create_app is one 1 056-line nested function holding every /api/* route; routes cannot be tested or reviewed in isolation. + +### Acceptance criteria (Given/When/Then) + +- Given One flow on storage v3 is the active program, When this Work ID is complete, Then the acceptance criteria above are checked and the analysis/canvas (if any) match implementation. + +## GitHub + +Create the issue manually in GitHub UI, then set **Number** (and matching `github_number` frontmatter when used) and commit. + +- Number: TBD +- Title: Split installer/app.py create_app into Flask blueprints +- Labels: sdlc-spdd, milestone-3, one-flow +- URL: + +### Description + +See this requirement and `spdd/analysis/SPIKE-005-architecture-review-analysis.md`. + +## Next Step + + /sdlc-spdd-analysis @sdlc-spdd/requirements/milestones/milestone-3/REF-004-split-installer-blueprints.md diff --git a/sdlc-spdd/requirements/milestones/milestone-3/REF-005-cli-command-modules.md b/sdlc-spdd/requirements/milestones/milestone-3/REF-005-cli-command-modules.md new file mode 100644 index 00000000..340524d6 --- /dev/null +++ b/sdlc-spdd/requirements/milestones/milestone-3/REF-005-cli-command-modules.md @@ -0,0 +1,99 @@ +--- +work_id: "REF-005-cli-command-modules" +jira_key: "" +github_number: "" +jira_epic: "" +jira_type: "Refactor" +jira_status: "To Do" +jira_assignee: "" +jira_due_date: "" +jira_sprint: "" +milestone: "milestone-3" +priority: "P1" +size: "M" +blocks: [] +depends_on: + - "REF-003-retire-bash-workflow-dual-path" +related: [] +--- + +# REF-005: One module per CLI command with result objects, not prints + +**Work ID:** REF-005-cli-command-modules +**Milestone:** Milestone 3 — One flow on storage v3 +**Status:** To Do +**Priority / size:** P1 / M +**Date:** 2026-09-13 +**Beck stage:** make it right (one engine, one persistence model, aligned docs/tests) + +## Related Work + +| Relationship | Work ID | Status | Notes | +|--------------|---------|--------|-------| +| Blocks | (none) | — | — | +| Depends On | `REF-003-retire-bash-workflow-dual-path` | To Do | See milestone-3 | +| Related | (none) | — | — | + +## User / Business Goal + +cli_parser imports every handler; handlers print as their API; importing the CLI loads storage, issues, and sunset. Commands need to be importable, testable units. + +## Scope + +### IN SCOPE + +- engine/src/sdlc_engine/commands/.py with register(subparsers) and run(args) -> CommandResult +- Lazy import of heavy services inside run() +- Consistent exit codes; no SystemExit in library modules (canvas.py) +- Remove cli.py compatibility re-exports and issue_tracker.py shim +- Package __init__ stops importing workflow/registry/archive + +### NOT IN SCOPE + +- Changing command names or flags + +## Acceptance Criteria + +- [ ] `python -c 'import sdlc_engine.cli'` imports no Flask, sqlite3, or urllib modules (test) +- [ ] No cmd_* function exceeds CCN 10 +- [ ] tests/test-sdlc-engine-shim.sh and unit CLI tests pass + +## Non-Goals + +- Expanding product surface (ADF, Vue3, Jira UX) under this Work ID +- Upstream PRs to embabel/guide + +## Jira + +Create the issue manually in Jira UI, then set **Key** (and matching `jira_key` frontmatter) and commit. + +- Key: TBD +- Issue type: Refactor +- Summary: One module per CLI command with result objects, not prints +- Labels: sdlc-spdd, milestone-3, one-flow +- Components: framework + +### Description + +cli_parser imports every handler; handlers print as their API; importing the CLI loads storage, issues, and sunset. Commands need to be importable, testable units. + +### Acceptance criteria (Given/When/Then) + +- Given One flow on storage v3 is the active program, When this Work ID is complete, Then the acceptance criteria above are checked and the analysis/canvas (if any) match implementation. + +## GitHub + +Create the issue manually in GitHub UI, then set **Number** (and matching `github_number` frontmatter when used) and commit. + +- Number: TBD +- Title: One module per CLI command with result objects, not prints +- Labels: sdlc-spdd, milestone-3, one-flow +- URL: + +### Description + +See this requirement and `spdd/analysis/SPIKE-005-architecture-review-analysis.md`. + +## Next Step + + /sdlc-spdd-analysis @sdlc-spdd/requirements/milestones/milestone-3/REF-005-cli-command-modules.md diff --git a/sdlc-spdd/requirements/milestones/milestone-3/REF-006-storage-records-and-atomic-appends.md b/sdlc-spdd/requirements/milestones/milestone-3/REF-006-storage-records-and-atomic-appends.md new file mode 100644 index 00000000..913526f8 --- /dev/null +++ b/sdlc-spdd/requirements/milestones/milestone-3/REF-006-storage-records-and-atomic-appends.md @@ -0,0 +1,98 @@ +--- +work_id: "REF-006-storage-records-and-atomic-appends" +jira_key: "" +github_number: "" +jira_epic: "" +jira_type: "Refactor" +jira_status: "To Do" +jira_assignee: "" +jira_due_date: "" +jira_sprint: "" +milestone: "milestone-3" +priority: "P1" +size: "M" +blocks: [] +depends_on: + - "REF-002-purge-pre-v3-compat" +related: [] +--- + +# REF-006: Typed storage records and atomic, locked JSONL appends + +**Work ID:** REF-006-storage-records-and-atomic-appends +**Milestone:** Milestone 3 — One flow on storage v3 +**Status:** To Do +**Priority / size:** P1 / M +**Date:** 2026-09-13 +**Beck stage:** make it right (one engine, one persistence model, aligned docs/tests) + +## Related Work + +| Relationship | Work ID | Status | Notes | +|--------------|---------|--------|-------| +| Blocks | (none) | — | — | +| Depends On | `REF-002-purge-pre-v3-compat` | To Do | See milestone-3 | +| Related | (none) | — | — | + +## User / Business Goal + +Ledger records are typed but registry events and persist results are ad-hoc dicts; LEDGER_KINDS is defined twice; stage and registry appends are unlocked open-append. + +## Scope + +### IN SCOPE + +- RegistryEvent dataclass; PersistResult dataclass; single LEDGER_KINDS +- io_util.append_jsonl_atomic with fcntl lock used by ledger stage, registry, and any JSONL writer +- io_util.save_json_dict atomic (tmp + replace) and adopted by persistence, integration_config, installer/guide +- Delete context_model dead helpers (iter_code_areas, extract_memory_facts) + +### NOT IN SCOPE + +- Schema version bump for existing ledgers + +## Acceptance Criteria + +- [ ] Concurrency test: 20 parallel captures produce 20 valid JSONL lines +- [ ] grep shows one LEDGER_KINDS definition +- [ ] No module writes JSON with bare write_text(json.dumps(...)) + +## Non-Goals + +- Expanding product surface (ADF, Vue3, Jira UX) under this Work ID +- Upstream PRs to embabel/guide + +## Jira + +Create the issue manually in Jira UI, then set **Key** (and matching `jira_key` frontmatter) and commit. + +- Key: TBD +- Issue type: Refactor +- Summary: Typed storage records and atomic, locked JSONL appends +- Labels: sdlc-spdd, milestone-3, one-flow +- Components: framework + +### Description + +Ledger records are typed but registry events and persist results are ad-hoc dicts; LEDGER_KINDS is defined twice; stage and registry appends are unlocked open-append. + +### Acceptance criteria (Given/When/Then) + +- Given One flow on storage v3 is the active program, When this Work ID is complete, Then the acceptance criteria above are checked and the analysis/canvas (if any) match implementation. + +## GitHub + +Create the issue manually in GitHub UI, then set **Number** (and matching `github_number` frontmatter when used) and commit. + +- Number: TBD +- Title: Typed storage records and atomic, locked JSONL appends +- Labels: sdlc-spdd, milestone-3, one-flow +- URL: + +### Description + +See this requirement and `spdd/analysis/SPIKE-005-architecture-review-analysis.md`. + +## Next Step + + /sdlc-spdd-analysis @sdlc-spdd/requirements/milestones/milestone-3/REF-006-storage-records-and-atomic-appends.md diff --git a/sdlc-spdd/requirements/milestones/milestone-3/REF-007-single-guide-transport.md b/sdlc-spdd/requirements/milestones/milestone-3/REF-007-single-guide-transport.md new file mode 100644 index 00000000..c8ba8eb1 --- /dev/null +++ b/sdlc-spdd/requirements/milestones/milestone-3/REF-007-single-guide-transport.md @@ -0,0 +1,98 @@ +--- +work_id: "REF-007-single-guide-transport" +jira_key: "" +github_number: "" +jira_epic: "" +jira_type: "Refactor" +jira_status: "To Do" +jira_assignee: "" +jira_due_date: "" +jira_sprint: "" +milestone: "milestone-3" +priority: "P1" +size: "S" +blocks: [] +depends_on: + - "REF-006-storage-records-and-atomic-appends" +related: + - "TEST-003-cretrieve-roundtrip" +--- + +# REF-007: GuideClient is the only Guide HTTP path + +**Work ID:** REF-007-single-guide-transport +**Milestone:** Milestone 3 — One flow on storage v3 +**Status:** To Do +**Priority / size:** P1 / S +**Date:** 2026-09-13 +**Beck stage:** make it right (one engine, one persistence model, aligned docs/tests) + +## Related Work + +| Relationship | Work ID | Status | Notes | +|--------------|---------|--------|-------| +| Blocks | (none) | — | — | +| Depends On | `REF-006-storage-records-and-atomic-appends` | To Do | See milestone-3 | +| Related | `TEST-003-cretrieve-roundtrip` | Complete | See milestone-2 | + +## User / Business Goal + +Guide HTTP is implemented three times (GuideClient, ContextStore raw urllib, installer guide_compliance/guide_ops); base URL resolution is inlined three times; a Neo4j default password is hardcoded. + +## Scope + +### IN SCOPE + +- ContextStore, guide_compliance, guide_ops call GuideClient +- resolve_guide_base_url used everywhere +- Neo4j password comes from env/config only; no literal in source + +### NOT IN SCOPE + +- Guide API changes +- Embabel upstream anything + +## Acceptance Criteria + +- [ ] `grep -rn urllib engine/src/sdlc_engine | grep -v guide_client.py` returns only issues.py +- [ ] TEST-003 C-RETRIEVE suite passes with the mocked client injected once + +## Non-Goals + +- Expanding product surface (ADF, Vue3, Jira UX) under this Work ID +- Upstream PRs to embabel/guide + +## Jira + +Create the issue manually in Jira UI, then set **Key** (and matching `jira_key` frontmatter) and commit. + +- Key: TBD +- Issue type: Refactor +- Summary: GuideClient is the only Guide HTTP path +- Labels: sdlc-spdd, milestone-3, one-flow +- Components: framework + +### Description + +Guide HTTP is implemented three times (GuideClient, ContextStore raw urllib, installer guide_compliance/guide_ops); base URL resolution is inlined three times; a Neo4j default password is hardcoded. + +### Acceptance criteria (Given/When/Then) + +- Given One flow on storage v3 is the active program, When this Work ID is complete, Then the acceptance criteria above are checked and the analysis/canvas (if any) match implementation. + +## GitHub + +Create the issue manually in GitHub UI, then set **Number** (and matching `github_number` frontmatter when used) and commit. + +- Number: TBD +- Title: GuideClient is the only Guide HTTP path +- Labels: sdlc-spdd, milestone-3, one-flow +- URL: + +### Description + +See this requirement and `spdd/analysis/SPIKE-005-architecture-review-analysis.md`. + +## Next Step + + /sdlc-spdd-analysis @sdlc-spdd/requirements/milestones/milestone-3/REF-007-single-guide-transport.md diff --git a/sdlc-spdd/requirements/milestones/milestone-3/REF-008-console-viewer-hardening.md b/sdlc-spdd/requirements/milestones/milestone-3/REF-008-console-viewer-hardening.md new file mode 100644 index 00000000..3824b77e --- /dev/null +++ b/sdlc-spdd/requirements/milestones/milestone-3/REF-008-console-viewer-hardening.md @@ -0,0 +1,99 @@ +--- +work_id: "REF-008-console-viewer-hardening" +jira_key: "" +github_number: "" +jira_epic: "" +jira_type: "Refactor" +jira_status: "To Do" +jira_assignee: "" +jira_due_date: "" +jira_sprint: "" +milestone: "milestone-3" +priority: "P1" +size: "M" +blocks: [] +depends_on: + - "REF-004-split-installer-blueprints" +related: [] +--- + +# REF-008: Threat model and hardening for ops console and ADF viewer + +**Work ID:** REF-008-console-viewer-hardening +**Milestone:** Milestone 3 — One flow on storage v3 +**Status:** To Do +**Priority / size:** P1 / M +**Date:** 2026-09-13 +**Beck stage:** make it right (one engine, one persistence model, aligned docs/tests) + +## Related Work + +| Relationship | Work ID | Status | Notes | +|--------------|---------|--------|-------| +| Blocks | (none) | — | — | +| Depends On | `REF-004-split-installer-blueprints` | To Do | See milestone-3 | +| Related | (none) | — | — | + +## User / Business Goal + +Viewer browses the whole filesystem, console shells install/upgrade, and --lan binds 0.0.0.0 with no auth or CSRF. + +## Scope + +### IN SCOPE + +- Write docs/security/local-web-surfaces.md threat model +- --lan requires a one-time token in the URL/header; default stays 127.0.0.1 +- Viewer store restricted to an allow-listed root +- CSRF token on mutating POSTs (console + viewer) + +### NOT IN SCOPE + +- Multi-user auth +- TLS + +## Acceptance Criteria + +- [ ] Integration test: POST without token is rejected when --lan +- [ ] Integration test: path outside root is rejected by viewer store +- [ ] Threat model doc linked from ops-console.md and adf-viewer.md + +## Non-Goals + +- Expanding product surface (ADF, Vue3, Jira UX) under this Work ID +- Upstream PRs to embabel/guide + +## Jira + +Create the issue manually in Jira UI, then set **Key** (and matching `jira_key` frontmatter) and commit. + +- Key: TBD +- Issue type: Refactor +- Summary: Threat model and hardening for ops console and ADF viewer +- Labels: sdlc-spdd, milestone-3, one-flow +- Components: framework + +### Description + +Viewer browses the whole filesystem, console shells install/upgrade, and --lan binds 0.0.0.0 with no auth or CSRF. + +### Acceptance criteria (Given/When/Then) + +- Given One flow on storage v3 is the active program, When this Work ID is complete, Then the acceptance criteria above are checked and the analysis/canvas (if any) match implementation. + +## GitHub + +Create the issue manually in GitHub UI, then set **Number** (and matching `github_number` frontmatter when used) and commit. + +- Number: TBD +- Title: Threat model and hardening for ops console and ADF viewer +- Labels: sdlc-spdd, milestone-3, one-flow +- URL: + +### Description + +See this requirement and `spdd/analysis/SPIKE-005-architecture-review-analysis.md`. + +## Next Step + + /sdlc-spdd-analysis @sdlc-spdd/requirements/milestones/milestone-3/REF-008-console-viewer-hardening.md diff --git a/sdlc-spdd/requirements/milestones/milestone-3/REF-009-adf-codec-and-viewer-assets.md b/sdlc-spdd/requirements/milestones/milestone-3/REF-009-adf-codec-and-viewer-assets.md new file mode 100644 index 00000000..e991db0b --- /dev/null +++ b/sdlc-spdd/requirements/milestones/milestone-3/REF-009-adf-codec-and-viewer-assets.md @@ -0,0 +1,95 @@ +--- +work_id: "REF-009-adf-codec-and-viewer-assets" +jira_key: "" +github_number: "" +jira_epic: "" +jira_type: "Refactor" +jira_status: "To Do" +jira_assignee: "" +jira_due_date: "" +jira_sprint: "" +milestone: "milestone-3" +priority: "P2" +size: "M" +blocks: [] +depends_on: + - "REF-004-split-installer-blueprints" +related: [] +--- + +# REF-009: One ADF codec; viewer UI as static assets or Vue tab + +**Work ID:** REF-009-adf-codec-and-viewer-assets +**Milestone:** Milestone 3 — One flow on storage v3 +**Status:** To Do +**Priority / size:** P2 / M +**Date:** 2026-09-13 +**Beck stage:** make it right (one engine, one persistence model, aligned docs/tests) + +## Related Work + +| Relationship | Work ID | Status | Notes | +|--------------|---------|--------|-------| +| Blocks | (none) | — | — | +| Depends On | `REF-004-split-installer-blueprints` | To Do | See milestone-3 | +| Related | (none) | — | — | + +## User / Business Goal + +ADF conversion lives in jira_format and again in viewer/adf_html + html_adf; viewer/pages.py is 1 190 lines of HTML in Python strings. + +## Scope + +### IN SCOPE + +- sdlc_engine/adf/codec.py shared by issues, viewer, templates +- Move viewer UI into console-ui (Vue) or static files; delete pages.py + +### NOT IN SCOPE + +- New ADF node types + +## Acceptance Criteria + +- [ ] One implementation of GWT list detection +- [ ] viewer/pages.py deleted; Playwright viewer suite passes + +## Non-Goals + +- Expanding product surface (ADF, Vue3, Jira UX) under this Work ID +- Upstream PRs to embabel/guide + +## Jira + +Create the issue manually in Jira UI, then set **Key** (and matching `jira_key` frontmatter) and commit. + +- Key: TBD +- Issue type: Refactor +- Summary: One ADF codec; viewer UI as static assets or Vue tab +- Labels: sdlc-spdd, milestone-3, one-flow +- Components: framework + +### Description + +ADF conversion lives in jira_format and again in viewer/adf_html + html_adf; viewer/pages.py is 1 190 lines of HTML in Python strings. + +### Acceptance criteria (Given/When/Then) + +- Given One flow on storage v3 is the active program, When this Work ID is complete, Then the acceptance criteria above are checked and the analysis/canvas (if any) match implementation. + +## GitHub + +Create the issue manually in GitHub UI, then set **Number** (and matching `github_number` frontmatter when used) and commit. + +- Number: TBD +- Title: One ADF codec; viewer UI as static assets or Vue tab +- Labels: sdlc-spdd, milestone-3, one-flow +- URL: + +### Description + +See this requirement and `spdd/analysis/SPIKE-005-architecture-review-analysis.md`. + +## Next Step + + /sdlc-spdd-analysis @sdlc-spdd/requirements/milestones/milestone-3/REF-009-adf-codec-and-viewer-assets.md diff --git a/sdlc-spdd/requirements/milestones/milestone-3/REF-010-pythonize-session-and-capture.md b/sdlc-spdd/requirements/milestones/milestone-3/REF-010-pythonize-session-and-capture.md new file mode 100644 index 00000000..c89341b5 --- /dev/null +++ b/sdlc-spdd/requirements/milestones/milestone-3/REF-010-pythonize-session-and-capture.md @@ -0,0 +1,96 @@ +--- +work_id: "REF-010-pythonize-session-and-capture" +jira_key: "" +github_number: "" +jira_epic: "" +jira_type: "Refactor" +jira_status: "To Do" +jira_assignee: "" +jira_due_date: "" +jira_sprint: "" +milestone: "milestone-3" +priority: "P2" +size: "L" +blocks: [] +depends_on: + - "REF-003-retire-bash-workflow-dual-path" +related: [] +--- + +# REF-010: Session brief, capture, resolve-context, and create-work in Python + +**Work ID:** REF-010-pythonize-session-and-capture +**Milestone:** Milestone 3 — One flow on storage v3 +**Status:** To Do +**Priority / size:** P2 / L +**Date:** 2026-09-13 +**Beck stage:** make it right (one engine, one persistence model, aligned docs/tests) + +## Related Work + +| Relationship | Work ID | Status | Notes | +|--------------|---------|--------|-------| +| Blocks | (none) | — | — | +| Depends On | `REF-003-retire-bash-workflow-dual-path` | To Do | See milestone-3 | +| Related | (none) | — | — | + +## User / Business Goal + +start-agent-session.sh, capture-session-memory.sh, resolve-agent-context.sh, create-work-from-milestone.sh (~2 200 LOC) have no Python twin; accept has two verbs. + +## Scope + +### IN SCOPE + +- sdlc-engine session start/resume, capture, accept, resolve, work create-from-milestone +- sdlc.sh routes those verbs to Python; shell scripts deleted +- create-from-milestone uses templates/reasons-canvas, not an inlined body + +### NOT IN SCOPE + +- Changing the session brief format + +## Acceptance Criteria + +- [ ] scripts/ contains no *.sh over 200 LOC except install/upgrade +- [ ] One accept verb + +## Non-Goals + +- Expanding product surface (ADF, Vue3, Jira UX) under this Work ID +- Upstream PRs to embabel/guide + +## Jira + +Create the issue manually in Jira UI, then set **Key** (and matching `jira_key` frontmatter) and commit. + +- Key: TBD +- Issue type: Refactor +- Summary: Session brief, capture, resolve-context, and create-work in Python +- Labels: sdlc-spdd, milestone-3, one-flow +- Components: framework + +### Description + +start-agent-session.sh, capture-session-memory.sh, resolve-agent-context.sh, create-work-from-milestone.sh (~2 200 LOC) have no Python twin; accept has two verbs. + +### Acceptance criteria (Given/When/Then) + +- Given One flow on storage v3 is the active program, When this Work ID is complete, Then the acceptance criteria above are checked and the analysis/canvas (if any) match implementation. + +## GitHub + +Create the issue manually in GitHub UI, then set **Number** (and matching `github_number` frontmatter when used) and commit. + +- Number: TBD +- Title: Session brief, capture, resolve-context, and create-work in Python +- Labels: sdlc-spdd, milestone-3, one-flow +- URL: + +### Description + +See this requirement and `spdd/analysis/SPIKE-005-architecture-review-analysis.md`. + +## Next Step + + /sdlc-spdd-analysis @sdlc-spdd/requirements/milestones/milestone-3/REF-010-pythonize-session-and-capture.md diff --git a/sdlc-spdd/requirements/milestones/milestone-3/REF-011-split-issue-sync-adapters.md b/sdlc-spdd/requirements/milestones/milestone-3/REF-011-split-issue-sync-adapters.md new file mode 100644 index 00000000..5bb4495a --- /dev/null +++ b/sdlc-spdd/requirements/milestones/milestone-3/REF-011-split-issue-sync-adapters.md @@ -0,0 +1,94 @@ +--- +work_id: "REF-011-split-issue-sync-adapters" +jira_key: "" +github_number: "" +jira_epic: "" +jira_type: "Refactor" +jira_status: "To Do" +jira_assignee: "" +jira_due_date: "" +jira_sprint: "" +milestone: "milestone-3" +priority: "P2" +size: "M" +blocks: [] +depends_on: + - "REF-005-cli-command-modules" +related: [] +--- + +# REF-011: Split IssueSyncService into Jira and GitHub adapters + +**Work ID:** REF-011-split-issue-sync-adapters +**Milestone:** Milestone 3 — One flow on storage v3 +**Status:** To Do +**Priority / size:** P2 / M +**Date:** 2026-09-13 +**Beck stage:** make it right (one engine, one persistence model, aligned docs/tests) + +## Related Work + +| Relationship | Work ID | Status | Notes | +|--------------|---------|--------|-------| +| Blocks | (none) | — | — | +| Depends On | `REF-005-cli-command-modules` | To Do | See milestone-3 | +| Related | (none) | — | — | + +## User / Business Goal + +issues.py is 1 209 LOC covering draft/create/edit/pull/ADF upload/link for two trackers in one class. + +## Scope + +### IN SCOPE + +- IssueTracker protocol; JiraAdapter, GitHubAdapter; shared draft/link logic + +### NOT IN SCOPE + +- New tracker features + +## Acceptance Criteria + +- [ ] No function in issues/ exceeds CCN 10 +- [ ] Mocked issue tests pass unchanged + +## Non-Goals + +- Expanding product surface (ADF, Vue3, Jira UX) under this Work ID +- Upstream PRs to embabel/guide + +## Jira + +Create the issue manually in Jira UI, then set **Key** (and matching `jira_key` frontmatter) and commit. + +- Key: TBD +- Issue type: Refactor +- Summary: Split IssueSyncService into Jira and GitHub adapters +- Labels: sdlc-spdd, milestone-3, one-flow +- Components: framework + +### Description + +issues.py is 1 209 LOC covering draft/create/edit/pull/ADF upload/link for two trackers in one class. + +### Acceptance criteria (Given/When/Then) + +- Given One flow on storage v3 is the active program, When this Work ID is complete, Then the acceptance criteria above are checked and the analysis/canvas (if any) match implementation. + +## GitHub + +Create the issue manually in GitHub UI, then set **Number** (and matching `github_number` frontmatter when used) and commit. + +- Number: TBD +- Title: Split IssueSyncService into Jira and GitHub adapters +- Labels: sdlc-spdd, milestone-3, one-flow +- URL: + +### Description + +See this requirement and `spdd/analysis/SPIKE-005-architecture-review-analysis.md`. + +## Next Step + + /sdlc-spdd-analysis @sdlc-spdd/requirements/milestones/milestone-3/REF-011-split-issue-sync-adapters.md diff --git a/sdlc-spdd/requirements/milestones/milestone-3/SPIKE-005-architecture-review.md b/sdlc-spdd/requirements/milestones/milestone-3/SPIKE-005-architecture-review.md new file mode 100644 index 00000000..94782128 --- /dev/null +++ b/sdlc-spdd/requirements/milestones/milestone-3/SPIKE-005-architecture-review.md @@ -0,0 +1,109 @@ +--- +work_id: "SPIKE-005-architecture-review" +jira_key: "" +github_number: "" +jira_epic: "" +jira_type: "Spike" +jira_status: "In Progress" +jira_assignee: "" +jira_due_date: "" +jira_sprint: "" +milestone: "milestone-3" +priority: "P0" +size: "M" +blocks: + - "REF-002-purge-pre-v3-compat" + - "REF-003-retire-bash-workflow-dual-path" + - "BUG-001-db-query-undefined-names" + - "CHORE-004-lint-and-complexity-gates-real" +depends_on: [] +related: + - "SPIKE-004-academic-contribution-bar" +--- + +# SPIKE-005: Full architectural review and Milestone 3/4/5 program + +**Work ID:** SPIKE-005-architecture-review +**Milestone:** Milestone 3 — One flow on storage v3 +**Status:** In Progress +**Priority / size:** P0 / M +**Date:** 2026-09-13 +**Beck stage:** make it right (one engine, one persistence model, aligned docs/tests) + +## Related Work + +| Relationship | Work ID | Status | Notes | +|--------------|---------|--------|-------| +| Blocks | `REF-002-purge-pre-v3-compat` | To Do | See milestone-3 | +| Blocks | `REF-003-retire-bash-workflow-dual-path` | To Do | See milestone-3 | +| Blocks | `BUG-001-db-query-undefined-names` | To Do | See milestone-3 | +| Blocks | `CHORE-004-lint-and-complexity-gates-real` | To Do | See milestone-3 | +| Depends On | (none) | — | — | +| Related | `SPIKE-004-academic-contribution-bar` | Complete | See milestone-2 | + +## User / Business Goal + +Review source, tests, and docs end to end; open governed milestones so consolidation work is Work IDs, not ad-hoc edits. + +## Scope + +### IN SCOPE + +- Measured baseline (tests, coverage, complexity, lint, shellcheck, doc links) +- Four-area review (engine, shell/templates, tests/CI, docs) with file:line evidence +- Milestone 3, 4, 5 definitions, requirement stubs, task lists, Obsidian monthly goals +- ROADMAP pointer and session note + +### NOT IN SCOPE + +- Implementing the follow-on Work IDs (each has its own stub) +- Research claims (Milestone 2 froze them) + +## Acceptance Criteria + +- [x] Analysis at spdd/analysis/SPIKE-005-architecture-review-analysis.md with measured baseline +- [x] Milestone 3 and 4 have definitions, _milestone.yml, Linked Work tables; Milestone 5 has an outline +- [x] Each follow-on Work ID has a requirement stub +- [x] Task lists exist under spdd/tasks/ including the Obsidian monthly goals file +- [x] ROADMAP names Milestones 3–5 +- [ ] REASONS canvas is the program plan (/sdlc-spdd-plan) + +## Non-Goals + +- Expanding product surface (ADF, Vue3, Jira UX) under this Work ID +- Upstream PRs to embabel/guide + +## Jira + +Create the issue manually in Jira UI, then set **Key** (and matching `jira_key` frontmatter) and commit. + +- Key: TBD +- Issue type: Spike +- Summary: Full architectural review and Milestone 3/4/5 program +- Labels: sdlc-spdd, milestone-3, one-flow +- Components: framework + +### Description + +Review source, tests, and docs end to end; open governed milestones so consolidation work is Work IDs, not ad-hoc edits. + +### Acceptance criteria (Given/When/Then) + +- Given One flow on storage v3 is the active program, When this Work ID is complete, Then the acceptance criteria above are checked and the analysis/canvas (if any) match implementation. + +## GitHub + +Create the issue manually in GitHub UI, then set **Number** (and matching `github_number` frontmatter when used) and commit. + +- Number: TBD +- Title: Full architectural review and Milestone 3/4/5 program +- Labels: sdlc-spdd, milestone-3, one-flow +- URL: + +### Description + +See this requirement and `spdd/analysis/SPIKE-005-architecture-review-analysis.md`. + +## Next Step + + /sdlc-spdd-analysis @sdlc-spdd/requirements/milestones/milestone-3/SPIKE-005-architecture-review.md diff --git a/sdlc-spdd/requirements/milestones/milestone-3/TEST-004-hermetic-unit-suite-and-fixtures.md b/sdlc-spdd/requirements/milestones/milestone-3/TEST-004-hermetic-unit-suite-and-fixtures.md new file mode 100644 index 00000000..9457c5b3 --- /dev/null +++ b/sdlc-spdd/requirements/milestones/milestone-3/TEST-004-hermetic-unit-suite-and-fixtures.md @@ -0,0 +1,100 @@ +--- +work_id: "TEST-004-hermetic-unit-suite-and-fixtures" +jira_key: "" +github_number: "" +jira_epic: "" +jira_type: "Test" +jira_status: "To Do" +jira_assignee: "" +jira_due_date: "" +jira_sprint: "" +milestone: "milestone-3" +priority: "P1" +size: "M" +blocks: [] +depends_on: + - "REF-002-purge-pre-v3-compat" +related: + - "CHORE-005-ci-reusable-workflows" +--- + +# TEST-004: Hermetic unit suite, shared installed-target fixture, no grab-bag tests + +**Work ID:** TEST-004-hermetic-unit-suite-and-fixtures +**Milestone:** Milestone 3 — One flow on storage v3 +**Status:** To Do +**Priority / size:** P1 / M +**Date:** 2026-09-13 +**Beck stage:** make it right (one engine, one persistence model, aligned docs/tests) + +## Related Work + +| Relationship | Work ID | Status | Notes | +|--------------|---------|--------|-------| +| Blocks | (none) | — | — | +| Depends On | `REF-002-purge-pre-v3-compat` | To Do | See milestone-3 | +| Related | `CHORE-005-ci-reusable-workflows` | To Do | See milestone-3 | + +## User / Business Goal + +Unit suite is documented as Flask-free but is not; grab-bag files are the only coverage for some modules; unit tests shell out to bash scripts; there is no shared fixture for an installed target. + +## Scope + +### IN SCOPE + +- Move Flask test-client tests to tests_integration +- Dissolve test_hard_review_gaps.py, test_remaining_cleanup_slices.py, test_installer_coverage_gaps.py into per-module tests +- conftest fixture installed_target(tmp_path) used by unit, integration, and research tests +- pytest testpaths lists all three suites with markers; bare pytest does not silently skip +- TESTING.md counts and suite descriptions regenerated from the tree + +### NOT IN SCOPE + +- New feature coverage + +## Acceptance Criteria + +- [ ] Unit suite imports no flask (test) +- [ ] No test file named *gaps* or *slices* +- [ ] TESTING.md numbers match `grep -c 'def test_'` + +## Non-Goals + +- Expanding product surface (ADF, Vue3, Jira UX) under this Work ID +- Upstream PRs to embabel/guide + +## Jira + +Create the issue manually in Jira UI, then set **Key** (and matching `jira_key` frontmatter) and commit. + +- Key: TBD +- Issue type: Test +- Summary: Hermetic unit suite, shared installed-target fixture, no grab-bag tests +- Labels: sdlc-spdd, milestone-3, one-flow +- Components: framework + +### Description + +Unit suite is documented as Flask-free but is not; grab-bag files are the only coverage for some modules; unit tests shell out to bash scripts; there is no shared fixture for an installed target. + +### Acceptance criteria (Given/When/Then) + +- Given One flow on storage v3 is the active program, When this Work ID is complete, Then the acceptance criteria above are checked and the analysis/canvas (if any) match implementation. + +## GitHub + +Create the issue manually in GitHub UI, then set **Number** (and matching `github_number` frontmatter when used) and commit. + +- Number: TBD +- Title: Hermetic unit suite, shared installed-target fixture, no grab-bag tests +- Labels: sdlc-spdd, milestone-3, one-flow +- URL: + +### Description + +See this requirement and `spdd/analysis/SPIKE-005-architecture-review-analysis.md`. + +## Next Step + + /sdlc-spdd-analysis @sdlc-spdd/requirements/milestones/milestone-3/TEST-004-hermetic-unit-suite-and-fixtures.md diff --git a/sdlc-spdd/requirements/milestones/milestone-3/_milestone.yml b/sdlc-spdd/requirements/milestones/milestone-3/_milestone.yml new file mode 100644 index 00000000..dd33058b --- /dev/null +++ b/sdlc-spdd/requirements/milestones/milestone-3/_milestone.yml @@ -0,0 +1,7 @@ +name: "Milestone 3 — One flow on storage v3" +number: 3 +start_date: "2026-09-13" +end_date: "" +status: "active" +related_epics: [] +notes: "One engine (Python sdlc-engine), one persistence model (storage v3), aligned code/tests/docs. Purge pre-v3 compatibility and the bash workflow twin; real lint/complexity/shellcheck gates; hermetic tests; consolidated CI. Opened by SPIKE-005." diff --git a/sdlc-spdd/requirements/milestones/milestone-4/CHORE-007-doc-links-gate.md b/sdlc-spdd/requirements/milestones/milestone-4/CHORE-007-doc-links-gate.md new file mode 100644 index 00000000..1a13832c --- /dev/null +++ b/sdlc-spdd/requirements/milestones/milestone-4/CHORE-007-doc-links-gate.md @@ -0,0 +1,97 @@ +--- +work_id: "CHORE-007-doc-links-gate" +jira_key: "" +github_number: "" +jira_epic: "" +jira_type: "Chore" +jira_status: "To Do" +jira_assignee: "" +jira_due_date: "" +jira_sprint: "" +milestone: "milestone-4" +priority: "P0" +size: "M" +blocks: [] +depends_on: + - "DOC-004-install-path-vocabulary" +related: + - "CHORE-008-single-docs-authoring-root" +--- + +# CHORE-007: Zero broken doc links, enforced in CI + +**Work ID:** CHORE-007-doc-links-gate +**Milestone:** Milestone 4 — Documentation truth and release hygiene +**Status:** To Do +**Priority / size:** P0 / M +**Date:** 2026-09-13 +**Beck stage:** make it right (docs) + +## Related Work + +| Relationship | Work ID | Status | Notes | +|--------------|---------|--------|-------| +| Blocks | (none) | — | — | +| Depends On | `DOC-004-install-path-vocabulary` | To Do | See milestone-4 | +| Related | `CHORE-008-single-docs-authoring-root` | To Do | See milestone-4 | + +## User / Business Goal + +verify-doc-links.sh reports 95 broken of 959 and is not in CI; shipped copies link orchestrator-only docs. + +## Scope + +### IN SCOPE + +- Fix in-tree broken links (hub → ROADMAP, agent-context README, milestone files) +- verify-doc-links.sh understands the install boundary (resolve templates/project-docs and sdlc-spdd/docs links against the installed layout) +- Add to ci-docs-boundary workflow + +### NOT IN SCOPE + +- Rewriting doc content + +## Acceptance Criteria + +- [ ] verify-doc-links.sh reports 0 broken +- [ ] CI fails on a new broken link (proved once) + +## Non-Goals + +- Expanding product surface (ADF, Vue3, Jira UX) under this Work ID +- Upstream PRs to embabel/guide + +## Jira + +Create the issue manually in Jira UI, then set **Key** (and matching `jira_key` frontmatter) and commit. + +- Key: TBD +- Issue type: Chore +- Summary: Zero broken doc links, enforced in CI +- Labels: sdlc-spdd, milestone-4, docs-truth +- Components: framework + +### Description + +verify-doc-links.sh reports 95 broken of 959 and is not in CI; shipped copies link orchestrator-only docs. + +### Acceptance criteria (Given/When/Then) + +- Given Documentation truth and release hygiene is the active program, When this Work ID is complete, Then the acceptance criteria above are checked and the analysis/canvas (if any) match implementation. + +## GitHub + +Create the issue manually in GitHub UI, then set **Number** (and matching `github_number` frontmatter when used) and commit. + +- Number: TBD +- Title: Zero broken doc links, enforced in CI +- Labels: sdlc-spdd, milestone-4, docs-truth +- URL: + +### Description + +See this requirement and `spdd/analysis/SPIKE-005-architecture-review-analysis.md`. + +## Next Step + + /sdlc-spdd-analysis @sdlc-spdd/requirements/milestones/milestone-4/CHORE-007-doc-links-gate.md diff --git a/sdlc-spdd/requirements/milestones/milestone-4/CHORE-008-single-docs-authoring-root.md b/sdlc-spdd/requirements/milestones/milestone-4/CHORE-008-single-docs-authoring-root.md new file mode 100644 index 00000000..e6da8a69 --- /dev/null +++ b/sdlc-spdd/requirements/milestones/milestone-4/CHORE-008-single-docs-authoring-root.md @@ -0,0 +1,96 @@ +--- +work_id: "CHORE-008-single-docs-authoring-root" +jira_key: "" +github_number: "" +jira_epic: "" +jira_type: "Chore" +jira_status: "To Do" +jira_assignee: "" +jira_due_date: "" +jira_sprint: "" +milestone: "milestone-4" +priority: "P1" +size: "M" +blocks: [] +depends_on: + - "CHORE-007-doc-links-gate" +related: [] +--- + +# CHORE-008: docs/ is the only authoring root; sdlc-spdd/docs is generated + +**Work ID:** CHORE-008-single-docs-authoring-root +**Milestone:** Milestone 4 — Documentation truth and release hygiene +**Status:** To Do +**Priority / size:** P1 / M +**Date:** 2026-09-13 +**Beck stage:** make it right (docs) + +## Related Work + +| Relationship | Work ID | Status | Notes | +|--------------|---------|--------|-------| +| Blocks | (none) | — | — | +| Depends On | `CHORE-007-doc-links-gate` | To Do | See milestone-4 | +| Related | (none) | — | — | + +## User / Business Goal + +Five shipped docs have diverged from docs/; research trees are split without saying so; one doc is orphaned. + +## Scope + +### IN SCOPE + +- Re-sync sdlc-spdd/docs from docs/ via upgrade; CI drift check +- Both README hubs state the research split explicitly +- Index or archive unattended-iterations.md; add spdd-compliance and sdlc-agents-and-the-framework to the hub + +### NOT IN SCOPE + +- Moving academic research out of dogfood + +## Acceptance Criteria + +- [ ] `diff -rq docs sdlc-spdd/docs` differs only in the intended README and research trees +- [ ] No orphan .md under docs/ or sdlc-spdd/docs + +## Non-Goals + +- Expanding product surface (ADF, Vue3, Jira UX) under this Work ID +- Upstream PRs to embabel/guide + +## Jira + +Create the issue manually in Jira UI, then set **Key** (and matching `jira_key` frontmatter) and commit. + +- Key: TBD +- Issue type: Chore +- Summary: docs/ is the only authoring root; sdlc-spdd/docs is generated +- Labels: sdlc-spdd, milestone-4, docs-truth +- Components: framework + +### Description + +Five shipped docs have diverged from docs/; research trees are split without saying so; one doc is orphaned. + +### Acceptance criteria (Given/When/Then) + +- Given Documentation truth and release hygiene is the active program, When this Work ID is complete, Then the acceptance criteria above are checked and the analysis/canvas (if any) match implementation. + +## GitHub + +Create the issue manually in GitHub UI, then set **Number** (and matching `github_number` frontmatter when used) and commit. + +- Number: TBD +- Title: docs/ is the only authoring root; sdlc-spdd/docs is generated +- Labels: sdlc-spdd, milestone-4, docs-truth +- URL: + +### Description + +See this requirement and `spdd/analysis/SPIKE-005-architecture-review-analysis.md`. + +## Next Step + + /sdlc-spdd-analysis @sdlc-spdd/requirements/milestones/milestone-4/CHORE-008-single-docs-authoring-root.md diff --git a/sdlc-spdd/requirements/milestones/milestone-4/CHORE-009-release-version-hygiene.md b/sdlc-spdd/requirements/milestones/milestone-4/CHORE-009-release-version-hygiene.md new file mode 100644 index 00000000..3a3cc6f7 --- /dev/null +++ b/sdlc-spdd/requirements/milestones/milestone-4/CHORE-009-release-version-hygiene.md @@ -0,0 +1,96 @@ +--- +work_id: "CHORE-009-release-version-hygiene" +jira_key: "" +github_number: "" +jira_epic: "" +jira_type: "Chore" +jira_status: "To Do" +jira_assignee: "" +jira_due_date: "" +jira_sprint: "" +milestone: "milestone-4" +priority: "P1" +size: "S" +blocks: [] +depends_on: + - "SPIKE-005-architecture-review" +related: [] +--- + +# CHORE-009: One version string, current README focus, dated CHANGELOG + +**Work ID:** CHORE-009-release-version-hygiene +**Milestone:** Milestone 4 — Documentation truth and release hygiene +**Status:** To Do +**Priority / size:** P1 / S +**Date:** 2026-09-13 +**Beck stage:** make it right (docs) + +## Related Work + +| Relationship | Work ID | Status | Notes | +|--------------|---------|--------|-------| +| Blocks | (none) | — | — | +| Depends On | `SPIKE-005-architecture-review` | In Progress | See milestone-3 | +| Related | (none) | — | — | + +## User / Business Goal + +Tags v2.0.0a7/a8 exist while pyproject, __init__, README, and a test pin 2.0.0a6; README Current focus cites past PRs as present; ROADMAP links archived canvases. + +## Scope + +### IN SCOPE + +- Version read from one place (pyproject) by __init__ and README badge check +- Release checklist in CONTRIBUTING (tag ↔ version ↔ CHANGELOG) +- README Current focus points at ROADMAP instead of PR lists; ROADMAP drops dead canvas links + +### NOT IN SCOPE + +- Cutting a release (human decision) + +## Acceptance Criteria + +- [ ] test_engine_shared asserts __version__ == pyproject without a literal +- [ ] CHANGELOG top entry is dated and matches the newest tag + +## Non-Goals + +- Expanding product surface (ADF, Vue3, Jira UX) under this Work ID +- Upstream PRs to embabel/guide + +## Jira + +Create the issue manually in Jira UI, then set **Key** (and matching `jira_key` frontmatter) and commit. + +- Key: TBD +- Issue type: Chore +- Summary: One version string, current README focus, dated CHANGELOG +- Labels: sdlc-spdd, milestone-4, docs-truth +- Components: framework + +### Description + +Tags v2.0.0a7/a8 exist while pyproject, __init__, README, and a test pin 2.0.0a6; README Current focus cites past PRs as present; ROADMAP links archived canvases. + +### Acceptance criteria (Given/When/Then) + +- Given Documentation truth and release hygiene is the active program, When this Work ID is complete, Then the acceptance criteria above are checked and the analysis/canvas (if any) match implementation. + +## GitHub + +Create the issue manually in GitHub UI, then set **Number** (and matching `github_number` frontmatter when used) and commit. + +- Number: TBD +- Title: One version string, current README focus, dated CHANGELOG +- Labels: sdlc-spdd, milestone-4, docs-truth +- URL: + +### Description + +See this requirement and `spdd/analysis/SPIKE-005-architecture-review-analysis.md`. + +## Next Step + + /sdlc-spdd-analysis @sdlc-spdd/requirements/milestones/milestone-4/CHORE-009-release-version-hygiene.md diff --git a/sdlc-spdd/requirements/milestones/milestone-4/CHORE-010-registry-reconciliation.md b/sdlc-spdd/requirements/milestones/milestone-4/CHORE-010-registry-reconciliation.md new file mode 100644 index 00000000..33fb0100 --- /dev/null +++ b/sdlc-spdd/requirements/milestones/milestone-4/CHORE-010-registry-reconciliation.md @@ -0,0 +1,97 @@ +--- +work_id: "CHORE-010-registry-reconciliation" +jira_key: "" +github_number: "" +jira_epic: "" +jira_type: "Chore" +jira_status: "To Do" +jira_assignee: "" +jira_due_date: "" +jira_sprint: "" +milestone: "milestone-4" +priority: "P1" +size: "S" +blocks: [] +depends_on: + - "SPIKE-005-architecture-review" +related: + - "REF-006-storage-records-and-atomic-appends" +--- + +# CHORE-010: registry.jsonl is the truth list-work reports + +**Work ID:** CHORE-010-registry-reconciliation +**Milestone:** Milestone 4 — Documentation truth and release hygiene +**Status:** To Do +**Priority / size:** P1 / S +**Date:** 2026-09-13 +**Beck stage:** make it right (docs) + +## Related Work + +| Relationship | Work ID | Status | Notes | +|--------------|---------|--------|-------| +| Blocks | (none) | — | — | +| Depends On | `SPIKE-005-architecture-review` | In Progress | See milestone-3 | +| Related | `REF-006-storage-records-and-atomic-appends` | To Do | See milestone-3 | + +## User / Business Goal + +11 Milestone 2 Work IDs have no claim/release event; SPIKE-004 and DOC-001 are still active; list-work derives done from canvas status, so registry and reality disagree. + +## Scope + +### IN SCOPE + +- Append release/archived events for Milestone 2 IDs via sdlc.sh (never hand-edit) +- list-work shows registry state and canvas state side by side, flags disagreement +- Milestone 2 _milestone.yml status complete with end_date + +### NOT IN SCOPE + +- Registry schema changes (REF-006) + +## Acceptance Criteria + +- [ ] Every Work ID with a canvas has a terminal registry event +- [ ] list-work prints a MISMATCH column when they differ + +## Non-Goals + +- Expanding product surface (ADF, Vue3, Jira UX) under this Work ID +- Upstream PRs to embabel/guide + +## Jira + +Create the issue manually in Jira UI, then set **Key** (and matching `jira_key` frontmatter) and commit. + +- Key: TBD +- Issue type: Chore +- Summary: registry.jsonl is the truth list-work reports +- Labels: sdlc-spdd, milestone-4, docs-truth +- Components: framework + +### Description + +11 Milestone 2 Work IDs have no claim/release event; SPIKE-004 and DOC-001 are still active; list-work derives done from canvas status, so registry and reality disagree. + +### Acceptance criteria (Given/When/Then) + +- Given Documentation truth and release hygiene is the active program, When this Work ID is complete, Then the acceptance criteria above are checked and the analysis/canvas (if any) match implementation. + +## GitHub + +Create the issue manually in GitHub UI, then set **Number** (and matching `github_number` frontmatter when used) and commit. + +- Number: TBD +- Title: registry.jsonl is the truth list-work reports +- Labels: sdlc-spdd, milestone-4, docs-truth +- URL: + +### Description + +See this requirement and `spdd/analysis/SPIKE-005-architecture-review-analysis.md`. + +## Next Step + + /sdlc-spdd-analysis @sdlc-spdd/requirements/milestones/milestone-4/CHORE-010-registry-reconciliation.md diff --git a/sdlc-spdd/requirements/milestones/milestone-4/DOC-004-install-path-vocabulary.md b/sdlc-spdd/requirements/milestones/milestone-4/DOC-004-install-path-vocabulary.md new file mode 100644 index 00000000..547b5fcb --- /dev/null +++ b/sdlc-spdd/requirements/milestones/milestone-4/DOC-004-install-path-vocabulary.md @@ -0,0 +1,98 @@ +--- +work_id: "DOC-004-install-path-vocabulary" +jira_key: "" +github_number: "" +jira_epic: "" +jira_type: "Documentation" +jira_status: "To Do" +jira_assignee: "" +jira_due_date: "" +jira_sprint: "" +milestone: "milestone-4" +priority: "P0" +size: "S" +blocks: [] +depends_on: + - "SPIKE-005-architecture-review" +related: + - "DOC-005-grounding-path-map-codegen" +--- + +# DOC-004: Docs describe the layout init actually creates + +**Work ID:** DOC-004-install-path-vocabulary +**Milestone:** Milestone 4 — Documentation truth and release hygiene +**Status:** To Do +**Priority / size:** P0 / S +**Date:** 2026-09-13 +**Beck stage:** make it right (docs) + +## Related Work + +| Relationship | Work ID | Status | Notes | +|--------------|---------|--------|-------| +| Blocks | (none) | — | — | +| Depends On | `SPIKE-005-architecture-review` | In Progress | See milestone-3 | +| Related | `DOC-005-grounding-path-map-codegen` | To Do | See milestone-4 | + +## User / Business Goal + +init writes sdlc-spdd/docs/ but guides say docs/sdlc-spdd/; first-day requires a legacy agent-context/ folder; CONTRIBUTING cites work-registry.tsv; workflow step counts disagree. + +## Scope + +### IN SCOPE + +- Replace docs/sdlc-spdd/ with sdlc-spdd/docs/ in installing, README hub, CONTRIBUTING +- first-day success criteria match init output +- work-registry.tsv → registry.jsonl; engine default = auto → single engine after REF-003 +- One workflow step count + +### NOT IN SCOPE + +- Restructuring docs trees (CHORE-008) + +## Acceptance Criteria + +- [ ] `grep -rn 'docs/sdlc-spdd' docs README.md CONTRIBUTING.md` is empty +- [ ] first-day checklist verified against a fresh init in /tmp + +## Non-Goals + +- Expanding product surface (ADF, Vue3, Jira UX) under this Work ID +- Upstream PRs to embabel/guide + +## Jira + +Create the issue manually in Jira UI, then set **Key** (and matching `jira_key` frontmatter) and commit. + +- Key: TBD +- Issue type: Documentation +- Summary: Docs describe the layout init actually creates +- Labels: sdlc-spdd, milestone-4, docs-truth +- Components: framework + +### Description + +init writes sdlc-spdd/docs/ but guides say docs/sdlc-spdd/; first-day requires a legacy agent-context/ folder; CONTRIBUTING cites work-registry.tsv; workflow step counts disagree. + +### Acceptance criteria (Given/When/Then) + +- Given Documentation truth and release hygiene is the active program, When this Work ID is complete, Then the acceptance criteria above are checked and the analysis/canvas (if any) match implementation. + +## GitHub + +Create the issue manually in GitHub UI, then set **Number** (and matching `github_number` frontmatter when used) and commit. + +- Number: TBD +- Title: Docs describe the layout init actually creates +- Labels: sdlc-spdd, milestone-4, docs-truth +- URL: + +### Description + +See this requirement and `spdd/analysis/SPIKE-005-architecture-review-analysis.md`. + +## Next Step + + /sdlc-spdd-analysis @sdlc-spdd/requirements/milestones/milestone-4/DOC-004-install-path-vocabulary.md diff --git a/sdlc-spdd/requirements/milestones/milestone-4/DOC-005-grounding-path-map-codegen.md b/sdlc-spdd/requirements/milestones/milestone-4/DOC-005-grounding-path-map-codegen.md new file mode 100644 index 00000000..fb25119d --- /dev/null +++ b/sdlc-spdd/requirements/milestones/milestone-4/DOC-005-grounding-path-map-codegen.md @@ -0,0 +1,97 @@ +--- +work_id: "DOC-005-grounding-path-map-codegen" +jira_key: "" +github_number: "" +jira_epic: "" +jira_type: "Documentation" +jira_status: "To Do" +jira_assignee: "" +jira_due_date: "" +jira_sprint: "" +milestone: "milestone-4" +priority: "P0" +size: "M" +blocks: [] +depends_on: + - "DOC-004-install-path-vocabulary" +related: + - "CHORE-006-dogfood-adapters-and-quick-spec" +--- + +# DOC-005: Generate assistant grounding from one path map + +**Work ID:** DOC-005-grounding-path-map-codegen +**Milestone:** Milestone 4 — Documentation truth and release hygiene +**Status:** To Do +**Priority / size:** P0 / M +**Date:** 2026-09-13 +**Beck stage:** make it right (docs) + +## Related Work + +| Relationship | Work ID | Status | Notes | +|--------------|---------|--------|-------| +| Blocks | (none) | — | — | +| Depends On | `DOC-004-install-path-vocabulary` | To Do | See milestone-4 | +| Related | `CHORE-006-dogfood-adapters-and-quick-spec` | To Do | See milestone-3 | + +## User / Business Goal + +Shipped grounding (templates/cursor/rules, templates/claude/CLAUDE.md, copilot-instructions) teaches ./scripts/sdlc-spdd/sdlc.sh and agent-context/harness, paths that do not exist in a target. + +## Scope + +### IN SCOPE + +- spec/grounding.spec.md + path map (orchestrator vs target) → generated .mdc / CLAUDE.md / copilot-instructions +- Dogfood grounding regenerated the same way +- CI --check like command adapters + +### NOT IN SCOPE + +- Changing grounding content beyond paths + +## Acceptance Criteria + +- [ ] No `scripts/sdlc-spdd/` or `agent-context/harness` in templates/ +- [ ] generate step is idempotent and checked in CI + +## Non-Goals + +- Expanding product surface (ADF, Vue3, Jira UX) under this Work ID +- Upstream PRs to embabel/guide + +## Jira + +Create the issue manually in Jira UI, then set **Key** (and matching `jira_key` frontmatter) and commit. + +- Key: TBD +- Issue type: Documentation +- Summary: Generate assistant grounding from one path map +- Labels: sdlc-spdd, milestone-4, docs-truth +- Components: framework + +### Description + +Shipped grounding (templates/cursor/rules, templates/claude/CLAUDE.md, copilot-instructions) teaches ./scripts/sdlc-spdd/sdlc.sh and agent-context/harness, paths that do not exist in a target. + +### Acceptance criteria (Given/When/Then) + +- Given Documentation truth and release hygiene is the active program, When this Work ID is complete, Then the acceptance criteria above are checked and the analysis/canvas (if any) match implementation. + +## GitHub + +Create the issue manually in GitHub UI, then set **Number** (and matching `github_number` frontmatter when used) and commit. + +- Number: TBD +- Title: Generate assistant grounding from one path map +- Labels: sdlc-spdd, milestone-4, docs-truth +- URL: + +### Description + +See this requirement and `spdd/analysis/SPIKE-005-architecture-review-analysis.md`. + +## Next Step + + /sdlc-spdd-analysis @sdlc-spdd/requirements/milestones/milestone-4/DOC-005-grounding-path-map-codegen.md diff --git a/sdlc-spdd/requirements/milestones/milestone-4/DOC-006-design-decisions-v3-and-starter-spec-archive.md b/sdlc-spdd/requirements/milestones/milestone-4/DOC-006-design-decisions-v3-and-starter-spec-archive.md new file mode 100644 index 00000000..bde75196 --- /dev/null +++ b/sdlc-spdd/requirements/milestones/milestone-4/DOC-006-design-decisions-v3-and-starter-spec-archive.md @@ -0,0 +1,95 @@ +--- +work_id: "DOC-006-design-decisions-v3-and-starter-spec-archive" +jira_key: "" +github_number: "" +jira_epic: "" +jira_type: "Documentation" +jira_status: "To Do" +jira_assignee: "" +jira_due_date: "" +jira_sprint: "" +milestone: "milestone-4" +priority: "P1" +size: "M" +blocks: [] +depends_on: + - "REF-002-purge-pre-v3-compat" +related: [] +--- + +# DOC-006: Rewrite design-decisions for storage v3; archive STARTER-SPEC + +**Work ID:** DOC-006-design-decisions-v3-and-starter-spec-archive +**Milestone:** Milestone 4 — Documentation truth and release hygiene +**Status:** To Do +**Priority / size:** P1 / M +**Date:** 2026-09-13 +**Beck stage:** make it right (docs) + +## Related Work + +| Relationship | Work ID | Status | Notes | +|--------------|---------|--------|-------| +| Blocks | (none) | — | — | +| Depends On | `REF-002-purge-pre-v3-compat` | To Do | See milestone-3 | +| Related | (none) | — | — | + +## User / Business Goal + +design-decisions.md still describes agent-context/, duplicate canvases, extensions trees (SPIKE-004 M7, unfixed); STARTER-SPEC.md is a 1 881-line fossil with two inbound links. + +## Scope + +### IN SCOPE + +- ADR-style design-decisions.md: ledger-first, single home, one engine, no archive folders, harness/skills +- Move STARTER-SPEC.md to docs/archive/ with a short pointer; update CHANGELOG link + +### NOT IN SCOPE + +- New decisions + +## Acceptance Criteria + +- [ ] design-decisions.md mentions no agent-context/ or extensions/ +- [ ] Root has no STARTER-SPEC.md; docs hub links the archive + +## Non-Goals + +- Expanding product surface (ADF, Vue3, Jira UX) under this Work ID +- Upstream PRs to embabel/guide + +## Jira + +Create the issue manually in Jira UI, then set **Key** (and matching `jira_key` frontmatter) and commit. + +- Key: TBD +- Issue type: Documentation +- Summary: Rewrite design-decisions for storage v3; archive STARTER-SPEC +- Labels: sdlc-spdd, milestone-4, docs-truth +- Components: framework + +### Description + +design-decisions.md still describes agent-context/, duplicate canvases, extensions trees (SPIKE-004 M7, unfixed); STARTER-SPEC.md is a 1 881-line fossil with two inbound links. + +### Acceptance criteria (Given/When/Then) + +- Given Documentation truth and release hygiene is the active program, When this Work ID is complete, Then the acceptance criteria above are checked and the analysis/canvas (if any) match implementation. + +## GitHub + +Create the issue manually in GitHub UI, then set **Number** (and matching `github_number` frontmatter when used) and commit. + +- Number: TBD +- Title: Rewrite design-decisions for storage v3; archive STARTER-SPEC +- Labels: sdlc-spdd, milestone-4, docs-truth +- URL: + +### Description + +See this requirement and `spdd/analysis/SPIKE-005-architecture-review-analysis.md`. + +## Next Step + + /sdlc-spdd-analysis @sdlc-spdd/requirements/milestones/milestone-4/DOC-006-design-decisions-v3-and-starter-spec-archive.md diff --git a/sdlc-spdd/requirements/milestones/milestone-4/DOC-007-glossary-and-engine-cli-guide.md b/sdlc-spdd/requirements/milestones/milestone-4/DOC-007-glossary-and-engine-cli-guide.md new file mode 100644 index 00000000..48b2ab38 --- /dev/null +++ b/sdlc-spdd/requirements/milestones/milestone-4/DOC-007-glossary-and-engine-cli-guide.md @@ -0,0 +1,94 @@ +--- +work_id: "DOC-007-glossary-and-engine-cli-guide" +jira_key: "" +github_number: "" +jira_epic: "" +jira_type: "Documentation" +jira_status: "To Do" +jira_assignee: "" +jira_due_date: "" +jira_sprint: "" +milestone: "milestone-4" +priority: "P2" +size: "S" +blocks: [] +depends_on: + - "REF-005-cli-command-modules" +related: [] +--- + +# DOC-007: Glossary and end-to-end 'add a CLI command' guide + +**Work ID:** DOC-007-glossary-and-engine-cli-guide +**Milestone:** Milestone 4 — Documentation truth and release hygiene +**Status:** To Do +**Priority / size:** P2 / S +**Date:** 2026-09-13 +**Beck stage:** make it right (docs) + +## Related Work + +| Relationship | Work ID | Status | Notes | +|--------------|---------|--------|-------| +| Blocks | (none) | — | — | +| Depends On | `REF-005-cli-command-modules` | To Do | See milestone-3 | +| Related | (none) | — | — | + +## User / Business Goal + +Contributors have adapter docs but no glossary and no guide for adding a Python subcommand through parser, tests, docs, and adapters. + +## Scope + +### IN SCOPE + +- docs/glossary.md (Work ID, canvas, ledger, registry, phase, gate, capture/accept, projection, quiet mode) +- docs/contributing-engine-cli.md walking one command end to end + +### NOT IN SCOPE + +- (none) + +## Acceptance Criteria + +- [ ] Both docs linked from docs/README.md and CONTRIBUTING.md + +## Non-Goals + +- Expanding product surface (ADF, Vue3, Jira UX) under this Work ID +- Upstream PRs to embabel/guide + +## Jira + +Create the issue manually in Jira UI, then set **Key** (and matching `jira_key` frontmatter) and commit. + +- Key: TBD +- Issue type: Documentation +- Summary: Glossary and end-to-end 'add a CLI command' guide +- Labels: sdlc-spdd, milestone-4, docs-truth +- Components: framework + +### Description + +Contributors have adapter docs but no glossary and no guide for adding a Python subcommand through parser, tests, docs, and adapters. + +### Acceptance criteria (Given/When/Then) + +- Given Documentation truth and release hygiene is the active program, When this Work ID is complete, Then the acceptance criteria above are checked and the analysis/canvas (if any) match implementation. + +## GitHub + +Create the issue manually in GitHub UI, then set **Number** (and matching `github_number` frontmatter when used) and commit. + +- Number: TBD +- Title: Glossary and end-to-end 'add a CLI command' guide +- Labels: sdlc-spdd, milestone-4, docs-truth +- URL: + +### Description + +See this requirement and `spdd/analysis/SPIKE-005-architecture-review-analysis.md`. + +## Next Step + + /sdlc-spdd-analysis @sdlc-spdd/requirements/milestones/milestone-4/DOC-007-glossary-and-engine-cli-guide.md diff --git a/sdlc-spdd/requirements/milestones/milestone-4/MILESTONE-4.md b/sdlc-spdd/requirements/milestones/milestone-4/MILESTONE-4.md new file mode 100644 index 00000000..7c23db43 --- /dev/null +++ b/sdlc-spdd/requirements/milestones/milestone-4/MILESTONE-4.md @@ -0,0 +1,66 @@ +# Milestone 4 — Documentation truth and release hygiene + +## Goal + +Every document describes the system as it is: the layout `init` creates, the one engine and one persistence model of Milestone 3, current versions and current work. There is one authoring root for shipped docs, generated grounding for the three assistants, zero broken links enforced in CI, and a registry that is the truth `list-work` reports. + +**Stage:** make it right (docs). Runs in parallel with Milestone 3; items that describe purged behavior (DOC-006) wait for the corresponding REF to land. + +Analysis that opened this milestone: `spdd/analysis/SPIKE-005-architecture-review-analysis.md` (M11–M13). + +## Outcome (definition of done) + +- `grep -rn 'docs/sdlc-spdd\|agent-context/harness\|scripts/sdlc-spdd/\|work-registry.tsv' docs templates README.md CONTRIBUTING.md` returns nothing. +- `scripts/verify-doc-links.sh` reports 0 broken and runs in CI. +- `sdlc-spdd/docs/` differs from `docs/` only in the intended installed README and the research trees, and CI proves it. +- Assistant grounding files are generated from one spec + path map, same as command adapters. +- `design-decisions.md` reads as ADRs for storage v3; `STARTER-SPEC.md` is archived. +- Version is declared once; README "Current focus" points at the ROADMAP; CHANGELOG top entry is dated. +- Every Work ID with a canvas has a terminal registry event; Milestone 2 is marked complete. + +## Scope + +P0 — stop teaching the wrong layout: + +- [ ] DOC-004-install-path-vocabulary +- [ ] DOC-005-grounding-path-map-codegen +- [ ] CHORE-007-doc-links-gate + +P1 — one source, current facts: + +- [ ] CHORE-008-single-docs-authoring-root +- [ ] DOC-006-design-decisions-v3-and-starter-spec-archive +- [ ] CHORE-009-release-version-hygiene +- [ ] CHORE-010-registry-reconciliation + +P2 — contributor ergonomics: + +- [ ] DOC-007-glossary-and-engine-cli-guide + +## Linked Work + +| Work ID | Pri | Size | Requirement | Status | Notes | +|---------|-----|------|-------------|--------|-------| +| DOC-004-install-path-vocabulary | P0 | S | [requirement](DOC-004-install-path-vocabulary.md) | To Do | Docs describe the layout init actually creates | +| DOC-005-grounding-path-map-codegen | P0 | M | [requirement](DOC-005-grounding-path-map-codegen.md) | To Do | Generate assistant grounding from one path map | +| CHORE-007-doc-links-gate | P0 | M | [requirement](CHORE-007-doc-links-gate.md) | To Do | Zero broken doc links, enforced in CI | +| CHORE-008-single-docs-authoring-root | P1 | M | [requirement](CHORE-008-single-docs-authoring-root.md) | To Do | `docs/` is the only authoring root | +| DOC-006-design-decisions-v3-and-starter-spec-archive | P1 | M | [requirement](DOC-006-design-decisions-v3-and-starter-spec-archive.md) | To Do | ADRs for storage v3; archive STARTER-SPEC | +| CHORE-009-release-version-hygiene | P1 | S | [requirement](CHORE-009-release-version-hygiene.md) | To Do | One version string; current README focus | +| CHORE-010-registry-reconciliation | P1 | S | [requirement](CHORE-010-registry-reconciliation.md) | To Do | `registry.jsonl` is the truth `list-work` reports | +| DOC-007-glossary-and-engine-cli-guide | P2 | S | [requirement](DOC-007-glossary-and-engine-cli-guide.md) | To Do | Glossary and "add a CLI command" guide | + +## Iteration order + +1. DOC-004 (small, unblocks everything that quotes paths) → CHORE-007 (fix links, add the gate) → DOC-005 (generated grounding). +2. CHORE-009 and CHORE-010 any time (independent of Milestone 3). +3. CHORE-008 after CHORE-007; DOC-006 after REF-002 lands (it documents the purge). +4. DOC-007 after REF-005 (the CLI guide should describe the command-module layout). + +## SDLC-SPDD Flow + +For each work item: `/sdlc-spdd-analysis` → `/sdlc-spdd-plan` → `/sdlc-spdd-architect` → `/sdlc-spdd-code` one operation at a time → review, capture, accept, retro. + +## Session Updates + +2026-09-13 — Milestone opened from SPIKE-005 (docs findings M11–M13). diff --git a/sdlc-spdd/requirements/milestones/milestone-4/_milestone.yml b/sdlc-spdd/requirements/milestones/milestone-4/_milestone.yml new file mode 100644 index 00000000..ab7428fb --- /dev/null +++ b/sdlc-spdd/requirements/milestones/milestone-4/_milestone.yml @@ -0,0 +1,7 @@ +name: "Milestone 4 — Documentation truth and release hygiene" +number: 4 +start_date: "2026-09-13" +end_date: "" +status: "active" +related_epics: [] +notes: "Docs describe the one flow of Milestone 3: correct install vocabulary, generated grounding, zero broken links in CI, single authoring root, current ADRs, version and registry hygiene. Opened by SPIKE-005." diff --git a/sdlc-spdd/requirements/milestones/milestone-5/MILESTONE-5.md b/sdlc-spdd/requirements/milestones/milestone-5/MILESTONE-5.md new file mode 100644 index 00000000..d9f64439 --- /dev/null +++ b/sdlc-spdd/requirements/milestones/milestone-5/MILESTONE-5.md @@ -0,0 +1,23 @@ +# Milestone 5 — Make it fast (outline; not opened) + +## Goal + +Act on the measurement built in FEAT-004/005/015: optimize prompts and context loading with numbers, and finish the product surfaces that measurement needs. Per the ROADMAP posture this comes last and does not start until Milestone 3 P0 and P1 are complete. + +**Stage:** make it fast. + +## Candidate work (no Work IDs yet) + +| Candidate | Why it waits | +|-----------|--------------| +| Act on metrics — prompt and context optimization driven by `sdlc-engine context metrics` (C-COMPLY, C-CONTEXT, C-REWORK, C-MEMORY) | Needs a stable single engine (REF-003) so the measured system is the shipped system | +| Context-budget telemetry and enforcement in `resolve-agent-context` | Needs REF-010 (resolve in Python) | +| Vue3 console parity and ADF viewer as a Vue tab; retire Flask-rendered pages | Needs REF-004 and REF-009 | +| Resume or close SPIKE-001 (Guide DICE hybrid) and SPIKE-002 (local models) with a go/no-go | Needs REF-007 (single Guide transport) and FEAT-017 IR baseline | +| Journal n ≥ 3 for TEST-002 (assistant behavior eval) | TEST-001 stop rule; independent of engineering work | + +## Opening criteria + +- Milestone 3 P0 and P1 checked off in `MILESTONE-3.md`. +- Milestone 4 P0 checked off (docs describe the system being measured). +- A SPIKE requirement that turns this outline into Work IDs with a REASONS canvas program plan. diff --git a/sdlc-spdd/requirements/milestones/milestone-5/_milestone.yml b/sdlc-spdd/requirements/milestones/milestone-5/_milestone.yml new file mode 100644 index 00000000..597ca412 --- /dev/null +++ b/sdlc-spdd/requirements/milestones/milestone-5/_milestone.yml @@ -0,0 +1,7 @@ +name: "Milestone 5 — Make it fast" +number: 5 +start_date: "" +end_date: "" +status: "planned" +related_epics: [] +notes: "Outline only. Act on FEAT-004/005/015 metrics, context-budget telemetry, Vue3 parity, SPIKE-001/002 go/no-go. Opens after Milestone 3 P0/P1 and Milestone 4 P0." diff --git a/sdlc-spdd/session-notes/2026-09-13.md b/sdlc-spdd/session-notes/2026-09-13.md new file mode 100644 index 00000000..91778c09 --- /dev/null +++ b/sdlc-spdd/session-notes/2026-09-13.md @@ -0,0 +1,28 @@ +# Session notes — 2026-09-13 + +## Intent + +Full architectural review of source, tests, and docs to find improvement areas and open new milestones; then begin fixing. + +## Owner decisions recorded this session + +- **No competing versions.** Python `sdlc-engine` is the only engine; storage v3 is the only persistence model; docs, tests, and code must describe the same flow. +- **Archive is not a contract to preserve two ways.** Purge the "move to `spdd/*/archive/`" behavior and all pre-v3 compatibility (legacy `agent-context/` layouts, `work-registry.tsv`, feature mirrors, migration shims, legacy-layout archiving on upgrade). Losing pre-v3 context is accepted. + +## Outcome + +- Analysis: `spdd/analysis/SPIKE-005-architecture-review-analysis.md` (measured baseline + 14 findings with file:line evidence) +- Milestone 3 — One flow on storage v3: `requirements/milestones/milestone-3/` (16 Work IDs incl. SPIKE-005) +- Milestone 4 — Documentation truth and release hygiene: `requirements/milestones/milestone-4/` (8 Work IDs) +- Milestone 5 — Make it fast: outline only +- Task lists: `spdd/tasks/milestone-3-one-flow.md`, `spdd/tasks/2026-10-monthly-goals.md` (Obsidian-ready) +- ROADMAP updated; Milestone 2 `_milestone.yml` marked complete +- First P0 fixes started on the SPIKE-005 branch (see CHANGELOG `[Unreleased]`) + +## Baseline measured + +281 unit / 128 integration / 87 research tests green; 66 % unit coverage; 67 functions CCN > 15; 9 undefined names in `db_query.py`; 34 shellcheck findings (7 errors); 95 / 959 doc links broken; tags `v2.0.0a7/a8` vs declared `2.0.0a6`. + +## Verdict (one line) + +The framework works and its claims are honest; what remains is deleting the second copy of everything. diff --git a/sdlc-spdd/spdd/analysis/SPIKE-005-architecture-review-analysis.md b/sdlc-spdd/spdd/analysis/SPIKE-005-architecture-review-analysis.md new file mode 100644 index 00000000..044b2df4 --- /dev/null +++ b/sdlc-spdd/spdd/analysis/SPIKE-005-architecture-review-analysis.md @@ -0,0 +1,230 @@ +# Analysis: SPIKE-005 — Full architectural review (source, tests, docs) + +**Work ID:** SPIKE-005-architecture-review +**Date:** 2026-09-13 +**Beck stage:** make it right (engineering consolidation) — opens Milestones 3, 4, 5 +**Inputs:** whole tree at `main` `8f7fef3`; four parallel read-only reviews (engine, shell/templates, tests/CI, docs) plus measured baselines below. + +## Scope Lock-In + +### In scope + +- `engine/src/sdlc_engine` (64 modules, ~21.3k LOC), `scripts/` + `scripts/lib/` (~10.5k LOC bash), `templates/` (installed runtime, adapters, harness), `spec/commands` +- `engine/tests_*`, `tests/`, `.github/workflows` (26 workflows) +- `README.md`, `CHANGELOG.md`, `CONTRIBUTING.md`, `TESTING.md`, `STARTER-SPEC.md`, `docs/`, `sdlc-spdd/docs/`, grounding files, dogfood governance artifacts +- Output: this analysis, Milestone 3/4/5 definitions, Work ID requirement stubs, task lists, ROADMAP pointers + +### Not in scope + +- Implementing the Work IDs (each follows analysis → plan → architect → code) +- Research claims (Milestone 2 froze those; DOC-001 allow-list stands) +- Upstream PRs to `embabel/guide` (forbidden by rule) +- Product features (ADF library, Vue3 tabs) beyond noting their engineering debt + +### Deferred + +- Make-it-fast items (act on metrics, context-budget telemetry, Vue3 parity) → Milestone 5 outline only +- Resuming SPIKE-001/SPIKE-002 (Guide DICE, local models) + +## Measured baseline (2026-09-13, this checkout) + +| Measure | Value | Command | +|---------|-------|---------| +| Unit tests | 281 passed, 34 s | `pytest engine/tests_unit` | +| Integration tests | 128 passed, 3.6 s | `pytest engine/tests_integration` | +| Research tests | 87 passed | `pytest tests/research` | +| Bash harnesses | 30 scripts, ~221 cases | `tests/test-*.sh` | +| Unit coverage of `sdlc_engine` | **66.4 %** of 10 551 statements | `--cov=sdlc_engine` | +| Modules under 30 % unit coverage | `viewer/app.py` 0 %, `viewer/html_adf.py` 6 %, `viewer/adf_html.py` 8 %, `installer/dashboard.py` 14 %, `viewer/store.py` 20 %, `installer/app.py` 26 % | coverage JSON | +| Functions with CCN > 15 | **67** of 766 | `lizard -l python engine/src` | +| Functions with CCN > 10 or NLOC > 80 | **153** | `lizard -C 10 -L 80 -w` | +| Longest functions | `installer/app.create_app` 1 056 lines; `cli_parser.build_parser` 679; `viewer/app.create_app` 530 | lizard | +| Highest CCN | 97 (`cli_commands.py`), 85 (`storage_migrate.run`), 75 (`db_rebuild.py`), 64 (`viewer/html_adf.handle_starttag`), 51 (`viewer/adf_html._block`) | lizard CSV | +| Ruff, configured rules (`F401,F811`) on `engine/src scripts` | clean | CI step | +| Ruff `F821` undefined names in `engine/src` | **9** (all in `db_query.py`) | `ruff --select F821` | +| Ruff broad (`E,F,W,B,UP,SIM,C901,PLR09*`) | 190 findings (48 C901, 25 too-many-args, 25 too-many-branches) | `ruff --statistics` | +| Shellcheck (`-S warning`) over scripts/templates/tests | **34** findings, **7 errors** | `shellcheck -f gcc` | +| Doc links | **95 broken / 959 checked** | `scripts/verify-doc-links.sh` | +| Version | `pyproject`/`__init__`/README say `2.0.0a6`; git tags `v2.0.0a7`, `v2.0.0a8` exist; CHANGELOG top is `[Unreleased]` | `git tag` | +| Dogfood registry | 11 of 13 Milestone 2 Work IDs have **no** claim/release event; `SPIKE-004`, `DOC-001` still `active` | `registry.jsonl` | + +The largest CLI handlers by lizard are `cmd_issues` (CCN 27, 90 NLOC), `cmd_db` (23), `cmd_template` (20), `cmd_local` (18); `workflow.gate_check` is CCN 25 / 94 NLOC. Treat "handlers over CCN 15" as the finding, not a single number. + +## Recommendation + +The framework is functionally complete ("make it work" done) and Milestone 2 made its research claims honest. What remains is **engineering consolidation**: the codebase carries two behavioral cores (bash and Python), several god modules, duplicated helpers, gates that prove the checker rather than the diff, and a documentation set whose install-path vocabulary no longer matches what `init` writes. None of this blocks a user today; all of it taxes every future change. + +Open three milestones: + +- **Milestone 3 — One flow on storage v3** (make it right, code/tests/CI): purge pre-v3 compatibility, retire the bash workflow twin, real lint/complexity gates, split god modules, hermetic test layers, consolidated CI, hardened local web surfaces. +- **Milestone 4 — Documentation truth and release hygiene** (make it right, docs): one authoring root, correct install vocabulary, generated grounding, doc-link gate, current ADRs, version/registry hygiene. +- **Milestone 5 — Make it fast** (outline only): act on FEAT-004/005/015 metrics, context-budget telemetry, Vue3 parity. Not opened until Milestone 3 P0/P1 land. + +## Major findings (evidence → Work ID) + +### M1. Two behavioral cores: bash workflow twin still maintained + +`templates/agent-context/sdlc-workflow.sh` (2 089 LOC) and `sdlc-team-registry.sh` (981 LOC) reimplement `workflow.py`, `registry.py`, `pointer.py`, and a shell `gate` fallback (`sdlc_workflow_gate`, ledger checks via JSON regex at `sdlc-workflow.sh:904–928`). Gate tables carry "keep in sync" comments (`sdlc-workflow.sh:347–348` ↔ `phases.py`). The gate path calls bare `python3` (`:873–887`) while the dispatcher enforces 3.12 via `scripts/lib/python.sh`; a wrong interpreter silently falls back to the shell gate. `cli_commands.cmd_shell` only bridges `scripts/`, not installed `sdlc-spdd/scripts/`. + +→ **REF-003-retire-bash-workflow-dual-path** + +### M2. Pre-v3 compatibility is still load-bearing, and it forked the archive contract + +Storage v3 (`docs/storage-v3.md`) is the only persistence model the project intends to support: committed `spdd/memory/lessons.jsonl` + `registry.jsonl`, gitignored `.sdlc/` runtime, `sdlc-spdd/` as the single home. Yet the code still carries the pre-v3 world: `project.py:57,129–133` falls back to root layouts and `agent-context/harness`; `registry.py` reads the legacy TSV registry; `storage_migrate.py` (317 LOC), `agent_context_upgrade.py` (93), `context_model.py` legacy markdown parsers (`:89+`), and `installer/rollback.py` exist only to move old trees; `upgrade-project.sh` has 43 legacy-path references and archives leftovers under `.sdlc/legacy-layout-archive/`; 40 test files assert legacy behaviors. + +The archive command shows the cost. Shell `sdlc_team_archive_work` deletes artifacts (`sdlc-team-registry.sh:626–628,670`, "git history is the audit trail"); Python `ArchiveService._move` relocates them into `spdd/*/archive/` (`archive.py:60–79`) and still sweeps `agent-context/sessions`. `SDLC_ENGINE=auto` routes `archive` to Python, so users get "move" while `docs/storage-v3.md`, `TESTING.md:234`, `ROADMAP.md`, and `MILESTONE-1.md` describe "delete". `tests/test-archive-work.sh:90–99` asserts removal; `engine/tests_unit/test_registry_archive.py:47,96` asserts archive files. Two green suites prove two contracts. + +Decision (owner, 2026-09-13): pre-v3 context may be lost. Purge legacy layouts, migrations, and archive folders; one data model, one flow. + +### M3. Latent `NameError`s shipped because the lint gate is too narrow + +`db_query.py` references `REL_AREA`, `NODE_AREA`, `NODE_LESSON`, `REL_ABOUT`, `NODE_REQUIREMENT`, `REL_REASONS`, `NODE_CANVAS` (lines 164–205) and `_utc_now` (line 425) without importing them. Reproduced: `LocalIndex(project, path).export_sql(...)` raises `NameError: name '_utc_now' is not defined`; `sdlc-engine db export` reaches it via `cli_commands.py:777`. `context_linked_to_section` has no caller. CI runs `ruff --select F401,F811` only (`test-sdlc-engine.yml:45`), so pyflakes' undefined-name check never runs. + +→ **BUG-001-db-query-undefined-names**, **CHORE-004-lint-and-complexity-gates-real** + +### M4. Complexity gate proves the checker, not the PR + +`scripts/check-complexity.py` implements CCN > 10 / NLOC > 80 / CCN-rise on changed files, but CI only runs `scripts/test-check-complexity.sh` (a synthetic repo) and the unit test that wraps it (`test-sdlc-engine.yml:46–49`). No workflow runs `check-complexity.py --base origin/main` on the actual diff. Shellcheck is not run anywhere (only `bash -n`). + +→ **CHORE-004-lint-and-complexity-gates-real** + +### M5. God modules and CLI-as-monolith + +`installer/app.py:154–1209` is one nested `create_app` with every `/api/*` route; `cli_parser.py:9–40` imports every `cmd_*`, so `import sdlc_engine.cli` loads `ContextStore`, `LocalIndex`, `IssueSyncService`, `SunsetService`; handlers print as their API (~100 `print` calls) and `cli.py:61–63` catches bare `Exception`. `canvas.py:668,674,739` raises `SystemExit` from library code. `__init__.py:3–6` imports workflow/registry/archive at package import. + +→ **REF-004-split-installer-blueprints**, **REF-005-cli-command-modules** + +### M6. Storage records are ad-hoc and appends are not atomic + +`LessonRecord` is typed (`lessons_ledger.py:58–144`) but registry events are `dict[str,str]` (`registry.py:85–96`) and `ContextStore`/`PersistResult` are `dict[str, Any]` (`context_store.py:68–87,279–322`). `LEDGER_KINDS` is defined twice (`context_model.py:20`, `lessons_ledger.py:43`). Stage and registry appends are plain `open(..., "a")` with no lock (`lessons_ledger.py:192–193`, `registry.py:144–145`); `io_util.save_json_dict` is non-atomic and bypassed by `persistence.save_config`, `integration_config`, `installer/guide.py:179`. + +→ **REF-006-storage-records-and-atomic-appends** + +### M7. Guide HTTP transport triplicated + +`GuideClient._request` (`guide_client.py:60–97`) coexists with raw `urllib` in `ContextStore.project_to_guide/guide_work/guide_stats` (`context_store.py:279–322`), `installer/guide_compliance.py:87–125`, `installer/guide_ops.py:26–45`. Base-URL resolution is inlined three times. Hardcoded Neo4j default password in `installer/guide.py:132` and `guide_ops.py:140`. + +→ **REF-007-single-guide-transport** + +### M8. Local web surfaces assume localhost but offer `--lan` + +`viewer/store.py:22–48` browses the full filesystem by design; `--lan` binds `0.0.0.0` (`cli_commands.py:902,920`, `viewer/__main__.py:29`); no CSRF/auth on mutating POSTs; `/api/run` shells install/upgrade (`installer/app.py:230–255`). `viewer/pages.py` (1 190 LOC) is a second frontend as Python strings; the Vue3 console pattern is not applied to the ADF viewer. ADF codec logic is duplicated between `jira_format.py` and `viewer/adf_html.py`/`html_adf.py` (`_is_gwt_list_item` at `jira_format.py:399` and `adf_html.py:65`). + +→ **REF-008-console-viewer-hardening**, **REF-009-adf-codec-and-viewer-assets** + +### M9. Test layers are porous; CI is 26 copy-pasted workflows + +Unit suite is documented as "no Flask" (`TESTING.md:106`, `engine/tests_unit/README.md`) but `test_vue3_console_serve.py`, `test_installer_templates_api.py`, `test_installer_playground.py` use Flask test clients and are also counted in the Suite 2 coverage job. Grab-bag files (`test_hard_review_gaps.py`, `test_remaining_cleanup_slices.py`, `test_installer_coverage_gaps.py`) are the only homes for `agent_context_upgrade` and `quiet` coverage. Unit tests shell out to `capture-session-memory.sh`. 26 workflows carry ~31 `checkout`, ~10 `setup-python`, ~11 `pip install -e ./engine` with no `workflow_call`/composite action; installer ≥90 % coverage runs twice; pointer/archive have standalone workflows and also run inside `test-sdlc-workflow.yml`; `validate-command-adapters` ⊂ `validate-command-spec-generation`; Playwright push paths are narrower than PR paths (`test-e2e-playwright.yml:4–16`). `TESTING.md:60,98` says 158 unit tests (actual 276). + +→ **CHORE-005-ci-reusable-workflows**, **TEST-004-hermetic-unit-suite-and-fixtures** + +### M10. Dogfood adapters lag the templates they were installed from + +After applying the install path rewrite, `.cursor/commands/sdlc-next.md` lacks the optional DIF step 11 and `.claude/commands/sdlc-spdd-code.md` lacks Kasana I1 steps 16–20 (Validation, stop-on-repeat-failure, diff-scope) that `templates/` and `spec/commands/*.spec.md` carry. `validate-sdlc-spdd-adapters.yml` did not catch it. `/sdlc-spdd-quick` exists in all three adapter trees with **no** `spec/commands/*.spec.md`, so the generator is not its source of truth. + +→ **CHORE-006-dogfood-adapters-and-quick-spec** + +### M11. Documentation teaches a layout `init` does not create + +`init-project.sh` installs docs to `sdlc-spdd/docs/`; `docs/installing-into-your-project.md:67,220`, `docs/README.md:3,57,155`, `CONTRIBUTING.md:53,94,162` say `docs/sdlc-spdd/`. `docs/first-day-with-sdlc-spdd.md:66` requires "`agent-context/` exists". Shipped grounding templates use `./scripts/sdlc-spdd/sdlc.sh` and `agent-context/harness/` (`templates/cursor/rules/sdlc-spdd.mdc:78–84`, `templates/claude/CLAUDE.md`, `templates/copilot/copilot-instructions.md`), paths that do not exist in a target. `CONTRIBUTING.md:145` cites `work-registry.tsv`. Workflow step count is 16 / "15" / "13" across `docs/workflow.md`, `docs/README.md:86`, `sdlc-spdd/docs/README.md:40`. `design-decisions.md` still describes `agent-context/` + duplicate canvases (SPIKE-004 M7, unfixed). `STARTER-SPEC.md` (1 881 lines) is referenced only by CHANGELOG and design-decisions. + +→ **DOC-004-install-path-vocabulary**, **DOC-005-grounding-path-map-codegen**, **DOC-006-design-decisions-v3-and-starter-spec-archive**, **DOC-007-glossary-and-engine-cli-guide** + +### M12. Two docs trees, 95 broken links, no link gate + +`docs/` (61 md) is the authoring root; `sdlc-spdd/docs/` (45 md) is the dogfood install copy but 5 files have diverged (`storage-v3.md`, `triple-path-context.md`, `spdd-compliance.md`, `adf-template-library-and-vue3-console.md`, `README.md` by design). Shipped copies link orchestrator-only docs (`guide-flow.md`, `dice-projection-runbook.md`, `diagrams/*.svg`) that `shipped-docs-boundary.sh` excludes, so they 404 after install. `verify-doc-links.sh` is not in CI. + +→ **CHORE-007-doc-links-gate**, **CHORE-008-single-docs-authoring-root** + +### M13. Release and registry hygiene + +Tags `v2.0.0a7`/`v2.0.0a8` exist while every declared version is `2.0.0a6` and `test_engine_shared.py:30` pins it; README "Latest release" and "Current focus" (#264–#270, #114) are stale; ROADMAP links archived canvases; `_milestone.yml` for Milestone 2 still says `planned`. `spdd/memory/registry.jsonl` has no events for 11 Milestone 2 Work IDs and leaves `SPIKE-004`/`DOC-001` active; `list-work` reports "done" from canvas status, so the registry is not the truth its docs say it is. + +→ **CHORE-009-release-version-hygiene**, **CHORE-010-registry-reconciliation** + +### M14. Shell-only session stack and issue-sync mega-service (P2) + +`start-agent-session.sh` (625), `capture-session-memory.sh` (504), `resolve-agent-context.sh` (563), `create-work-from-milestone.sh` (498) have no Python twin; `accept` has two verbs (`sdlc.sh accept` shell, `sdlc-engine context accept`). `issues.py` (1 209 LOC) covers draft/push/pull/ADF/link in one class. `create-work-from-milestone.sh` inlines its own canvas body instead of `templates/reasons-canvas`. + +→ **REF-010-pythonize-session-and-capture**, **REF-011-split-issue-sync-adapters** + +## Minor findings (fold into the Work IDs above) + +- `templates/cursor/sdlc-spdd-code.md:56` references `./scripts/sdlc-spdd/resolve-context-backend.sh` (nonexistent path) — DOC-005. +- `docs/engine-v2.md:66–70` says the default engine is shell; `scripts/sdlc.sh` default is `auto` — DOC-004. +- Shellcheck errors: `resolve-agent-context.sh:343` SC2066 (quoted loop runs once), `setup-engine-venv.sh:102–103` SC1087, `tests/test-command-specs.sh:333–355` SC2218, `tests/test-sdlc-engine-shim.sh:63` SC2144 — CHORE-004. +- `ls -1t` for session rotation (`capture-session-memory.sh:220`, `start-agent-session.sh:255,597`) — REF-010. +- `context_model.iter_code_areas`, `context_model.extract_memory_facts`, `installer/guide.resolve_guide_home` have no callers — REF-006. +- `issue_tracker.py` is a re-export shim; `cli.py:9–46` re-exports all `cmd_*` "for compatibility" — REF-005. +- `sdlc-spdd/docs/research/unattended-iterations.md` has no inbound links — CHORE-008. +- `tests/eval` has one test; `pyproject` `testpaths=["tests_unit"]` makes bare `pytest` silently skip two suites — TEST-004. + +## Strengths (do not regress) + +- Storage v3 is directionally right: one committed ledger, staged captures, derived projections, soft-fail Guide. +- Flask is consistently lazy-imported; runtime has zero dependencies. +- `timeutil.utc_now` adoption is complete; `LessonRecord`, `VerifyReceipt`, `ProcessMetrics`, `RegistryRow` are good typed models to extend. +- Recent PRs (#294–#307) already removed prose-grep and MagicMock-as-proof anti-patterns; `test_live_graph_required_ci.py` is the right meta-test pattern. +- Spec → adapter generation with `--check` in CI works for templates; the gap is only the dogfood copy. +- Diagrams have source + SVG + render script + validation workflow. + +## Code areas (for the implementation Work IDs) + +| Area | Paths | +|------|-------| +| engine-cli | `engine/src/sdlc_engine/{cli,cli_parser,cli_commands}.py` | +| engine-storage | `lessons_ledger.py`, `registry.py`, `pointer.py`, `context_store.py`, `context_model.py`, `db*.py`, `persistence.py`, `io_util.py` | +| engine-web | `installer/app.py`, `installer/dashboard.py`, `viewer/*.py`, `console-ui/` | +| engine-guide | `guide_client.py`, `guide_query.py`, `installer/guide*.py` | +| shell-workflow | `templates/agent-context/sdlc-*.sh`, `scripts/sdlc.sh`, `scripts/lib/python.sh` | +| shell-session | `scripts/{start-agent-session,capture-session-memory,resolve-agent-context,create-work-from-milestone,accept-lessons}.sh` | +| ci | `.github/workflows/*.yml`, `scripts/check-complexity.py`, `scripts/run-test-suites.sh` | +| docs | `docs/`, `sdlc-spdd/docs/`, `templates/project-docs/`, `templates/{cursor,claude,copilot}` grounding, root `*.md` | + +## Domain Keywords + +- one-flow +- storage-v3 +- purge-legacy +- bash-twin +- lint-gate +- complexity-gate +- shellcheck +- god-module +- atomic-append +- guide-transport +- doc-links +- install-vocabulary +- release-hygiene + +## Code Areas + +- engine/src/sdlc_engine/cli_commands.py +- engine/src/sdlc_engine/cli_parser.py +- engine/src/sdlc_engine/archive.py +- engine/src/sdlc_engine/project.py +- engine/src/sdlc_engine/registry.py +- engine/src/sdlc_engine/storage_migrate.py +- engine/src/sdlc_engine/db_query.py +- engine/src/sdlc_engine/installer/app.py +- engine/src/sdlc_engine/viewer/ +- templates/agent-context/sdlc-workflow.sh +- templates/agent-context/sdlc-team-registry.sh +- scripts/sdlc.sh +- scripts/upgrade-project.sh +- .github/workflows/ +- docs/ +- sdlc-spdd/docs/ + +## Assumptions + +- Storage v3 is the only supported persistence model. Pre-v3 layouts (`agent-context/` trees, `work-registry.tsv`, feature mirrors, `spdd/*/archive/`) are not migrated; they are ignored or removed. Losing pre-v3 context is accepted. +- Python `sdlc-engine` is the behavioral core going forward (REF-001); bash remains for install/upgrade packaging. +- Version bump and tag reconciliation are a human release decision; CHORE-009 prepares it, it does not cut a release. +- Milestone 5 stays an outline until Milestone 3 P0 and P1 are complete (roadmap posture: do not optimize before it is right). + +## Next + +1. `/sdlc-spdd-plan @sdlc-spdd/requirements/milestones/milestone-3/SPIKE-005-architecture-review.md` — canvas as the program plan (sequence, stop rules). +2. Start Milestone 3 P0: BUG-001, CHORE-004, CHORE-006 (small, test-first), then REF-002 T01 (archive deletes; drop `spdd/*/archive/` and legacy session dirs). +3. Start Milestone 4 P0 in parallel (docs-only, no engine risk): DOC-004, CHORE-007. diff --git a/sdlc-spdd/spdd/memory/registry.jsonl b/sdlc-spdd/spdd/memory/registry.jsonl index 1aafd130..0527f119 100644 --- a/sdlc-spdd/spdd/memory/registry.jsonl +++ b/sdlc-spdd/spdd/memory/registry.jsonl @@ -18,3 +18,17 @@ {"event": "archive", "work_id": "SPIKE-002-local-llm-and-embedding-format", "status": "archived", "phase": "archive", "operation": "", "owner": "John Menke", "note": "archived:forced", "ts": "2026-08-09T01:03:37Z"} {"event": "claim", "work_id": "SPIKE-004-academic-contribution-bar", "status": "active", "phase": "plan", "operation": "", "owner": "Cursor Agent", "note": "", "ts": "2026-09-06T16:02:54Z"} {"event": "claim", "work_id": "DOC-001-research-questions-and-constructs", "status": "active", "phase": "code", "operation": "T02", "owner": "Cursor Agent", "note": "", "ts": "2026-09-06T16:13:01Z"} +{"event": "refresh", "work_id": "CHORE-003-dogfood-ledger", "status": "done", "phase": "sync", "operation": "", "owner": "Cursor Agent", "note": "canvas Final Status: Complete", "ts": "2026-09-13T23:05:53Z"} +{"event": "refresh", "work_id": "DOC-001-research-questions-and-constructs", "status": "done", "phase": "code", "operation": "T02", "owner": "Cursor Agent", "note": "canvas Final Status: Complete", "ts": "2026-09-13T23:05:53Z"} +{"event": "refresh", "work_id": "DOC-002-related-work-map", "status": "done", "phase": "sync", "operation": "", "owner": "Cursor Agent", "note": "canvas Final Status: Complete", "ts": "2026-09-13T23:05:53Z"} +{"event": "refresh", "work_id": "DOC-003-replication-package", "status": "done", "phase": "sync", "operation": "", "owner": "Cursor Agent", "note": "canvas Final Status: Complete", "ts": "2026-09-13T23:05:53Z"} +{"event": "refresh", "work_id": "FEAT-014-semantic-canvas-validation", "status": "done", "phase": "sync", "operation": "", "owner": "Cursor Agent", "note": "canvas Final Status: Complete", "ts": "2026-09-13T23:05:53Z"} +{"event": "refresh", "work_id": "FEAT-015-first-class-metrics", "status": "done", "phase": "sync", "operation": "", "owner": "Cursor Agent", "note": "canvas Final Status: Complete", "ts": "2026-09-13T23:05:53Z"} +{"event": "refresh", "work_id": "FEAT-016-intent-code-traceability", "status": "done", "phase": "sync", "operation": "", "owner": "Cursor Agent", "note": "canvas Final Status: Complete", "ts": "2026-09-13T23:05:53Z"} +{"event": "refresh", "work_id": "FEAT-017-retrieval-ir-eval", "status": "done", "phase": "sync", "operation": "", "owner": "Cursor Agent", "note": "canvas Final Status: Complete", "ts": "2026-09-13T23:05:53Z"} +{"event": "refresh", "work_id": "REF-001-engine-single-source", "status": "done", "phase": "sync", "operation": "", "owner": "Cursor Agent", "note": "canvas Final Status: Complete", "ts": "2026-09-13T23:05:53Z"} +{"event": "refresh", "work_id": "SPIKE-004-academic-contribution-bar", "status": "done", "phase": "plan", "operation": "", "owner": "Cursor Agent", "note": "canvas Final Status: Complete", "ts": "2026-09-13T23:05:53Z"} +{"event": "refresh", "work_id": "TEST-001-evaluation-protocol", "status": "done", "phase": "sync", "operation": "", "owner": "Cursor Agent", "note": "canvas Final Status: Complete", "ts": "2026-09-13T23:05:53Z"} +{"event": "refresh", "work_id": "TEST-002-assistant-behavior-eval", "status": "done", "phase": "sync", "operation": "", "owner": "Cursor Agent", "note": "canvas Final Status: Complete", "ts": "2026-09-13T23:05:53Z"} +{"event": "refresh", "work_id": "TEST-003-cretrieve-roundtrip", "status": "done", "phase": "sync", "operation": "", "owner": "Cursor Agent", "note": "canvas Final Status: Complete", "ts": "2026-09-13T23:05:53Z"} +{"event": "claim", "work_id": "SPIKE-005-architecture-review", "status": "active", "phase": "plan", "operation": "", "owner": "Cursor Agent", "note": "branch:cursor/architecture-review-milestones-8088", "ts": "2026-09-13T23:25:54Z"} diff --git a/sdlc-spdd/spdd/tasks/2026-10-monthly-goals.md b/sdlc-spdd/spdd/tasks/2026-10-monthly-goals.md new file mode 100644 index 00000000..46eebe76 --- /dev/null +++ b/sdlc-spdd/spdd/tasks/2026-10-monthly-goals.md @@ -0,0 +1,124 @@ +--- +title: SDLC-SPDD — October 2026 goals (one flow on storage v3) +tags: [sdlc-spdd, milestone-3, milestone-4, goals, one-flow] +source: sdlc-spdd/spdd/analysis/SPIKE-005-architecture-review-analysis.md +created: 2026-09-13 +--- + +# October 2026 goals — one engine, one persistence model, aligned docs/tests/code + +Thesis for the month: **no competing versions**. Python `sdlc-engine` is the only engine; storage v3 (`spdd/memory/lessons.jsonl` + `registry.jsonl`, `.sdlc/` runtime, `sdlc-spdd/` home) is the only persistence model; docs, tests, and code describe the same flow. Pre-v3 context may be lost. + +Each task below is one Work ID (requirement stub under `sdlc-spdd/requirements/milestones/milestone-3|4/`). Sizes: S ≤ 1 PR, M = 2–4 PRs, L = a sequence of one-operation PRs. Checkboxes are the acceptance greps from the stubs, so a task is done when the grep says so, not when it feels done. + +## Week 1 — gates and quick wins (P0, small) #week1 + +- [ ] **BUG-001** Fix `db_query.py` undefined names (`export_sql` raises `NameError` today) — S + - [ ] `ruff check --select F821 engine/src` clean + - [ ] unit test for `LocalIndex.export_sql` on a temp project + - [ ] `context_linked_to_section` tested or deleted +- [ ] **CHORE-004** Lint / complexity / shellcheck gates run on the real diff — M + - [ ] pyproject ruff `select = ["F", "E9"]`; CI uses the pyproject rule set + - [ ] `check-complexity.py --base origin/main` runs on PR diffs in `test-sdlc-engine.yml` + - [ ] shellcheck workflow (`-S error` first) over `scripts/`, `scripts/lib/`, `templates/agent-context/`, `tests/*.sh`; fix SC2066 / SC1087 / SC2218 / SC2144 + - [ ] `test_quality_gates.py` asserts the new rule set +- [ ] **CHORE-006** Regenerate dogfood adapters; spec for `/sdlc-spdd-quick` — S + - [ ] `.cursor/commands`, `.claude/commands`, `.github/prompts` diff clean vs path-rewritten templates + - [ ] `spec/commands/lifecycle-quick.spec.md` exists; `generate-command-adapters.sh --check` covers it + - [ ] `validate-sdlc-spdd-adapters` fails on stale dogfood packs +- [ ] **DOC-004** Docs describe the layout `init` creates — S + - [ ] `grep -rn 'docs/sdlc-spdd' docs README.md CONTRIBUTING.md` empty + - [ ] first-day checklist verified against a fresh `init` in `/tmp` + - [ ] `work-registry.tsv` → `registry.jsonl`; one workflow step count +- [ ] **CHORE-009** One version string; README focus points at ROADMAP — S + - [ ] `test_engine_shared` compares `__version__` to pyproject without a literal + - [ ] CHANGELOG top entry dated; tag ↔ version ↔ CHANGELOG checklist in CONTRIBUTING + - [ ] ROADMAP drops links to archived canvases +- [ ] **CHORE-010** Registry reconciliation — S + - [ ] terminal registry event for every Milestone 2 Work ID (via `sdlc.sh`, never hand-edit) + - [ ] `list-work` shows registry vs canvas state and flags MISMATCH + - [ ] Milestone 2 `_milestone.yml` status `complete` + +## Weeks 2–3 — the purge and the single engine (P0, large) #week2 #week3 + +- [ ] **REF-002** Purge pre-v3 persistence compatibility — L + - [ ] `archive` deletes canvas/analysis/review/sync/session/state; no `spdd/*/archive/`; unit + bash tests assert the same contract + - [ ] `project.py` has no root-layout or `agent-context/harness` fallback + - [ ] `registry.py` reads `registry.jsonl` only (no TSV) + - [ ] `storage_migrate.py`, `agent_context_upgrade.py`, `context_model.py` legacy parsers, `installer/rollback.py` legacy-layout code deleted with their tests + - [ ] `upgrade-project.sh` / `verify-project-install.sh` / `framework-install.sh` refuse legacy sprawl with a message; no `legacy-layout-archive` + - [ ] `grep -r 'agent-context\|work-registry' engine/src scripts templates` returns only the folder name `templates/agent-context/` + - [ ] `TESTING.md`, `docs/storage-v3.md` have no legacy sections +- [ ] **REF-003** Retire the bash workflow twin — L + - [ ] `SDLC_ENGINE` / `SDLC_GATE_ENGINE` removed; `sdlc.sh` errors with an install hint when Python engine is missing + - [ ] `templates/agent-context/` has no `sdlc-workflow.sh`, `sdlc-team-registry.sh`, `sdlc-pointer.sh` + - [ ] gate path uses `resolve_engine_python` (`SDLC_PY`), never bare `python3` + - [ ] `cmd_shell` bridges `sdlc-spdd/scripts` in installed targets + - [ ] bash harnesses pass against the Python-only dispatcher or are replaced by pytest + - [ ] shipped script LOC down ≥ 3 000; `docs/engine-v2.md`, `TESTING.md`, README say one engine +- [ ] **CHORE-007** Zero broken doc links, enforced in CI — M + - [ ] in-tree broken links fixed (hub → ROADMAP, `agent-context/README`, milestone files) + - [ ] `verify-doc-links.sh` resolves shipped docs against the installed layout + - [ ] `verify-doc-links.sh` reports 0 broken and runs in a docs workflow +- [ ] **DOC-005** Generate assistant grounding from one path map — M + - [ ] `spec/grounding.spec.md` + path map → `.mdc`, `CLAUDE.md`, `copilot-instructions.md` + - [ ] no `scripts/sdlc-spdd/` or `agent-context/harness` in `templates/` + - [ ] generation is idempotent and `--check`ed in CI + +## Week 4 — make the one flow maintainable (P1) #week4 + +- [ ] **REF-004** Split `installer/app.py` `create_app` (1 056 lines) into blueprints — M + - [ ] no function in `installer/` over 80 NLOC or CCN 10 + - [ ] integration + Vue3 Playwright pass unchanged; installer coverage ≥ 90 % +- [ ] **REF-005** One module per CLI command; result objects, not prints — M + - [ ] `import sdlc_engine.cli` loads no Flask / sqlite3 / urllib (test) + - [ ] no `cmd_*` over CCN 10; no `SystemExit` in library modules + - [ ] `cli.py` re-exports and `issue_tracker.py` shim deleted +- [ ] **REF-006** Typed storage records; atomic, locked JSONL appends — M + - [ ] `RegistryEvent`, `PersistResult` dataclasses; one `LEDGER_KINDS` + - [ ] `io_util.append_jsonl_atomic` with lock used by ledger stage and registry + - [ ] concurrency test: 20 parallel captures → 20 valid lines +- [ ] **TEST-004** Hermetic unit suite; shared installed-target fixture — M + - [ ] unit suite imports no Flask (test); Flask tests moved to `tests_integration` + - [ ] `*gaps*` / `*slices*` test files dissolved into per-module tests + - [ ] `installed_target` fixture shared by unit / integration / research + - [ ] `TESTING.md` counts match `grep -c 'def test_'` +- [ ] **CHORE-005** 26 workflows → ≤ 10 with a reusable setup — M + - [ ] `workflow_call` setup (checkout, py3.12, engine install, optional node/playwright) + - [ ] e2e push/PR path filters aligned; duplicate installer-cov job dropped; adapter validators merged + - [ ] mapping table old job → new job in `TESTING.md` + +## Carry into November (P1/P2) #next-month + +- [ ] **REF-007** `GuideClient` is the only Guide HTTP path; no hardcoded Neo4j password — S +- [ ] **REF-008** Threat model + hardening for console/viewer (`--lan` token, viewer root allow-list, CSRF) — M +- [ ] **CHORE-008** `docs/` is the only authoring root; `sdlc-spdd/docs` generated; CI drift check — M +- [ ] **DOC-006** `design-decisions.md` rewritten as storage v3 ADRs; `STARTER-SPEC.md` archived — M +- [ ] **REF-009** One ADF codec; `viewer/pages.py` deleted (assets or Vue tab) — M +- [ ] **REF-010** Session brief / capture / resolve / create-work in Python; one `accept` verb — L +- [ ] **REF-011** Split `IssueSyncService` into Jira and GitHub adapters — M +- [ ] **DOC-007** Glossary + "add a CLI command" contributor guide — S +- [ ] Open **Milestone 5** (make it fast) once Milestone 3 P0/P1 are checked + +## Baseline to beat (2026-09-13) + +| Measure | Now | Target end of October | +|---------|-----|-----------------------| +| Engines | 2 (bash twin + Python) | 1 | +| Persistence models supported | v3 + legacy layouts | v3 only | +| Functions CCN > 15 in `engine/src` | 67 | 0 | +| Longest function | 1 056 lines (`installer/app.create_app`) | ≤ 80 NLOC everywhere | +| Unit coverage of `sdlc_engine` | 66 % | ≥ 80 % (viewer/installer no longer 0–26 %) | +| Ruff rules in CI | `F401,F811` | full `F` + `E9`, complexity on diff, shellcheck | +| Undefined names shipped | 9 | 0 | +| Broken doc links | 95 / 959 | 0, gated | +| GitHub workflows | 26 | ≤ 10 | +| Shellcheck errors | 7 | 0, gated | +| Shipped bash LOC (`templates/agent-context` + `scripts`) | ~13.7 k | ≤ 10 k | + +## Rules for the month + +- One Work ID, one PR series; no PR adds a second implementation of anything Python already does. +- A legacy path stays only if a named user needs it; otherwise delete. +- Docs change in the same PR as the behavior they describe. +- Capture lessons with `./scripts/sdlc.sh capture`; accept at retro. diff --git a/sdlc-spdd/spdd/tasks/milestone-3-one-flow.md b/sdlc-spdd/spdd/tasks/milestone-3-one-flow.md new file mode 100644 index 00000000..84319eeb --- /dev/null +++ b/sdlc-spdd/spdd/tasks/milestone-3-one-flow.md @@ -0,0 +1,66 @@ +# Milestone 3 task list — One flow on storage v3 + +Iteration list for consolidating `sdlc-spdd-orchestrator` onto one engine (Python `sdlc-engine`) and one persistence model (storage v3), with code, tests, and docs aligned. + +Full review: [`../analysis/SPIKE-005-architecture-review-analysis.md`](../analysis/SPIKE-005-architecture-review-analysis.md) +Milestone: [`../../requirements/milestones/milestone-3/MILESTONE-3.md`](../../requirements/milestones/milestone-3/MILESTONE-3.md) +Monthly goals (Obsidian): [`2026-10-monthly-goals.md`](2026-10-monthly-goals.md) + +## How to use this list + +One Work ID at a time. For each row: analysis → plan/canvas → architect → code one operation → review. Beck stage for the whole list: **make it right**. + +## P0 — one flow + +| # | Work ID | Task | Done when | +|---|---------|------|-----------| +| 0 | `SPIKE-005-architecture-review` | Review + program (this list) | Analysis, milestones 3/4/5, stubs, canvas | +| 1 | `BUG-001-db-query-undefined-names` | Fix `NameError` in `db export`; test it | `ruff --select F821` clean; export test | +| 2 | `CHORE-004-lint-and-complexity-gates-real` | Full pyflakes, complexity on PR diff, shellcheck in CI | A CCN-11 function fails CI; shellcheck `-S error` green | +| 3 | `CHORE-006-dogfood-adapters-and-quick-spec` | Dogfood packs from spec; `quick` spec | Rewritten templates diff clean vs dogfood | +| 4 | `REF-002-purge-pre-v3-compat` | Delete legacy layouts, migrations, archive folders | Acceptance greps in stub | +| 5 | `REF-003-retire-bash-workflow-dual-path` | Delete bash workflow twin and engine switches | No `sdlc-workflow.sh`; `SDLC_ENGINE` gone | + +## P1 — maintainable + +| # | Work ID | Task | Done when | +|---|---------|------|-----------| +| 6 | `REF-004-split-installer-blueprints` | Blueprints per console tab | No installer fn > 80 NLOC / CCN 10 | +| 7 | `REF-005-cli-command-modules` | Command modules + result objects | `import sdlc_engine.cli` loads no heavy deps | +| 8 | `REF-006-storage-records-and-atomic-appends` | Typed records; locked appends | 20 parallel captures → 20 lines | +| 9 | `REF-007-single-guide-transport` | `GuideClient` only | No raw `urllib` outside client/issues | +| 10 | `REF-008-console-viewer-hardening` | Threat model, `--lan` token, root allow-list, CSRF | Rejection tests pass | +| 11 | `CHORE-005-ci-reusable-workflows` | 26 → ≤ 10 workflows | Mapping table in TESTING.md | +| 12 | `TEST-004-hermetic-unit-suite-and-fixtures` | Flask-free unit; shared fixture; no grab-bags | Import test; counts match tree | + +## P2 — finish + +| # | Work ID | Task | Done when | +|---|---------|------|-----------| +| 13 | `REF-009-adf-codec-and-viewer-assets` | One ADF codec; drop `pages.py` | Viewer Playwright green | +| 14 | `REF-010-pythonize-session-and-capture` | Session/capture/resolve/create-work in Python | One `accept` verb | +| 15 | `REF-011-split-issue-sync-adapters` | Jira + GitHub adapters | No issues fn > CCN 10 | + +## Review findings → Work IDs + +| Finding (SPIKE-005) | Work ID | +|---------------------|---------| +| M1 two behavioral cores | REF-003 | +| M2 pre-v3 compat load-bearing; archive fork | REF-002 | +| M3 latent NameErrors; narrow lint gate | BUG-001, CHORE-004 | +| M4 complexity gate proves the checker | CHORE-004 | +| M5 god modules / CLI monolith | REF-004, REF-005 | +| M6 ad-hoc records; unlocked appends | REF-006 | +| M7 Guide transport ×3 | REF-007 | +| M8 web surfaces assume localhost | REF-008, REF-009 | +| M9 porous test layers; 26 workflows | TEST-004, CHORE-005 | +| M10 dogfood adapters lag templates | CHORE-006 | +| M14 shell-only session stack; issues mega-service | REF-010, REF-011 | + +Docs findings M11–M13 are Milestone 4: [`../../requirements/milestones/milestone-4/MILESTONE-4.md`](../../requirements/milestones/milestone-4/MILESTONE-4.md). + +## First next action + + /sdlc-spdd-plan @sdlc-spdd/requirements/milestones/milestone-3/SPIKE-005-architecture-review.md + +Then BUG-001 (smallest, proves the gate widening is needed). From 53f142a6fc501c4dea5e65db935284c9c1707bfe Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 13 Sep 2026 23:27:45 +0000 Subject: [PATCH 02/22] BUG-001: fix NameError in db export --format sql; drop dead context_linked_to_section. db_query.py referenced _utc_now and NODE_*/REL_* constants without importing them, so sdlc-engine db export --format sql crashed. Import utc_now, delete the uncalled context_linked_to_section (the only user of the graph constants), and add a round-trip regression test for export_sql and the CLI path. Co-authored-by: John Menke --- engine/src/sdlc_engine/db_query.py | 66 +----------------------------- engine/tests_unit/test_db.py | 16 ++++++++ 2 files changed, 17 insertions(+), 65 deletions(-) diff --git a/engine/src/sdlc_engine/db_query.py b/engine/src/sdlc_engine/db_query.py index 6958d276..71ab210e 100644 --- a/engine/src/sdlc_engine/db_query.py +++ b/engine/src/sdlc_engine/db_query.py @@ -8,6 +8,7 @@ from typing import Any, Iterable from .db_schema import SCHEMA_VERSION +from .timeutil import utc_now as _utc_now _SELECT_RE = re.compile(r"^\s*select\b", re.IGNORECASE | re.DOTALL) @@ -148,71 +149,6 @@ def graph_for_work(self, work_id: str) -> dict[str, Any]: "edges": edges, } - def context_linked_to_section( - self, *, section_kind: str, section_id: str - ) -> dict[str, Any]: - """Context parts (areas/lessons) linked to a requirement or REASONS canvas.""" - self.ensure_schema() - kind = (section_kind or "").strip().lower() - sid = (section_id or "").strip() - with self.connect() as conn: - area_ids = [ - r["dst_id"] - for r in conn.execute( - "SELECT dst_id FROM edges WHERE src_kind = ? AND src_id = ? " - "AND rel = ? AND dst_kind = ?", - (kind, sid, REL_AREA, NODE_AREA), - ).fetchall() - ] - lesson_ids = [ - r["src_id"] - for r in conn.execute( - "SELECT src_id FROM edges WHERE src_kind = ? AND rel = ? " - "AND dst_kind = ? AND dst_id = ?", - (NODE_LESSON, REL_ABOUT, kind, sid), - ).fetchall() - ] - areas = [] - if area_ids: - placeholders = ",".join("?" * len(area_ids)) - areas = [ - dict(r) - for r in conn.execute( - f"SELECT * FROM areas WHERE id IN ({placeholders})", - area_ids, - ).fetchall() - ] - lessons = [] - if lesson_ids: - placeholders = ",".join("?" * len(lesson_ids)) - lessons = [ - dict(r) - for r in conn.execute( - f"SELECT * FROM lessons WHERE id IN ({placeholders}) " - "ORDER BY ts DESC, id", - lesson_ids, - ).fetchall() - ] - reasons = [] - if kind == NODE_REQUIREMENT: - reasons = [ - dict(r) - for r in conn.execute( - "SELECT c.* FROM canvases c " - "JOIN edges e ON e.dst_id = c.id " - "WHERE e.src_kind = ? AND e.src_id = ? AND e.rel = ? " - "AND e.dst_kind = ?", - (NODE_REQUIREMENT, sid, REL_REASONS, NODE_CANVAS), - ).fetchall() - ] - return { - "section_kind": kind, - "section_id": sid, - "areas": areas, - "lessons": lessons, - "reasons_canvases": reasons, - } - LOOKUP_COLUMNS = ( "work_id", "title", diff --git a/engine/tests_unit/test_db.py b/engine/tests_unit/test_db.py index 796f1443..a6036804 100644 --- a/engine/tests_unit/test_db.py +++ b/engine/tests_unit/test_db.py @@ -123,6 +123,22 @@ def test_export_json_and_cli(tmp_path: Path, monkeypatch) -> None: ) +def test_export_sql_dump_and_cli(tmp_path: Path, monkeypatch) -> None: + """BUG-001: export_sql raised NameError (_utc_now) — the SQL dump path must round-trip.""" + monkeypatch.setenv("SDLC_USER", "db-tester") + _seed(tmp_path, "FEAT-704-sql-export") + idx = LocalIndex(Project(tmp_path)) + idx.rebuild() + out = tmp_path / "dump.sql" + text = idx.export_sql(out) + assert text.startswith("-- SDLC local index dump (") + assert "BEGIN;" in text and "COMMIT;" in text + assert "work_items" in text + assert out.read_text(encoding="utf-8") == text + assert main(["--root", str(tmp_path), "db", "export", "--format", "sql", "--output", str(tmp_path / "cli.sql")]) == 0 + assert (tmp_path / "cli.sql").is_file() + + def test_lookup_json_and_markdown(tmp_path: Path, monkeypatch) -> None: monkeypatch.setenv("SDLC_USER", "db-tester") wid = "FEAT-704-lookup" From d79d6e56e3b961bd24b3a3c00e01fcb9e156dc88 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 13 Sep 2026 23:30:03 +0000 Subject: [PATCH 03/22] CHORE-004: run full pyflakes, PR-diff complexity, and shellcheck as real gates. Ruff select is F,E9 in pyproject/CI/test (was F401,F811, which let 9 undefined names ship). test-sdlc-engine runs check-complexity.py against the PR base on pull requests. New shellcheck workflow (bash -n + -S error) over scripts, installed runtime templates, tests, and install.sh. Fixes the current shellcheck errors: quoted glob in resolve-agent-context skill patterns never expanded (SC2066), array-style expansion in setup-engine-venv (SC1087), restore_victim redefinitions in test-command-specs (SC2218), [[ -f glob ]] in the shim harness (SC2144). Drops four F541 f-strings. TESTING.md documents the gates and fixes the stale 158-test count. Co-authored-by: John Menke --- .github/workflows/shellcheck.yml | 37 +++++++++++++++++++++++++ .github/workflows/test-sdlc-engine.yml | 9 ++++-- TESTING.md | 20 +++++++++++-- engine/pyproject.toml | 4 ++- engine/src/sdlc_engine/cli_commands.py | 2 +- engine/src/sdlc_engine/phases.py | 4 +-- engine/src/sdlc_engine/viewer/app.py | 2 +- engine/tests_unit/test_quality_gates.py | 15 ++++++++-- scripts/resolve-agent-context.sh | 4 ++- scripts/setup-engine-venv.sh | 4 +-- tests/test-command-specs.sh | 6 ++-- tests/test-sdlc-engine-shim.sh | 9 ++---- 12 files changed, 89 insertions(+), 27 deletions(-) create mode 100644 .github/workflows/shellcheck.yml diff --git a/.github/workflows/shellcheck.yml b/.github/workflows/shellcheck.yml new file mode 100644 index 00000000..8837bd8f --- /dev/null +++ b/.github/workflows/shellcheck.yml @@ -0,0 +1,37 @@ +name: Shellcheck + +on: + pull_request: + paths: + - 'scripts/**/*.sh' + - 'templates/agent-context/**/*.sh' + - 'tests/**/*.sh' + - '.cursor/install.sh' + - '.github/workflows/shellcheck.yml' + push: + branches: [main] + paths: + - 'scripts/**/*.sh' + - 'templates/agent-context/**/*.sh' + - 'tests/**/*.sh' + - '.cursor/install.sh' + - '.github/workflows/shellcheck.yml' + +jobs: + shellcheck: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install shellcheck + run: sudo apt-get update -qq && sudo apt-get install -y -qq shellcheck + - name: Syntax (bash -n) + run: | + set -euo pipefail + find scripts templates/agent-context tests .cursor -name '*.sh' -print0 \ + | xargs -0 -n1 bash -n + - name: Shellcheck (errors fail; warnings reported) + run: | + set -euo pipefail + mapfile -d '' files < <(find scripts templates/agent-context tests .cursor -name '*.sh' -print0) + shellcheck -S warning -f gcc "${files[@]}" || true + shellcheck -S error -f gcc "${files[@]}" diff --git a/.github/workflows/test-sdlc-engine.yml b/.github/workflows/test-sdlc-engine.yml index 7f65bab0..3724faa4 100644 --- a/.github/workflows/test-sdlc-engine.yml +++ b/.github/workflows/test-sdlc-engine.yml @@ -41,10 +41,15 @@ jobs: python-version: '3.12' - name: Install engine run: python3 -m pip install -e './engine[dev,viewer]' - - name: Ruff unused imports - run: python3 -m ruff check --select F401,F811 engine/src scripts + - name: Ruff (full pyflakes + E9, mirrors engine/pyproject.toml) + run: python3 -m ruff check --select F,E9 engine/src scripts - name: Complexity proving test run: bash scripts/test-check-complexity.sh + - name: Complexity gate on this diff (CCN>10 / NLOC>80 / CCN rise) + if: github.event_name == 'pull_request' + run: | + git fetch --no-tags origin "${{ github.base_ref }}" + python3 scripts/check-complexity.py --base "origin/${{ github.base_ref }}" --paths engine/src scripts - name: Suite 1 — unit run: PYTHONPATH=engine/src pytest -q engine/tests_unit diff --git a/TESTING.md b/TESTING.md index bf297701..5d36fcc2 100644 --- a/TESTING.md +++ b/TESTING.md @@ -57,7 +57,7 @@ Then: `./scripts/setup-engine-venv.sh --e2e` 1. **Preflight first** — fail in seconds, not after a 600s Guide wait or hung `/sse` curl. 2. **One suite at a time** — unit → integration → e2e (never jump to e2e while unit is red). -3. **Target failures** — use `--lf` or a single `path::test`; don’t re-run 158 unit tests while fixing one. +3. **Target failures** — use `--lf` or a single `path::test`; don’t re-run the full unit suite (≈280 tests) while fixing one. 4. **Skip green suites** — `all` skips suites already passed at the current commit (stored in `.sdlc/test-suite-state.tsv`). ### Typical session @@ -95,7 +95,7 @@ Then: `./scripts/setup-engine-venv.sh --e2e` | Situation | Do this | Don’t do this | |-----------|---------|----------------| -| One unit test red | `unit -- path::test` then `unit --lf` | `unit` (full 158 tests) | +| One unit test red | `unit -- path::test` then `unit --lf` | `unit` (full suite) | | Unit green, editing installer | `integration -- path::test` | `all` from scratch | | Playwright flake | `e2e -- engine/tests_e2e/test_vue3_console_playwright.py::test_foo` | `e2e --guide` | | Guide not up | `preflight e2e --guide` first | bare `curl …/sse` | @@ -103,7 +103,21 @@ Then: `./scripts/setup-engine-venv.sh --e2e` Shell workflow harness (separate from engine pytest): `./tests/test-sdlc-workflow.sh` — run only when touching `scripts/sdlc.sh` or workflow gates. -**Suite 1** — fast, isolated: mocks, `tmp_path`, no Flask server, no browser, no network. +**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.) + +### Static gates (Suite 1 and CI) + +| Gate | Where | Fails when | +|------|-------|------------| +| Ruff `F,E9` (full pyflakes + syntax) | `engine/pyproject.toml`, `test_quality_gates.py`, `test-sdlc-engine.yml` | any undefined name, unused import/variable, redefinition, syntax error in `engine/src` or `scripts` | +| Complexity on the PR diff | `scripts/check-complexity.py --base origin/` in `test-sdlc-engine.yml` (PRs only) | a changed file adds a function with CCN > 10 or NLOC > 80, or raises an existing function's CCN | +| Complexity proving test | `scripts/test-check-complexity.sh` (synthetic repo) | the checker itself stops detecting a CCN-11 function | +| Shellcheck | `.github/workflows/shellcheck.yml` | `bash -n` fails or any `-S error` finding in `scripts/`, `templates/agent-context/`, `tests/`, `.cursor/install.sh` (warnings are printed, not failing yet) | + +Proved 2026-09-13 (CHORE-004): adding a CCN-11 `probe()` under `engine/src` made +`check-complexity.py --base main` exit 1 with `NEW …::probe CCN=11`; the tree +without it exits 0. **Suite 2** — Flask `test_client`: ops console installer API, ADF viewer HTTP, mocked externals. diff --git a/engine/pyproject.toml b/engine/pyproject.toml index 14c8f391..5a1c3701 100644 --- a/engine/pyproject.toml +++ b/engine/pyproject.toml @@ -39,4 +39,6 @@ target-version = "py312" src = ["src"] [tool.ruff.lint] -select = ["F401", "F811"] +# Full pyflakes (undefined names, unused imports/variables, redefinitions) plus +# syntax/IO errors. CHORE-004: the narrower F401,F811 set shipped 9 NameErrors. +select = ["F", "E9"] diff --git a/engine/src/sdlc_engine/cli_commands.py b/engine/src/sdlc_engine/cli_commands.py index a7959b89..643b6ec5 100644 --- a/engine/src/sdlc_engine/cli_commands.py +++ b/engine/src/sdlc_engine/cli_commands.py @@ -795,7 +795,7 @@ def cmd_quick(args: argparse.Namespace) -> int: session = svc.start(intent=intent, title=intent) print(f"Started local session {session.id}") print(f"Pointer set. Artifacts: .sdlc/local-sessions/{session.id}/") - print(f"Brief: .sdlc/current-local-session.md") + print("Brief: .sdlc/current-local-session.md") print("This work stays offline until: ./scripts/sdlc.sh local promote --type feature --name \"...\"") return 0 diff --git a/engine/src/sdlc_engine/phases.py b/engine/src/sdlc_engine/phases.py index cd1df420..de597bcd 100644 --- a/engine/src/sdlc_engine/phases.py +++ b/engine/src/sdlc_engine/phases.py @@ -65,7 +65,7 @@ def valid_phase(phase: str) -> bool: def recommended_command(phase: str, work_id: str = "", operation: str = "") -> str: wid = work_id or "" mapping = { - "init": f"/sdlc-spdd-init @requirements/ @ROADMAP.md", + "init": "/sdlc-spdd-init @requirements/ @ROADMAP.md", "analysis": f"/sdlc-spdd-analysis @requirements/milestones/{wid}.md", "plan": f"/sdlc-spdd-plan @requirements/milestones/{wid}.md @ROADMAP.md", "architect": f"/sdlc-spdd-architect @spdd/canvas/{wid}.md", @@ -80,7 +80,7 @@ def recommended_command(phase: str, work_id: str = "", operation: str = "") -> s "retro": f"/sdlc-spdd-retro @spdd/canvas/{wid}.md", "sync": f"/sdlc-spdd-sync @spdd/canvas/{wid}.md", } - return mapping.get(phase, f"/sdlc-spdd-whereami") + return mapping.get(phase, "/sdlc-spdd-whereami") def gates_for_phase(phase: str) -> tuple[str, ...]: diff --git a/engine/src/sdlc_engine/viewer/app.py b/engine/src/sdlc_engine/viewer/app.py index 28453e35..b1c811df 100644 --- a/engine/src/sdlc_engine/viewer/app.py +++ b/engine/src/sdlc_engine/viewer/app.py @@ -559,6 +559,6 @@ def run_viewer( root_p = Path(root).expanduser().resolve() print(f"Start directory: {root_p}") print(f"Default ADF folder: {root_p / 'adf'}") - print(f"Browse: any local path (local-only tool)") + print("Browse: any local path (local-only tool)") print(f"Open: http://{host}:{port}/") app.run(host=host, port=port, debug=debug) diff --git a/engine/tests_unit/test_quality_gates.py b/engine/tests_unit/test_quality_gates.py index 498faa6f..bbca1641 100644 --- a/engine/tests_unit/test_quality_gates.py +++ b/engine/tests_unit/test_quality_gates.py @@ -10,10 +10,19 @@ REPO = Path(__file__).resolve().parents[2] -def test_ruff_unused_imports() -> None: +RUFF_SELECT = "F,E9" # mirrors [tool.ruff.lint] select in engine/pyproject.toml + + +def test_ruff_rule_set_matches_pyproject() -> None: + text = (REPO / "engine" / "pyproject.toml").read_text(encoding="utf-8") + assert 'select = ["F", "E9"]' in text, "CI/test rule set drifted from pyproject" + + +def test_ruff_pyflakes_clean() -> None: + """CHORE-004: full pyflakes, not only unused imports — F821 shipped a NameError once.""" ruff = shutil.which("ruff") or sys.executable - cmd = [ruff, "check", "--select", "F401,F811", "engine/src", "scripts"] if ruff != sys.executable else [ - sys.executable, "-m", "ruff", "check", "--select", "F401,F811", "engine/src", "scripts" + cmd = [ruff, "check", "--select", RUFF_SELECT, "engine/src", "scripts"] if ruff != sys.executable else [ + sys.executable, "-m", "ruff", "check", "--select", RUFF_SELECT, "engine/src", "scripts" ] proc = subprocess.run(cmd, cwd=REPO, capture_output=True, text=True, check=False) assert proc.returncode == 0, proc.stdout + proc.stderr diff --git a/scripts/resolve-agent-context.sh b/scripts/resolve-agent-context.sh index 4d5280ed..037ab66c 100755 --- a/scripts/resolve-agent-context.sh +++ b/scripts/resolve-agent-context.sh @@ -340,7 +340,9 @@ add_phase_index_glob() { local pattern="$1" shopt -s nullglob local match - for match in "${FRAMEWORK_HOME}/${pattern}"; do + # ${pattern} is intentionally unquoted so the glob expands. + # shellcheck disable=SC2086 + for match in "${FRAMEWORK_HOME}"/${pattern}; do if [[ -f "${match}" ]]; then add_path "${match}" fi diff --git a/scripts/setup-engine-venv.sh b/scripts/setup-engine-venv.sh index 31e0bded..82db6c4b 100755 --- a/scripts/setup-engine-venv.sh +++ b/scripts/setup-engine-venv.sh @@ -99,8 +99,8 @@ fi echo "==> upgrading pip ($("$BIN/python" --version))" "$BIN/pip" install -U pip wheel setuptools -echo "==> pip install -e '$ENGINE[$EXTRAS]'" -"$BIN/pip" install -e "$ENGINE[$EXTRAS]" +echo "==> pip install -e '${ENGINE}[${EXTRAS}]'" +"$BIN/pip" install -e "${ENGINE}[${EXTRAS}]" if [[ "$EXTRAS" == *viewer-e2e* ]]; then echo "==> playwright install chromium (for E2E)" diff --git a/tests/test-command-specs.sh b/tests/test-command-specs.sh index 5ae53357..898830bd 100755 --- a/tests/test-command-specs.sh +++ b/tests/test-command-specs.sh @@ -322,10 +322,11 @@ expect_pass "validate-command-adapters clean" "${VALIDATE}" # --------------------------------------------------------------------------- echo "== Validator catches stripped semantic contracts ==" +# restore_victim reads bak/victim at call time; defined once for all blocks below. +restore_victim() { cp "${bak}" "${victim}"; rm -f "${bak}"; } victim="${REPO_ROOT}/templates/cursor/sdlc-spdd-whereami.md" bak="$(mktemp)" cp "${victim}" "${bak}" -restore_victim() { cp "${bak}" "${victim}"; rm -f "${bak}"; } trap 'restore_victim' EXIT # Remove the jira-ask step while keeping file otherwise valid enough to parse. grep -Fv -- 'Jira as `missing` or `draft`' "${bak}" > "${victim}" @@ -337,7 +338,6 @@ expect_pass "validate after restore" "${VALIDATE}" victim="${REPO_ROOT}/templates/cursor/sdlc-spdd-code.md" bak="$(mktemp)" cp "${victim}" "${bak}" -restore_victim() { cp "${bak}" "${victim}"; rm -f "${bak}"; } trap 'restore_victim' EXIT grep -Fv -- 'Ready For Coding' "${bak}" > "${victim}" expect_fail "validate fails when code readiness stripped" "${VALIDATE}" @@ -348,7 +348,6 @@ expect_pass "validate after code restore" "${VALIDATE}" victim="${REPO_ROOT}/templates/cursor/sdlc-spdd-code.md" bak="$(mktemp)" cp "${victim}" "${bak}" -restore_victim() { cp "${bak}" "${victim}"; rm -f "${bak}"; } trap 'restore_victim' EXIT grep -Fv -- 'git diff --name-only' "${bak}" > "${victim}" expect_fail "validate fails when code Files: allowlist stripped" "${VALIDATE}" @@ -359,7 +358,6 @@ expect_pass "validate after code I1 restore" "${VALIDATE}" victim="${REPO_ROOT}/templates/cursor/sdlc-spdd-review.md" bak="$(mktemp)" cp "${victim}" "${bak}" -restore_victim() { cp "${bak}" "${victim}"; rm -f "${bak}"; } trap 'restore_victim' EXIT grep -Fv -- 'check-operation-diff-scope.sh' "${bak}" > "${victim}" expect_fail "validate fails when review Files: diff-scope check stripped" "${VALIDATE}" diff --git a/tests/test-sdlc-engine-shim.sh b/tests/test-sdlc-engine-shim.sh index dc9d7576..43b05c19 100755 --- a/tests/test-sdlc-engine-shim.sh +++ b/tests/test-sdlc-engine-shim.sh @@ -60,15 +60,10 @@ if grep -Fq 'Started local session LOCAL-' <<< "${out}"; then else bad "local start unexpected: ${out}" fi -if [[ -f "${tmp}/.sdlc/local-sessions/"LOCAL-*/session.json ]]; then +if find "${tmp}/.sdlc/local-sessions" -path '*/LOCAL-*/session.json' 2>/dev/null | grep -q .; then ok "local session artifacts under .sdlc/local-sessions" else - # glob may not expand in [[ -f ]]; check via find - if find "${tmp}/.sdlc/local-sessions" -name session.json | grep -q .; then - ok "local session artifacts under .sdlc/local-sessions" - else - bad "missing local session artifacts" - fi + bad "missing local session artifacts" fi echo "== work init-from-adf (python engine + sdlc.sh route) ==" From 4b2d77314cd21462bc1eeea47f733b1e11ff49f9 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 13 Sep 2026 23:31:58 +0000 Subject: [PATCH 04/22] REF-002 T01: archive removes contract artifacts; no spdd/*/archive folders. Python ArchiveService moved canvases/analysis/reviews/sync into spdd/*/archive/ and swept legacy agent-context/sessions, while the documented storage v3 contract (docs/storage-v3.md, TESTING.md, ROADMAP) and the shell path delete. One contract now: remove canvas, analysis, review, sync, matching hot session briefs, and the workflow .state; append the archived registry event; leave requirements and lessons.jsonl untouched. Unit and CLI tests assert the same behavior the bash harness already asserted; legacy feature-mirror seeding is gone from the tests. Co-authored-by: John Menke --- engine/src/sdlc_engine/archive.py | 82 ++++++++-------------- engine/tests_unit/test_cli.py | 3 +- engine/tests_unit/test_registry_archive.py | 26 ++++--- 3 files changed, 49 insertions(+), 62 deletions(-) diff --git a/engine/src/sdlc_engine/archive.py b/engine/src/sdlc_engine/archive.py index cc69119b..bf67dcdd 100644 --- a/engine/src/sdlc_engine/archive.py +++ b/engine/src/sdlc_engine/archive.py @@ -1,8 +1,14 @@ -"""Archive completed/cancelled Work ID artifacts.""" +"""Archive completed/cancelled Work ID artifacts. + +Storage v3 contract: archive *removes* the Work ID's contract artifacts from the +working tree (canvas, analysis, review, sync, hot session briefs, workflow +state) and appends an ``archived`` event to ``spdd/memory/registry.jsonl``. +Git history is the audit trail; there are no ``spdd/*/archive/`` folders. +Requirements and the lessons ledger are never touched. +""" from __future__ import annotations -import shutil from dataclasses import dataclass from pathlib import Path @@ -24,18 +30,14 @@ def __post_init__(self) -> None: self.workflow = self.workflow or WorkflowEngine(self.project) self.registry = self.registry or TeamRegistry(self.project, self.workflow) - def _move(self, src: Path, dest: Path, dry_run: bool) -> bool: - if not src.exists(): + def _remove(self, path: Path, dry_run: bool) -> bool: + if not path.exists(): return False if dry_run: - print(f"[dry-run] would move {self.project.rel(src)} -> {self.project.rel(dest)}") + print(f"[dry-run] would remove {self.project.rel(path)}") return True - dest.parent.mkdir(parents=True, exist_ok=True) - if dest.exists(): - print(f"archive: destination already exists, skipping {self.project.rel(dest)}") - return False - shutil.move(str(src), str(dest)) - print(f"Moved {self.project.rel(src)} -> {self.project.rel(dest)}") + path.unlink() + print(f"Removed {self.project.rel(path)}") return True def archive_work(self, work_id: str, *, dry_run: bool = False, force: bool = False) -> None: @@ -56,48 +58,24 @@ def archive_work(self, work_id: str, *, dry_run: bool = False, force: bool = Fal self.workflow.pointer.reset() print(f"Cleared local pointer (was {work_id})") - home = self.project.home - moved = False - for src, dest in [ - ( - self.project.canvas_path(work_id), - self.project.spdd_dir / "canvas" / "archive" / f"{work_id}.md", - ), - ( - self.project.analysis_path(work_id), - self.project.spdd_dir / "analysis" / "archive" / f"{work_id}-analysis.md", - ), - ( - self.project.review_path(work_id), - self.project.spdd_dir / "reviews" / "archive" / f"{work_id}-review.md", - ), - ( - self.project.sync_path(work_id), - self.project.spdd_dir / "sync" / "archive" / f"{work_id}-sync.md", - ), - ]: - moved |= self._move(src, dest, dry_run) - - session_dirs = [self.project.hot_session_dir(), home / "agent-context" / "sessions"] - if home != self.project.root: - session_dirs.append(self.project.root / "agent-context" / "sessions") - seen_sessions: set[Path] = set() - for sessions in session_dirs: - if not sessions.is_dir() or sessions in seen_sessions: - continue - seen_sessions.add(sessions) - for sess in sessions.iterdir(): + removed = False + for src in ( + self.project.canvas_path(work_id), + self.project.analysis_path(work_id), + self.project.review_path(work_id), + self.project.sync_path(work_id), + ): + removed |= self._remove(src, dry_run) + + sessions = self.project.hot_session_dir() + if sessions.is_dir(): + for sess in sorted(sessions.iterdir()): if not sess.is_file() or sess.name == "current-session.md": continue if work_id in sess.name: - moved |= self._move(sess, sessions / "archive" / sess.name, dry_run) + removed |= self._remove(sess, dry_run) - state_src = self.project.workflows_dir / f"{work_id}.state" - moved |= self._move( - state_src, - self.project.workflows_dir / "archive" / f"{work_id}.state", - dry_run, - ) + removed |= self._remove(self.project.workflows_dir / f"{work_id}.state", dry_run) if dry_run: print(f"[dry-run] would mark {work_id} archived in registry.jsonl") @@ -114,10 +92,10 @@ def archive_work(self, work_id: str, *, dry_run: bool = False, force: bool = Fal note=note, ) ) - if not moved: - print(f"archive: {work_id} marked archived (no movable artifacts found; milestone left in place)") + if not removed: + print(f"archive: {work_id} marked archived (no artifacts found; requirement left in place)") else: - print(f"Archived {work_id} ({kind}). Commit moved paths + spdd/memory/registry.jsonl.") + print(f"Archived {work_id} ({kind}). Commit the removals + spdd/memory/registry.jsonl.") print(f"Left in place: requirements/milestones/{work_id}.md (if present).") print("Left in place: spdd/memory/lessons.jsonl (archive never truncates the lessons ledger).") diff --git a/engine/tests_unit/test_cli.py b/engine/tests_unit/test_cli.py index 04977474..89d5c3db 100644 --- a/engine/tests_unit/test_cli.py +++ b/engine/tests_unit/test_cli.py @@ -16,7 +16,8 @@ def test_cli_claim_next_archive(tmp_path: Path, monkeypatch) -> None: assert main(["--root", str(tmp_path), "claim", work_id]) == 0 assert main(["--root", str(tmp_path), "next"]) == 0 assert main(["--root", str(tmp_path), "archive", work_id]) == 0 - assert (tmp_path / "spdd" / "canvas" / "archive" / f"{work_id}.md").is_file() + assert not (tmp_path / "spdd" / "canvas" / f"{work_id}.md").exists() + assert not (tmp_path / "spdd" / "canvas" / "archive").exists() assert main(["--root", str(tmp_path), "version"]) == 0 diff --git a/engine/tests_unit/test_registry_archive.py b/engine/tests_unit/test_registry_archive.py index 55591ab7..3995d1f2 100644 --- a/engine/tests_unit/test_registry_archive.py +++ b/engine/tests_unit/test_registry_archive.py @@ -12,9 +12,6 @@ def _seed(root: Path, work_id: str, status: str) -> None: f"# {work_id}\n\n## Final Status\n\n- Status: {status}\n", encoding="utf-8", ) - feat = root / "agent-context" / "features" / work_id - feat.mkdir(parents=True, exist_ok=True) - (feat / "requirement.md").write_text("# req\n", encoding="utf-8") (root / "requirements" / "milestones").mkdir(parents=True, exist_ok=True) (root / "requirements" / "milestones" / f"{work_id}.md").write_text("# m\n", encoding="utf-8") (root / "spdd" / "analysis").mkdir(parents=True, exist_ok=True) @@ -35,7 +32,8 @@ def test_claim_and_list_work(tmp_path: Path, monkeypatch) -> None: assert "tester" in team -def test_archive_complete_keeps_milestone(tmp_path: Path, monkeypatch) -> None: +def test_archive_complete_removes_contracts_keeps_requirement(tmp_path: Path, monkeypatch) -> None: + """Storage v3 (REF-002): archive deletes contract artifacts; git history is the record.""" monkeypatch.setenv("SDLC_USER", "archiver") work_id = "FEAT-021-done" _seed(tmp_path, work_id, "Complete") @@ -44,14 +42,24 @@ def test_archive_complete_keeps_milestone(tmp_path: Path, monkeypatch) -> None: svc = ArchiveService(Project(tmp_path), reg) svc.archive_work(work_id) assert not (tmp_path / "spdd" / "canvas" / f"{work_id}.md").exists() - assert (tmp_path / "spdd" / "canvas" / "archive" / f"{work_id}.md").is_file() - # Feature mirrors are legacy; archive no longer moves them (storage v3). - assert (tmp_path / "agent-context" / "features" / work_id).is_dir() + assert not (tmp_path / "spdd" / "analysis" / f"{work_id}-analysis.md").exists() + assert not (tmp_path / "spdd" / "canvas" / "archive").exists() + assert not (tmp_path / "spdd" / "analysis" / "archive").exists() assert (tmp_path / "requirements" / "milestones" / f"{work_id}.md").is_file() rows = {r.work_id: r for r in reg.rows()} assert rows[work_id].status == "archived" +def test_archive_dry_run_removes_nothing(tmp_path: Path, monkeypatch) -> None: + monkeypatch.setenv("SDLC_USER", "archiver") + work_id = "FEAT-025-dry" + _seed(tmp_path, work_id, "Complete") + svc = ArchiveService(Project(tmp_path)) + svc.archive_work(work_id, dry_run=True) + assert (tmp_path / "spdd" / "canvas" / f"{work_id}.md").is_file() + assert (tmp_path / "spdd" / "analysis" / f"{work_id}-analysis.md").is_file() + + def test_archive_does_not_touch_lessons_ledger(tmp_path: Path, monkeypatch) -> None: monkeypatch.setenv("SDLC_USER", "archiver") work_id = "FEAT-024-ledger" @@ -74,7 +82,7 @@ def test_archive_does_not_touch_lessons_ledger(tmp_path: Path, monkeypatch) -> N assert ledger.read_text(encoding="utf-8") == before -def test_archive_v3_home_moves_canvas_under_sdlc_spdd(tmp_path: Path, monkeypatch) -> None: +def test_archive_v3_home_removes_canvas_under_sdlc_spdd(tmp_path: Path, monkeypatch) -> None: """Default auto (REF-001) routes archive to Python; storage v3 lives under sdlc-spdd/.""" monkeypatch.setenv("SDLC_USER", "archiver") work_id = "FEAT-002-done-live" @@ -93,7 +101,7 @@ def test_archive_v3_home_moves_canvas_under_sdlc_spdd(tmp_path: Path, monkeypatc svc = ArchiveService(proj) svc.archive_work(work_id) assert not canvas.exists() - assert (home / "spdd" / "canvas" / "archive" / f"{work_id}.md").is_file() + assert not (home / "spdd" / "canvas" / "archive").exists() assert (req / f"{work_id}.md").is_file() From 4aa73a5a43abdc68ac4da6f50ed498f644d9b361 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 13 Sep 2026 23:33:32 +0000 Subject: [PATCH 05/22] CHORE-006: regenerate dogfood command packs; harness checks every adapter. .cursor/commands, .claude/commands, and .github/prompts lagged the templates (Kasana I1 Files: allowlist, DIF steps, whereami jira ask, next validation) in 13 files. Regenerated through framework_rewrite_adapter_paths, the same rewrite the installer applies. test-command-specs.sh now diffs every slug in all three packs against the rewritten template instead of two Cursor files; the workflow triggers on Claude/Copilot dogfood paths too. Co-authored-by: John Menke --- .claude/commands/sdlc-next.md | 5 ++++ .claude/commands/sdlc-spdd-architect.md | 6 +++++ .claude/commands/sdlc-spdd-code.md | 14 ++++++++++ .claude/commands/sdlc-spdd-review.md | 24 +++++++++++++++++ .claude/commands/sdlc-spdd-whereami.md | 5 ++++ .cursor/commands/sdlc-next.md | 5 ++++ .cursor/commands/sdlc-spdd-architect.md | 6 +++++ .cursor/commands/sdlc-spdd-whereami.md | 5 ++++ .github/prompts/sdlc-next.prompt.md | 5 ++++ .github/prompts/sdlc-spdd-architect.prompt.md | 6 +++++ .github/prompts/sdlc-spdd-code.prompt.md | 14 ++++++++++ .github/prompts/sdlc-spdd-review.prompt.md | 24 +++++++++++++++++ .github/prompts/sdlc-spdd-whereami.prompt.md | 5 ++++ .../validate-command-spec-generation.yml | 6 +++++ tests/test-command-specs.sh | 27 +++++++++++++++++-- 15 files changed, 155 insertions(+), 2 deletions(-) diff --git a/.claude/commands/sdlc-next.md b/.claude/commands/sdlc-next.md index 2683c07a..f89964c5 100644 --- a/.claude/commands/sdlc-next.md +++ b/.claude/commands/sdlc-next.md @@ -20,6 +20,11 @@ Do not implement code. 8. When `next` / status / session brief shows Jira as `missing` or `draft`, ask the user for the issue key (or confirm none applies) before coding or claiming tracker progress; then `claim --jira KEY` or set `- Key:` on the requirement. Do not invent a key. 9. When `next` / status shows canvas readiness that blocks coding (Needs Analysis, Needs Clarification, Needs Redesign, Blocked), prefer `/sdlc-spdd-architect` (or prompt-update) over `/sdlc-spdd-code`, even if the stored phase is `code`. 10. Do not start unrelated work or implement code on a Work ID claimed by another teammate (non-stale). +11. Optional DIF check (never required). If the DIF CLI exists for the active + canvas, run `architect --quiet --canvas`. Exit 1: prefer `/sdlc-spdd-architect` over + `/sdlc-spdd-code` even if the stored phase is `code`. If the script is + missing, continue — that is not an error. Do not start a JVM from + `sdlc.sh next` or `sdlc.sh gate`. ## Output diff --git a/.claude/commands/sdlc-spdd-architect.md b/.claude/commands/sdlc-spdd-architect.md index b9b83cc3..d82a7544 100644 --- a/.claude/commands/sdlc-spdd-architect.md +++ b/.claude/commands/sdlc-spdd-architect.md @@ -30,6 +30,12 @@ Do not implement code. 13. Mark whether the work is ready for coding by setting Metadata `- Readiness:` (or YAML frontmatter `readiness:`) to a **canvas readiness** vocabulary value (see Output). Prefer Title Case aliases agents already use. +14. Optional DIF check (never required). If `$DIF_HOME/scripts/dif-fold.sh` or a + sibling `../embabel-dif/scripts/dif-fold.sh` exists, run + `architect --quiet --canvas sdlc-spdd/spdd/canvas/.md`. Exit 1: set Readiness to + Needs Clarification; do not set Ready For Coding; quote the conflicts. If + the script is missing, continue — that is not an error. Do not start a JVM + from `sdlc.sh next` or `sdlc.sh gate`. ## Context Backend (runtime-resolved) diff --git a/.claude/commands/sdlc-spdd-code.md b/.claude/commands/sdlc-spdd-code.md index 5030c831..aa9ba1b9 100644 --- a/.claude/commands/sdlc-spdd-code.md +++ b/.claude/commands/sdlc-spdd-code.md @@ -31,6 +31,20 @@ Your job is to implement exactly one approved operation from a REASONS Canvas. 13. Update task status in the canvas and stage progress evidence via `./sdlc-spdd/scripts/sdlc.sh capture` (session record). 14. If the requested behavior conflicts with the canvas, stop and recommend `/sdlc-spdd-prompt-update` before changing code. 15. If no task is selected, ask which approved operation to implement before changing code. +16. Optional DIF check (never required). If `$DIF_HOME/scripts/dif-fold.sh` or a + sibling `../embabel-dif/scripts/dif-fold.sh` exists, run + `architect --quiet --canvas` on this Work ID. Exit 1: STOP and recommend + `/sdlc-spdd-architect` — do not implement. If the script is missing, continue + — that is not an error. Do not start a JVM from `sdlc.sh next` or `sdlc.sh gate`. +17. Run Validation commands named on the selected canvas operation. If none, + discover the project's documented test/lint/typecheck commands when they + exist. +18. On failure: do not mark the T## complete. Return the command output AND + the Norm/Safeguard it maps to. Host may retry in-session. +19. If the same verify command fails twice with the same error, STOP. + Recommend `/sdlc-spdd-prompt-update` or shelf. Do not loop forever. +20. After edits: `git diff --name-only` must stay within the active T## + `Files:` plus test paths. Extra paths: do not mark complete. ## Context Backend (runtime-resolved) diff --git a/.claude/commands/sdlc-spdd-review.md b/.claude/commands/sdlc-spdd-review.md index 27e9b3de..6477b5a0 100644 --- a/.claude/commands/sdlc-spdd-review.md +++ b/.claude/commands/sdlc-spdd-review.md @@ -39,6 +39,30 @@ Do not make code changes unless explicitly asked. `readiness:`) to **Reviewed** (or **Complete** if Final Status is also Complete). 20. Recommend `/sdlc-spdd-prompt-update` for behavior or requirement changes before additional code changes. 21. Recommend `/sdlc-spdd-sync` for accepted non-behavioral refactors after review. +22. Optional DIF check (never required). If `$DIF_HOME/scripts/dif-fold.sh` or a + sibling `../embabel-dif/scripts/dif-fold.sh` exists **and** snapshot files + `sdlc-spdd/spdd/snapshots/-before.json` and `sdlc-spdd/spdd/snapshots/-after.json` + exist, run `review --quiet --before … --after … --canvas sdlc-spdd/spdd/canvas/.md`. + Exit 1: do not set Reviewed / Approved; cite the one-line `dif=blocked` (and + `.gate.json` / VerificationResult). If the CLI or either snapshot is missing, + continue — that is not an error. Do not use login fixtures. Do not start a + JVM from `sdlc.sh next` or `sdlc.sh gate`. +23. Run the operation-diff-scope machine check: + `./scripts/check-operation-diff-scope.sh --work-id ` + (orchestrator: `./scripts/check-operation-diff-scope.sh`; installed + projects: `./sdlc-spdd/scripts/check-operation-diff-scope.sh` or + `python -m sdlc_engine.canvas --work-id `). Optional + `--base ` adds `git diff --name-only ...HEAD`. Optional + `--ops T01,T02` limits Files: to those operations; default is + the T## under review (in-progress/selected, else the last + completed T## — not the union of every completed T##). Changed paths + must be a subset of those Files: plus allowed test paths + (`tests/**`, `engine/tests_unit/**`, `engine/tests_integration/**`, + `engine/tests_e2e/**`, or basename `test_*.py` / `*_test.py` / + `*.spec.md`). Exit 1 (extra production paths or `..` traversal): + do not set Result to Approved or Approved With Notes — use Changes + Requested or Blocked and list the extra paths. Hunk-level C-DRIFT + inside an allowed file remains a human finding. ## Context Backend (runtime-resolved) diff --git a/.claude/commands/sdlc-spdd-whereami.md b/.claude/commands/sdlc-spdd-whereami.md index 03a18f0e..4d344927 100644 --- a/.claude/commands/sdlc-spdd-whereami.md +++ b/.claude/commands/sdlc-spdd-whereami.md @@ -20,6 +20,11 @@ Do not implement code. 8. When `next` / status / session brief shows Jira as `missing` or `draft`, ask the user for the issue key (or confirm none applies) before coding or claiming tracker progress; then `claim --jira KEY` or set `- Key:` on the requirement. Do not invent a key. 9. When `next` / status shows canvas readiness that blocks coding (Needs Analysis, Needs Clarification, Needs Redesign, Blocked), prefer `/sdlc-spdd-architect` (or prompt-update) over `/sdlc-spdd-code`, even if the stored phase is `code`. 10. Do not start unrelated work or implement code on a Work ID claimed by another teammate (non-stale). +11. Optional DIF check (never required). If the DIF CLI exists for the active + canvas, run `architect --quiet --canvas`. Exit 1: prefer `/sdlc-spdd-architect` over + `/sdlc-spdd-code` even if the stored phase is `code`. If the script is + missing, continue — that is not an error. Do not start a JVM from + `sdlc.sh next` or `sdlc.sh gate`. ## Output diff --git a/.cursor/commands/sdlc-next.md b/.cursor/commands/sdlc-next.md index 2683c07a..f89964c5 100644 --- a/.cursor/commands/sdlc-next.md +++ b/.cursor/commands/sdlc-next.md @@ -20,6 +20,11 @@ Do not implement code. 8. When `next` / status / session brief shows Jira as `missing` or `draft`, ask the user for the issue key (or confirm none applies) before coding or claiming tracker progress; then `claim --jira KEY` or set `- Key:` on the requirement. Do not invent a key. 9. When `next` / status shows canvas readiness that blocks coding (Needs Analysis, Needs Clarification, Needs Redesign, Blocked), prefer `/sdlc-spdd-architect` (or prompt-update) over `/sdlc-spdd-code`, even if the stored phase is `code`. 10. Do not start unrelated work or implement code on a Work ID claimed by another teammate (non-stale). +11. Optional DIF check (never required). If the DIF CLI exists for the active + canvas, run `architect --quiet --canvas`. Exit 1: prefer `/sdlc-spdd-architect` over + `/sdlc-spdd-code` even if the stored phase is `code`. If the script is + missing, continue — that is not an error. Do not start a JVM from + `sdlc.sh next` or `sdlc.sh gate`. ## Output diff --git a/.cursor/commands/sdlc-spdd-architect.md b/.cursor/commands/sdlc-spdd-architect.md index b9b83cc3..d82a7544 100644 --- a/.cursor/commands/sdlc-spdd-architect.md +++ b/.cursor/commands/sdlc-spdd-architect.md @@ -30,6 +30,12 @@ Do not implement code. 13. Mark whether the work is ready for coding by setting Metadata `- Readiness:` (or YAML frontmatter `readiness:`) to a **canvas readiness** vocabulary value (see Output). Prefer Title Case aliases agents already use. +14. Optional DIF check (never required). If `$DIF_HOME/scripts/dif-fold.sh` or a + sibling `../embabel-dif/scripts/dif-fold.sh` exists, run + `architect --quiet --canvas sdlc-spdd/spdd/canvas/.md`. Exit 1: set Readiness to + Needs Clarification; do not set Ready For Coding; quote the conflicts. If + the script is missing, continue — that is not an error. Do not start a JVM + from `sdlc.sh next` or `sdlc.sh gate`. ## Context Backend (runtime-resolved) diff --git a/.cursor/commands/sdlc-spdd-whereami.md b/.cursor/commands/sdlc-spdd-whereami.md index 03a18f0e..4d344927 100644 --- a/.cursor/commands/sdlc-spdd-whereami.md +++ b/.cursor/commands/sdlc-spdd-whereami.md @@ -20,6 +20,11 @@ Do not implement code. 8. When `next` / status / session brief shows Jira as `missing` or `draft`, ask the user for the issue key (or confirm none applies) before coding or claiming tracker progress; then `claim --jira KEY` or set `- Key:` on the requirement. Do not invent a key. 9. When `next` / status shows canvas readiness that blocks coding (Needs Analysis, Needs Clarification, Needs Redesign, Blocked), prefer `/sdlc-spdd-architect` (or prompt-update) over `/sdlc-spdd-code`, even if the stored phase is `code`. 10. Do not start unrelated work or implement code on a Work ID claimed by another teammate (non-stale). +11. Optional DIF check (never required). If the DIF CLI exists for the active + canvas, run `architect --quiet --canvas`. Exit 1: prefer `/sdlc-spdd-architect` over + `/sdlc-spdd-code` even if the stored phase is `code`. If the script is + missing, continue — that is not an error. Do not start a JVM from + `sdlc.sh next` or `sdlc.sh gate`. ## Output diff --git a/.github/prompts/sdlc-next.prompt.md b/.github/prompts/sdlc-next.prompt.md index 04607def..afb0c5aa 100644 --- a/.github/prompts/sdlc-next.prompt.md +++ b/.github/prompts/sdlc-next.prompt.md @@ -23,6 +23,11 @@ Show where the user is in the workflow and what to do next. Alias for `/sdlc-spd 8. When `next` / status / session brief shows Jira as `missing` or `draft`, ask the user for the issue key (or confirm none applies) before coding or claiming tracker progress; then `claim --jira KEY` or set `- Key:` on the requirement. Do not invent a key. 9. When `next` / status shows canvas readiness that blocks coding (Needs Analysis, Needs Clarification, Needs Redesign, Blocked), prefer `/sdlc-spdd-architect` (or prompt-update) over `/sdlc-spdd-code`, even if the stored phase is `code`. 10. Do not start unrelated work or implement code on a Work ID claimed by another teammate (non-stale). +11. Optional DIF check (never required). If the DIF CLI exists for the active + canvas, run `architect --quiet --canvas`. Exit 1: prefer `/sdlc-spdd-architect` over + `/sdlc-spdd-code` even if the stored phase is `code`. If the script is + missing, continue — that is not an error. Do not start a JVM from + `sdlc.sh next` or `sdlc.sh gate`. ## Output diff --git a/.github/prompts/sdlc-spdd-architect.prompt.md b/.github/prompts/sdlc-spdd-architect.prompt.md index 4ad698f0..12387742 100644 --- a/.github/prompts/sdlc-spdd-architect.prompt.md +++ b/.github/prompts/sdlc-spdd-architect.prompt.md @@ -33,6 +33,12 @@ Review and harden a REASONS Canvas before implementation. Do not implement code. 13. Mark whether the work is ready for coding by setting Metadata `- Readiness:` (or YAML frontmatter `readiness:`) to a **canvas readiness** vocabulary value (see Output). Prefer Title Case aliases agents already use. +14. Optional DIF check (never required). If `$DIF_HOME/scripts/dif-fold.sh` or a + sibling `../embabel-dif/scripts/dif-fold.sh` exists, run + `architect --quiet --canvas sdlc-spdd/spdd/canvas/.md`. Exit 1: set Readiness to + Needs Clarification; do not set Ready For Coding; quote the conflicts. If + the script is missing, continue — that is not an error. Do not start a JVM + from `sdlc.sh next` or `sdlc.sh gate`. ## Context Backend (runtime-resolved) diff --git a/.github/prompts/sdlc-spdd-code.prompt.md b/.github/prompts/sdlc-spdd-code.prompt.md index 506613cf..56f36e92 100644 --- a/.github/prompts/sdlc-spdd-code.prompt.md +++ b/.github/prompts/sdlc-spdd-code.prompt.md @@ -34,6 +34,20 @@ Implement exactly one approved operation from a REASONS Canvas. 12. Do not add dependencies unless the canvas allows it. 13. Update task status in the canvas and stage progress evidence via `./sdlc-spdd/scripts/sdlc.sh capture` (session record). 14. If the requested behavior conflicts with the canvas, stop and recommend `/sdlc-spdd-prompt-update` before changing code. +15. Optional DIF check (never required). If `$DIF_HOME/scripts/dif-fold.sh` or a + sibling `../embabel-dif/scripts/dif-fold.sh` exists, run + `architect --quiet --canvas` on this Work ID. Exit 1: STOP and recommend + `/sdlc-spdd-architect` — do not implement. If the script is missing, continue + — that is not an error. Do not start a JVM from `sdlc.sh next` or `sdlc.sh gate`. +16. Run Validation commands named on the selected canvas operation. If none, + discover the project's documented test/lint/typecheck commands when they + exist. +17. On failure: do not mark the T## complete. Return the command output AND + the Norm/Safeguard it maps to. Host may retry in-session. +18. If the same verify command fails twice with the same error, STOP. + Recommend `/sdlc-spdd-prompt-update` or shelf. Do not loop forever. +19. After edits: `git diff --name-only` must stay within the active T## + `Files:` plus test paths. Extra paths: do not mark complete. If no task is selected, ask the user which operation to implement before changing code. diff --git a/.github/prompts/sdlc-spdd-review.prompt.md b/.github/prompts/sdlc-spdd-review.prompt.md index b9a74a82..61ecdfe0 100644 --- a/.github/prompts/sdlc-spdd-review.prompt.md +++ b/.github/prompts/sdlc-spdd-review.prompt.md @@ -42,6 +42,30 @@ Review code changes against the REASONS Canvas. Do not make code changes unless `readiness:`) to **Reviewed** (or **Complete** if Final Status is also Complete). 20. Recommend `/sdlc-spdd-prompt-update` for behavior or requirement changes before additional code changes. 21. Recommend `/sdlc-spdd-sync` for accepted non-behavioral refactors after review. +22. Optional DIF check (never required). If `$DIF_HOME/scripts/dif-fold.sh` or a + sibling `../embabel-dif/scripts/dif-fold.sh` exists **and** snapshot files + `sdlc-spdd/spdd/snapshots/-before.json` and `sdlc-spdd/spdd/snapshots/-after.json` + exist, run `review --quiet --before … --after … --canvas sdlc-spdd/spdd/canvas/.md`. + Exit 1: do not set Reviewed / Approved; cite the one-line `dif=blocked` (and + `.gate.json` / VerificationResult). If the CLI or either snapshot is missing, + continue — that is not an error. Do not use login fixtures. Do not start a + JVM from `sdlc.sh next` or `sdlc.sh gate`. +23. Run the operation-diff-scope machine check: + `./scripts/check-operation-diff-scope.sh --work-id ` + (orchestrator: `./scripts/check-operation-diff-scope.sh`; installed + projects: `./sdlc-spdd/scripts/check-operation-diff-scope.sh` or + `python -m sdlc_engine.canvas --work-id `). Optional + `--base ` adds `git diff --name-only ...HEAD`. Optional + `--ops T01,T02` limits Files: to those operations; default is + the T## under review (in-progress/selected, else the last + completed T## — not the union of every completed T##). Changed paths + must be a subset of those Files: plus allowed test paths + (`tests/**`, `engine/tests_unit/**`, `engine/tests_integration/**`, + `engine/tests_e2e/**`, or basename `test_*.py` / `*_test.py` / + `*.spec.md`). Exit 1 (extra production paths or `..` traversal): + do not set Result to Approved or Approved With Notes — use Changes + Requested or Blocked and list the extra paths. Hunk-level C-DRIFT + inside an allowed file remains a human finding. ## Context Backend (runtime-resolved) diff --git a/.github/prompts/sdlc-spdd-whereami.prompt.md b/.github/prompts/sdlc-spdd-whereami.prompt.md index 4e504726..86c7e2c6 100644 --- a/.github/prompts/sdlc-spdd-whereami.prompt.md +++ b/.github/prompts/sdlc-spdd-whereami.prompt.md @@ -23,6 +23,11 @@ Show the user exactly where they are in the workflow and what to do next. Do not 8. When `next` / status / session brief shows Jira as `missing` or `draft`, ask the user for the issue key (or confirm none applies) before coding or claiming tracker progress; then `claim --jira KEY` or set `- Key:` on the requirement. Do not invent a key. 9. When `next` / status shows canvas readiness that blocks coding (Needs Analysis, Needs Clarification, Needs Redesign, Blocked), prefer `/sdlc-spdd-architect` (or prompt-update) over `/sdlc-spdd-code`, even if the stored phase is `code`. 10. Do not start unrelated work or implement code on a Work ID claimed by another teammate (non-stale). +11. Optional DIF check (never required). If the DIF CLI exists for the active + canvas, run `architect --quiet --canvas`. Exit 1: prefer `/sdlc-spdd-architect` over + `/sdlc-spdd-code` even if the stored phase is `code`. If the script is + missing, continue — that is not an error. Do not start a JVM from + `sdlc.sh next` or `sdlc.sh gate`. ## Output diff --git a/.github/workflows/validate-command-spec-generation.yml b/.github/workflows/validate-command-spec-generation.yml index f402b917..71141cc7 100644 --- a/.github/workflows/validate-command-spec-generation.yml +++ b/.github/workflows/validate-command-spec-generation.yml @@ -10,6 +10,9 @@ on: - 'sdlc-spdd/scripts/validate-command-adapters.sh' - '.github/workflows/validate-sdlc-spdd-adapters.yml' - '.cursor/commands/sdlc-*.md' + - '.claude/commands/sdlc-*.md' + - '.github/prompts/sdlc-*.prompt.md' + - 'scripts/lib/framework-install.sh' - '.cursor/rules/sdlc-spdd.mdc' - 'tests/test-command-specs.sh' - 'templates/cursor/sdlc-*.md' @@ -31,6 +34,9 @@ on: - 'sdlc-spdd/scripts/validate-command-adapters.sh' - '.github/workflows/validate-sdlc-spdd-adapters.yml' - '.cursor/commands/sdlc-*.md' + - '.claude/commands/sdlc-*.md' + - '.github/prompts/sdlc-*.prompt.md' + - 'scripts/lib/framework-install.sh' - '.cursor/rules/sdlc-spdd.mdc' - 'tests/test-command-specs.sh' - 'templates/cursor/sdlc-*.md' diff --git a/tests/test-command-specs.sh b/tests/test-command-specs.sh index 898830bd..21da6b0a 100755 --- a/tests/test-command-specs.sh +++ b/tests/test-command-specs.sh @@ -227,8 +227,31 @@ assert_dogfood_matches_rewritten_template() { rm -f "${expected}" } -assert_dogfood_matches_rewritten_template sdlc-spdd-code -assert_dogfood_matches_rewritten_template sdlc-spdd-review +# CHORE-006: every dogfood pack must equal its path-rewritten template. Content lag +# (missing I1/DIF steps) is a failure, not a warning. +assert_dogfood_pack_matches() { + local tdir="$1" ddir="$2" ext="$3" f b expected + for f in "${REPO_ROOT}/${tdir}"/*"${ext}"; do + b="$(basename "${f}")" + if [[ ! -f "${REPO_ROOT}/${ddir}/${b}" ]]; then + bad "dogfood ${ddir}/${b} missing (template exists)" + continue + fi + expected="$(mktemp)" + cp "${f}" "${expected}" + framework_rewrite_adapter_paths "${expected}" + if diff -q "${expected}" "${REPO_ROOT}/${ddir}/${b}" >/dev/null 2>&1; then + ok "dogfood ${ddir}/${b} matches rewritten template" + else + bad "dogfood ${ddir}/${b} drifted from ${tdir}/${b} beyond rewrite allowlist" + diff -u "${expected}" "${REPO_ROOT}/${ddir}/${b}" | head -20 >&2 || true + fi + rm -f "${expected}" + done +} +assert_dogfood_pack_matches templates/cursor .cursor/commands .md +assert_dogfood_pack_matches templates/claude/commands .claude/commands .md +assert_dogfood_pack_matches templates/copilot/prompts .github/prompts .prompt.md assert_contains "${REPO_ROOT}/.cursor/commands/sdlc-spdd-code.md" "Optional DIF check" \ "dogfood code has Optional DIF" assert_contains "${REPO_ROOT}/.cursor/commands/sdlc-spdd-review.md" "Optional DIF check" \ From 11740cf7ade7665e9b92cc4a5b74b6968d736dd6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 13 Sep 2026 23:34:58 +0000 Subject: [PATCH 06/22] DOC-004: docs describe the layout init actually creates. init-project.sh writes sdlc-spdd/docs/, but the install guide, hub, upgrade guide, cheat sheet, demo narration, and CONTRIBUTING said docs/sdlc-spdd/. first-day expected results now match init output (single sdlc-spdd/ home, no agent-context/). CONTRIBUTING's registry checklist names spdd/memory/registry.jsonl, not work-registry.tsv. README states storage v3 is the only supported layout instead of promising legacy consolidation. Shipped copies under sdlc-spdd/docs re-synced for the five changed guides. Co-authored-by: John Menke --- CONTRIBUTING.md | 10 +++++----- README.md | 6 +++--- docs/README.md | 6 +++--- docs/agent-session-scripts.md | 4 ++-- docs/demos/docgen.yaml | 2 +- docs/demos/hints/project-context.md | 2 +- docs/demos/hints/segment-02-install-and-workflow.md | 2 +- docs/demos/narration/02-install-and-workflow.md | 2 +- docs/first-day-with-sdlc-spdd.md | 9 ++++----- docs/framework-upgrade.md | 6 +++--- docs/installing-into-your-project.md | 8 ++++---- docs/sdlc-spdd-cheat-sheet.md | 2 +- sdlc-spdd/docs/agent-session-scripts.md | 4 ++-- sdlc-spdd/docs/first-day-with-sdlc-spdd.md | 9 ++++----- sdlc-spdd/docs/framework-upgrade.md | 6 +++--- sdlc-spdd/docs/installing-into-your-project.md | 8 ++++---- sdlc-spdd/docs/sdlc-spdd-cheat-sheet.md | 2 +- 17 files changed, 43 insertions(+), 45 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 06af79cc..92e61d79 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -50,7 +50,7 @@ installs into a target project. - **Shipped surfaces (must stay neutral):** `templates/**`, every `docs/*.md` that is *not* listed as orchestrator-only in - `scripts/lib/shipped-docs-boundary.sh` (installed as `docs/sdlc-spdd/`), the + `scripts/lib/shipped-docs-boundary.sh` (installed as `sdlc-spdd/docs/`), the grounding files (`templates/{claude/CLAUDE.md,copilot/copilot-instructions.md,cursor/rules/sdlc-spdd.mdc}`), and the `templates/agent-context/` harness files that install copies. @@ -91,9 +91,9 @@ Doc paths follow the same pattern: | Context | Documentation | |---------|---------------| | This orchestrator repo | `docs/*.md` | -| Installed target application | `docs/sdlc-spdd/*.md` | +| Installed target application | `sdlc-spdd/docs/*.md` | -Generated session briefs reference `docs/sdlc-spdd/…` because they are written for target projects. +Generated session briefs reference `sdlc-spdd/docs/…` because they are written for target projects. ## Command adapters and extensions @@ -142,7 +142,7 @@ Before merging doc or script changes that touch the three-part model (Planning, - [ ] **Workflow CLI** — daily runbook, first-day walkthrough, cheat sheet, and session prompt standard teach `sdlc.sh` (`next`, `claim`, `capture`) not raw scripts alone - [ ] **Workflow chat commands** — `/sdlc-claim`, `/sdlc-shelf`, `/sdlc-advance`, `/sdlc-next`, `/sdlc-team` appear alongside shell equivalents where users start/claim/orient -- [ ] **Pointer + team registry** — docs explain `.sdlc/pointer` / `.sdlc/workflows/` (local) vs `work-registry.tsv` (committed); claim/release commit reminder present +- [ ] **Pointer + team registry** — docs explain `.sdlc/pointer` / `.sdlc/workflows/` (local) vs `spdd/memory/registry.jsonl` (committed); claim/release commit reminder present - [ ] **Whereami / next** — `/sdlc-next` and `/sdlc-spdd-whereami` in assistant orientation tables (README, cheat sheet, cursor/copilot/claude usage) - [ ] **Jira draft** — milestone `## Jira` convention referenced from runbook, cheat sheet, planning prompts where Jira drafts appear - [ ] **Three-part mandate** — no Planning, SPDD, or SDLC artifacts removed or conflated; see [design mandate](docs/three-part-operating-path.md#three-part-design-mandate) @@ -159,7 +159,7 @@ Before merging doc or script changes that touch the three-part model (Planning, - [ ] **Script output** — if a script prints “next step” prompts, they align with the matching prompt standard doc - [ ] **Diagrams** — if you changed PlantUML under `docs/diagrams/*.puml`, `./scripts/render-diagrams.sh --check` passes; regenerate committed SVG exports with `./scripts/render-diagrams.sh` - [ ] **Daily doc roles** — prompts stay in `session-prompt-standard.md`; step table in `workflow.md`; rules/checklists in `daily-runbook.md`; Cursor/Copilot/Claude Code syntax in `initialization-and-invocation.md`; concepts in `useful-concepts-and-commands.md`; commands in `sdlc-spdd-cheat-sheet.md` (link, do not duplicate) -- [ ] **Target docs hub** — `docs/README.md` is orchestrator-only; installed projects use `templates/project-docs/docs-sdlc-spdd-README.md` → `docs/sdlc-spdd/README.md` (do not copy orchestrator `docs/README.md` to targets) +- [ ] **Target docs hub** — `docs/README.md` is orchestrator-only; installed projects use `templates/project-docs/docs-sdlc-spdd-README.md` → `sdlc-spdd/docs/README.md` (do not copy orchestrator `docs/README.md` to targets) - [ ] **Assistant vs shell** — `/sdlc-spdd-*` is chat (link [How to run assistant commands](docs/initialization-and-invocation.md#how-to-run-assistant-commands)); `./scripts/` is terminal - [ ] **Script paths** — install/setup from orchestrator `./scripts/`; daily/runtime in target `./sdlc-spdd/scripts/`; label which context in examples diff --git a/README.md b/README.md index 23e4562e..e60d5fec 100644 --- a/README.md +++ b/README.md @@ -303,9 +303,9 @@ your-app/ adf/ Optional checked-in ADF JSON ``` -Legacy sprawled installs (framework folders at the repo root, `agent-context/` -trees, `work-registry.tsv`) keep working read-only and are consolidated by -`upgrade` + `sdlc-engine storage migrate`. +Storage v3 is the only supported layout. Pre-v3 installs (framework folders at +the repo root, `agent-context/` trees, `work-registry.tsv`) are not migrated; +re-run `init-project.sh` on a clean checkout (Milestone 3, REF-002). | Path (this repo) | Purpose | | ---------------- | ------- | diff --git a/docs/README.md b/docs/README.md index 8169aa42..77ac8629 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,6 @@ # Documentation -**Orchestrator repo hub** — when SDLC-SPDD is installed into a target application, that project gets a leaner hub at `docs/sdlc-spdd/README.md` (same guides, clearer entry path). +**Orchestrator repo hub** — when SDLC-SPDD is installed into a target application, that project gets a leaner hub at `sdlc-spdd/docs/README.md` (same guides, clearer entry path). Use this hub to choose the right guide for your current task. If you are new, read the first section in order. If you are already operating a project, jump to the section that matches what you are doing. @@ -54,7 +54,7 @@ Jira link & sync / ADF viewer), [ADF templates + Vue3 console](adf-template-libr 5. [Maintaining your project](maintaining-your-project.md) — upgrades, memory hygiene, canvas sync, links, and session maintenance. 6. [Storage v3](storage-v3.md) — where memory lives: ledger, Guide working store, runtime, backends. -**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/`. +**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 `sdlc-spdd/docs/`. **Python engine:** [SDLC Engine](engine-v2.md) — reusable `sdlc_engine` package; `SDLC_ENGINE=auto|python|shell` on `scripts/sdlc.sh`. @@ -152,4 +152,4 @@ Also useful day to day: Follow the adoption path in the repository [README](../README.md#the-adoption-path): install → first day → three-part operating path → daily prompts. -After install, target-local copies of these docs live at `docs/sdlc-spdd/` in your application. Cursor/Copilot/Claude Code slash-command examples: [Initialization and invocation](initialization-and-invocation.md). +After install, target-local copies of these docs live at `sdlc-spdd/docs/` in your application. Cursor/Copilot/Claude Code slash-command examples: [Initialization and invocation](initialization-and-invocation.md). diff --git a/docs/agent-session-scripts.md b/docs/agent-session-scripts.md index c47d1747..28891745 100644 --- a/docs/agent-session-scripts.md +++ b/docs/agent-session-scripts.md @@ -63,7 +63,7 @@ repo root (see the [install layout diagram](diagrams/09-install-layout.svg) and `.github/prompts/`, `CLAUDE.md` + `.claude/commands/` - `.github/workflows/validate-sdlc-spdd-adapters.yml` when both Cursor and Copilot adapters are installed -- `docs/sdlc-spdd/` — target-local copies of these docs +- `sdlc-spdd/docs/` — target-local copies of these docs ## Upgrade an Older Installation @@ -75,7 +75,7 @@ Preview first: ./scripts/upgrade-project.sh --target /path/to/app --all --dry-run -The upgrade updates framework-owned prompts, harness/skills files, target-local docs under `docs/sdlc-spdd/`, and target-local runtime scripts. It preserves application source, application docs outside `docs/sdlc-spdd/`, requirements, canvases, reviews, sync logs, the lessons ledger, existing root `CLAUDE.md`, and target workflow customizations. Legacy memory layouts are converted by `sdlc-engine storage migrate` — see [Framework upgrade](framework-upgrade.md). +The upgrade updates framework-owned prompts, harness/skills files, target-local docs under `sdlc-spdd/docs/`, and target-local runtime scripts. It preserves application source, application docs outside `sdlc-spdd/docs/`, requirements, canvases, reviews, sync logs, the lessons ledger, existing root `CLAUDE.md`, and target workflow customizations. See [Framework upgrade](framework-upgrade.md). ## 2. Start a New Agent Session diff --git a/docs/demos/docgen.yaml b/docs/demos/docgen.yaml index 72c30c78..2b73e8ae 100644 --- a/docs/demos/docgen.yaml +++ b/docs/demos/docgen.yaml @@ -93,7 +93,7 @@ narration_from_source: run analysis with index-spdd-analysis.sh, then plan.' - Mention Work ID prefixes (FEAT, BUG, REF, SPIKE, DOC, TEST, CHORE) and that canvases must reach Ready For Coding before /sdlc-spdd-code. - - Close by noting deeper guides live in docs/README.md and docs/sdlc-spdd/ in target projects after install. + - Close by noting deeper guides live in docs/README.md and sdlc-spdd/docs/ in target projects after install. context: paths: - docs/first-day-with-sdlc-spdd.md diff --git a/docs/demos/hints/project-context.md b/docs/demos/hints/project-context.md index 23723688..c95947d2 100644 --- a/docs/demos/hints/project-context.md +++ b/docs/demos/hints/project-context.md @@ -16,7 +16,7 @@ docgen: Plain spoken paragraphs suitable for TTS; no markdown headings in narration output. - >- Emphasize the REASONS Canvas, Work IDs, and the three-part operating path (Planning, - SPDD, SDLC). Target projects receive templates and docs/sdlc-spdd/ — not Guide RAG + SPDD, SDLC). Target projects receive templates and sdlc-spdd/docs/ — not Guide RAG or this docgen bundle. context: paths: diff --git a/docs/demos/hints/segment-02-install-and-workflow.md b/docs/demos/hints/segment-02-install-and-workflow.md index abc6c0dc..b03c9063 100644 --- a/docs/demos/hints/segment-02-install-and-workflow.md +++ b/docs/demos/hints/segment-02-install-and-workflow.md @@ -21,7 +21,7 @@ docgen: Mention Work ID prefixes (FEAT, BUG, REF, SPIKE, DOC, TEST, CHORE) and that canvases must reach Ready For Coding before /sdlc-spdd-code. - >- - Close by noting deeper guides live in docs/README.md and docs/sdlc-spdd/ in target + Close by noting deeper guides live in docs/README.md and sdlc-spdd/docs/ in target projects after install. context: paths: diff --git a/docs/demos/narration/02-install-and-workflow.md b/docs/demos/narration/02-install-and-workflow.md index 57732cb8..8ccad6b9 100644 --- a/docs/demos/narration/02-install-and-workflow.md +++ b/docs/demos/narration/02-install-and-workflow.md @@ -8,4 +8,4 @@ The workflow table in the docs gives you the canonical sequence. Step one is set Work IDs use prefixes like FEAT, BUG, REF, SPIKE, DOC, TEST, and CHORE. The REASONS Canvas should reach Ready For Coding before you run slash sdlc-spdd-code on an operation. -After install, your target project gets a docs slash sdlc-spdd hub and the same guides under a leaner entry path. The orchestrator docs README remains the full reference for contributors extending the framework itself. Deeper guides live in docs/README.md and docs/sdlc-spdd/ in target projects after install. +After install, your target project gets a sdlc-spdd slash docs hub and the same guides under a leaner entry path. The orchestrator docs README remains the full reference for contributors extending the framework itself. Deeper guides live in docs/README.md and sdlc-spdd/docs/ in target projects after install. diff --git a/docs/first-day-with-sdlc-spdd.md b/docs/first-day-with-sdlc-spdd.md index d335fe80..3f3c9230 100644 --- a/docs/first-day-with-sdlc-spdd.md +++ b/docs/first-day-with-sdlc-spdd.md @@ -61,11 +61,10 @@ Then open the target application in Cursor, a Copilot-enabled editor, or Claude Expected result: -- `requirements/` exists. -- `spdd/` exists. -- `agent-context/` exists. -- `ROADMAP.md`, `milestone-1.md`, `requirements/milestones/`, and `session-notes/` exist when they were missing. -- stack information is captured in project memory. +- `sdlc-spdd/` exists — the single framework home. +- `sdlc-spdd/requirements/milestones/milestone-1/`, `sdlc-spdd/spdd/{canvas,analysis,tasks,reviews,sync,memory}/`, `sdlc-spdd/harness/`, `sdlc-spdd/docs/`, `sdlc-spdd/scripts/`, and `sdlc-spdd/session-notes/` exist. +- `sdlc-spdd/ROADMAP.md` and `sdlc-spdd/spdd/memory/lessons.jsonl` exist when they were missing. +- adapter stubs exist at the repo root (`.cursor/`, `.github/`, `.claude/`, `CLAUDE.md`) for the assistants you selected. - no application source code is changed. ## 3. Create or Choose a Work ID diff --git a/docs/framework-upgrade.md b/docs/framework-upgrade.md index 17a52d60..8702baf3 100644 --- a/docs/framework-upgrade.md +++ b/docs/framework-upgrade.md @@ -11,7 +11,7 @@ The upgrade script updates **framework-owned files only**. It does not overwrite: - application source code -- application docs outside `docs/sdlc-spdd/` +- application docs outside `sdlc-spdd/docs/` - existing `ROADMAP.md` - existing `milestone-*.md` - existing `session-notes/` @@ -30,7 +30,7 @@ It can update: - `.github/prompts/sdlc-spdd-*.prompt.md` - `.claude/commands/sdlc-spdd-*.md` - `harness/*.md` and `harness/skills/*.md` -- `docs/sdlc-spdd/*.md` +- `sdlc-spdd/docs/*.md` - `sdlc-spdd/scripts/*.sh` (including `sdlc.sh`) It can create when missing: @@ -144,7 +144,7 @@ After upgrade, review: - `.github/copilot-instructions.md` if your project had custom Copilot rules - `CLAUDE.md` if the upgrade created it for the first time - `harness/skills/` if your team added custom `#SkillName` files -- SDLC-SPDD docs if your team edited `docs/sdlc-spdd/` +- SDLC-SPDD docs if your team edited `sdlc-spdd/docs/` - `spdd/memory/registry.jsonl` claims after template merge - backup folder for any local framework changes worth reapplying diff --git a/docs/installing-into-your-project.md b/docs/installing-into-your-project.md index 3b24e689..91696923 100644 --- a/docs/installing-into-your-project.md +++ b/docs/installing-into-your-project.md @@ -64,7 +64,7 @@ This installs a single framework home at `/sdlc-spdd/`. See the - `.github/copilot-instructions.md` and `.github/prompts/` - `CLAUDE.md` and `.claude/commands/` - `.github/workflows/validate-sdlc-spdd-adapters.yml` (when both Cursor and Copilot adapters are installed) -- `docs/sdlc-spdd/` — target-local doc hub (`docs/sdlc-spdd/README.md`) +- `sdlc-spdd/docs/` — target-local doc hub (`sdlc-spdd/docs/README.md`) **Gitignored runtime** (created on first use, not committed): @@ -217,17 +217,17 @@ Avoid hand-editing generated framework prompt files unless you intend to keep lo - `.claude/commands/sdlc-spdd-*.md` - `CLAUDE.md` - `sdlc-spdd/scripts/*.sh` -- `docs/sdlc-spdd/*.md` +- `sdlc-spdd/docs/*.md` - `spdd/memory/lessons.jsonl` and `spdd/memory/registry.jsonl` (written only by `accept` and `claim`/`release`) Team-specific process guidance and custom skills belong in: - `sdlc-spdd/harness/skills/` — add `.md` skill files; request with `#SkillName` -- project docs outside `docs/sdlc-spdd/` +- project docs outside `sdlc-spdd/docs/` **Local agent state** (gitignored, do not commit): `.sdlc/pointer`, `.sdlc/workflows/`, `.sdlc/sessions/`, `.sdlc/staged/`. -Keep application-specific documentation outside `docs/sdlc-spdd/` so framework upgrades can refresh SDLC-SPDD docs safely. +Keep application-specific documentation outside `sdlc-spdd/docs/` so framework upgrades can refresh SDLC-SPDD docs safely. ## Troubleshooting diff --git a/docs/sdlc-spdd-cheat-sheet.md b/docs/sdlc-spdd-cheat-sheet.md index 70d1daa2..f25e3872 100644 --- a/docs/sdlc-spdd-cheat-sheet.md +++ b/docs/sdlc-spdd-cheat-sheet.md @@ -35,7 +35,7 @@ One assistant only (advanced): Target-local docs: - docs/sdlc-spdd/ + sdlc-spdd/docs/ Project planning: diff --git a/sdlc-spdd/docs/agent-session-scripts.md b/sdlc-spdd/docs/agent-session-scripts.md index c47d1747..28891745 100644 --- a/sdlc-spdd/docs/agent-session-scripts.md +++ b/sdlc-spdd/docs/agent-session-scripts.md @@ -63,7 +63,7 @@ repo root (see the [install layout diagram](diagrams/09-install-layout.svg) and `.github/prompts/`, `CLAUDE.md` + `.claude/commands/` - `.github/workflows/validate-sdlc-spdd-adapters.yml` when both Cursor and Copilot adapters are installed -- `docs/sdlc-spdd/` — target-local copies of these docs +- `sdlc-spdd/docs/` — target-local copies of these docs ## Upgrade an Older Installation @@ -75,7 +75,7 @@ Preview first: ./scripts/upgrade-project.sh --target /path/to/app --all --dry-run -The upgrade updates framework-owned prompts, harness/skills files, target-local docs under `docs/sdlc-spdd/`, and target-local runtime scripts. It preserves application source, application docs outside `docs/sdlc-spdd/`, requirements, canvases, reviews, sync logs, the lessons ledger, existing root `CLAUDE.md`, and target workflow customizations. Legacy memory layouts are converted by `sdlc-engine storage migrate` — see [Framework upgrade](framework-upgrade.md). +The upgrade updates framework-owned prompts, harness/skills files, target-local docs under `sdlc-spdd/docs/`, and target-local runtime scripts. It preserves application source, application docs outside `sdlc-spdd/docs/`, requirements, canvases, reviews, sync logs, the lessons ledger, existing root `CLAUDE.md`, and target workflow customizations. See [Framework upgrade](framework-upgrade.md). ## 2. Start a New Agent Session diff --git a/sdlc-spdd/docs/first-day-with-sdlc-spdd.md b/sdlc-spdd/docs/first-day-with-sdlc-spdd.md index d335fe80..3f3c9230 100644 --- a/sdlc-spdd/docs/first-day-with-sdlc-spdd.md +++ b/sdlc-spdd/docs/first-day-with-sdlc-spdd.md @@ -61,11 +61,10 @@ Then open the target application in Cursor, a Copilot-enabled editor, or Claude Expected result: -- `requirements/` exists. -- `spdd/` exists. -- `agent-context/` exists. -- `ROADMAP.md`, `milestone-1.md`, `requirements/milestones/`, and `session-notes/` exist when they were missing. -- stack information is captured in project memory. +- `sdlc-spdd/` exists — the single framework home. +- `sdlc-spdd/requirements/milestones/milestone-1/`, `sdlc-spdd/spdd/{canvas,analysis,tasks,reviews,sync,memory}/`, `sdlc-spdd/harness/`, `sdlc-spdd/docs/`, `sdlc-spdd/scripts/`, and `sdlc-spdd/session-notes/` exist. +- `sdlc-spdd/ROADMAP.md` and `sdlc-spdd/spdd/memory/lessons.jsonl` exist when they were missing. +- adapter stubs exist at the repo root (`.cursor/`, `.github/`, `.claude/`, `CLAUDE.md`) for the assistants you selected. - no application source code is changed. ## 3. Create or Choose a Work ID diff --git a/sdlc-spdd/docs/framework-upgrade.md b/sdlc-spdd/docs/framework-upgrade.md index 17a52d60..8702baf3 100644 --- a/sdlc-spdd/docs/framework-upgrade.md +++ b/sdlc-spdd/docs/framework-upgrade.md @@ -11,7 +11,7 @@ The upgrade script updates **framework-owned files only**. It does not overwrite: - application source code -- application docs outside `docs/sdlc-spdd/` +- application docs outside `sdlc-spdd/docs/` - existing `ROADMAP.md` - existing `milestone-*.md` - existing `session-notes/` @@ -30,7 +30,7 @@ It can update: - `.github/prompts/sdlc-spdd-*.prompt.md` - `.claude/commands/sdlc-spdd-*.md` - `harness/*.md` and `harness/skills/*.md` -- `docs/sdlc-spdd/*.md` +- `sdlc-spdd/docs/*.md` - `sdlc-spdd/scripts/*.sh` (including `sdlc.sh`) It can create when missing: @@ -144,7 +144,7 @@ After upgrade, review: - `.github/copilot-instructions.md` if your project had custom Copilot rules - `CLAUDE.md` if the upgrade created it for the first time - `harness/skills/` if your team added custom `#SkillName` files -- SDLC-SPDD docs if your team edited `docs/sdlc-spdd/` +- SDLC-SPDD docs if your team edited `sdlc-spdd/docs/` - `spdd/memory/registry.jsonl` claims after template merge - backup folder for any local framework changes worth reapplying diff --git a/sdlc-spdd/docs/installing-into-your-project.md b/sdlc-spdd/docs/installing-into-your-project.md index 3b24e689..91696923 100644 --- a/sdlc-spdd/docs/installing-into-your-project.md +++ b/sdlc-spdd/docs/installing-into-your-project.md @@ -64,7 +64,7 @@ This installs a single framework home at `/sdlc-spdd/`. See the - `.github/copilot-instructions.md` and `.github/prompts/` - `CLAUDE.md` and `.claude/commands/` - `.github/workflows/validate-sdlc-spdd-adapters.yml` (when both Cursor and Copilot adapters are installed) -- `docs/sdlc-spdd/` — target-local doc hub (`docs/sdlc-spdd/README.md`) +- `sdlc-spdd/docs/` — target-local doc hub (`sdlc-spdd/docs/README.md`) **Gitignored runtime** (created on first use, not committed): @@ -217,17 +217,17 @@ Avoid hand-editing generated framework prompt files unless you intend to keep lo - `.claude/commands/sdlc-spdd-*.md` - `CLAUDE.md` - `sdlc-spdd/scripts/*.sh` -- `docs/sdlc-spdd/*.md` +- `sdlc-spdd/docs/*.md` - `spdd/memory/lessons.jsonl` and `spdd/memory/registry.jsonl` (written only by `accept` and `claim`/`release`) Team-specific process guidance and custom skills belong in: - `sdlc-spdd/harness/skills/` — add `.md` skill files; request with `#SkillName` -- project docs outside `docs/sdlc-spdd/` +- project docs outside `sdlc-spdd/docs/` **Local agent state** (gitignored, do not commit): `.sdlc/pointer`, `.sdlc/workflows/`, `.sdlc/sessions/`, `.sdlc/staged/`. -Keep application-specific documentation outside `docs/sdlc-spdd/` so framework upgrades can refresh SDLC-SPDD docs safely. +Keep application-specific documentation outside `sdlc-spdd/docs/` so framework upgrades can refresh SDLC-SPDD docs safely. ## Troubleshooting diff --git a/sdlc-spdd/docs/sdlc-spdd-cheat-sheet.md b/sdlc-spdd/docs/sdlc-spdd-cheat-sheet.md index 70d1daa2..f25e3872 100644 --- a/sdlc-spdd/docs/sdlc-spdd-cheat-sheet.md +++ b/sdlc-spdd/docs/sdlc-spdd-cheat-sheet.md @@ -35,7 +35,7 @@ One assistant only (advanced): Target-local docs: - docs/sdlc-spdd/ + sdlc-spdd/docs/ Project planning: From ec067a45035410cd8e58c4210b747721d7d54d39 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 13 Sep 2026 23:39:55 +0000 Subject: [PATCH 07/22] =?UTF-8?q?CHORE-007=20slice:=20fix=20in-tree=20brok?= =?UTF-8?q?en=20doc=20links=20(95=20=E2=86=92=2070).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Orchestrator docs pointed at agent-context/README.md, requirements/, and ROADMAP.md at the repo root; they live under templates/ and sdlc-spdd/. Shipped copies keep target-relative links (../requirements/) and drop the link to the non-shipped agent-context README. Remaining 70 are install-boundary links from shipped docs to orchestrator-only docs; CHORE-007 proper fixes the verifier. Co-authored-by: John Menke --- README.md | 2 +- docs/README.md | 6 +++--- docs/contributing-command-specs.md | 2 +- docs/daily-runbook.md | 4 ++-- docs/planning-prompt-standard.md | 2 +- docs/research/jira-adf-and-requirements-sync.md | 2 +- docs/roadmap-milestones-and-session-notes.md | 4 ++-- docs/sdlc-spdd-cheat-sheet.md | 2 +- docs/useful-concepts-and-commands.md | 4 ++-- sdlc-spdd/docs/daily-runbook.md | 2 +- .../docs/roadmap-milestones-and-session-notes.md | 4 ++-- sdlc-spdd/docs/useful-concepts-and-commands.md | 2 +- .../2026-07-15-catch-up-branch-evaluation.md | 14 +++++++------- 13 files changed, 25 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index e60d5fec..848a49c8 100644 --- a/README.md +++ b/README.md @@ -365,7 +365,7 @@ resumable, and reusable across assistants and teammates. We develop this framework through its own Work IDs (`sdlc-spdd/spdd/canvas/`, `sdlc-spdd/requirements/milestones/`). See [CONTRIBUTING.md](CONTRIBUTING.md) for script-path rules (orchestrator vs target) and -[ROADMAP.md](ROADMAP.md) for delivery posture. +[ROADMAP.md](sdlc-spdd/ROADMAP.md) for delivery posture. This checkout commits `.cursor/environment.json` so Cloud Agents run `.cursor/install.sh` and get `sdlc-engine`. `init` / `upgrade` do **not** copy diff --git a/docs/README.md b/docs/README.md index 77ac8629..e22069aa 100644 --- a/docs/README.md +++ b/docs/README.md @@ -54,7 +54,7 @@ Jira link & sync / ADF viewer), [ADF templates + Vue3 console](adf-template-libr 5. [Maintaining your project](maintaining-your-project.md) — upgrades, memory hygiene, canvas sync, links, and session maintenance. 6. [Storage v3](storage-v3.md) — where memory lives: ledger, Guide working store, runtime, backends. -**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 `sdlc-spdd/docs/`. +**Workflow CLI reference** (pointer, phase tracking, team registry): [agent-context/README.md](../templates/agent-context/README.md#sdlc-pointer-current-choretask) — installed in target projects at `sdlc-spdd/scripts/` with docs under `sdlc-spdd/docs/`. **Python engine:** [SDLC Engine](engine-v2.md) — reusable `sdlc_engine` package; `SDLC_ENGINE=auto|python|shell` on `scripts/sdlc.sh`. @@ -94,7 +94,7 @@ Also useful day to day: | [Roadmap, milestones, and session notes](roadmap-milestones-and-session-notes.md) | You use `ROADMAP.md`, `milestone-*.md`, and `session-notes/` to track project progress | | [Top useful concepts and commands](useful-concepts-and-commands.md) | You want definitions for Work ID, canvas, sync, memory | | [Cheat sheet](sdlc-spdd-cheat-sheet.md) | You want a one-page PDF-friendly **command** reference | -| [agent-context/README.md](../agent-context/README.md) | You need workflow CLI, pointer, or team registry detail | +| [agent-context/README.md](../templates/agent-context/README.md) | You need workflow CLI, pointer, or team registry detail | ## If You Are Integrating with Jira or GitHub @@ -146,7 +146,7 @@ Also useful day to day: |-------|-------------| | [Java Spring Boot usage](java-spring-boot-usage.md) | You are applying SDLC-SPDD to a Spring Boot project | | [Tekton usage](tekton-usage.md) | You are applying SDLC-SPDD to Tekton pipelines | -| [Roadmap](../ROADMAP.md) | Planned capabilities (repo root, not docs/) | +| [Roadmap](../sdlc-spdd/ROADMAP.md) | Planned capabilities (repo root, not docs/) | ## Quick Start (one path) diff --git a/docs/contributing-command-specs.md b/docs/contributing-command-specs.md index 97fc1fb0..8c5aa212 100644 --- a/docs/contributing-command-specs.md +++ b/docs/contributing-command-specs.md @@ -144,4 +144,4 @@ If templates were edited by hand (avoid — prefer spec edits), refresh specs: - [spec/commands/README.md](../spec/commands/README.md) — format reference - [TESTING.md](../TESTING.md) — command and adapter test stack -- Canvas: [spdd/canvas/FEAT-002-command-spec-generation.md](../spdd/canvas/FEAT-002-command-spec-generation.md) +- Canvas: `FEAT-002-command-spec-generation` (archived after Milestone 1; see the [ROADMAP Work Map](../sdlc-spdd/ROADMAP.md)) diff --git a/docs/daily-runbook.md b/docs/daily-runbook.md index 9e36014f..e1c196e1 100644 --- a/docs/daily-runbook.md +++ b/docs/daily-runbook.md @@ -9,7 +9,7 @@ Use this runbook for **operational rhythm**: rules, script sequences, phase chec | Step order (1–13) | [Workflow](workflow.md) | | Planning → SPDD → SDLC loop | [Three-part operating path](three-part-operating-path.md) | | **Rules, scripts, checklists** | **This page** | -| Workflow CLI + team registry (reference) | [agent-context/README.md](../agent-context/README.md#sdlc-pointer-current-choretask) | +| Workflow CLI + team registry (reference) | [agent-context/README.md](../templates/agent-context/README.md#sdlc-pointer-current-choretask) | ## Daily Operating Rules @@ -165,7 +165,7 @@ Create new Jira issue draft in the milestone requirement file first: requirements/milestones/.md → ## Jira section (Key, Summary, Type, …) -See [requirements/milestones/README.md](../requirements/milestones/README.md) and [jira-runbook.md](jira-runbook.md). On claim, `./sdlc-spdd/scripts/sdlc.sh claim ` auto-links the Key into the registry event note (`spdd/memory/registry.jsonl`). +See [requirements/milestones/README.md](../sdlc-spdd/requirements/milestones/README.md) and [jira-runbook.md](jira-runbook.md). On claim, `./sdlc-spdd/scripts/sdlc.sh claim ` auto-links the Key into the registry event note (`spdd/memory/registry.jsonl`). Then draft for Jira UI or automation: diff --git a/docs/planning-prompt-standard.md b/docs/planning-prompt-standard.md index 0d886f16..43f2424b 100644 --- a/docs/planning-prompt-standard.md +++ b/docs/planning-prompt-standard.md @@ -35,7 +35,7 @@ Single item: ./sdlc-spdd/scripts/create-work-from-milestone.sh --target . --milestone milestone-1.md --item "Add order status API" --type feature -The script prints **Next SPDD prompts** for each created Work ID. Each stub includes a scaffolded `## Jira` section — see [requirements/milestones/README.md](../requirements/milestones/README.md). +The script prints **Next SPDD prompts** for each created Work ID. Each stub includes a scaffolded `## Jira` section — see [requirements/milestones/README.md](../sdlc-spdd/requirements/milestones/README.md). ### Plan with milestone context diff --git a/docs/research/jira-adf-and-requirements-sync.md b/docs/research/jira-adf-and-requirements-sync.md index 1b989644..d6ebce40 100644 --- a/docs/research/jira-adf-and-requirements-sync.md +++ b/docs/research/jira-adf-and-requirements-sync.md @@ -233,7 +233,7 @@ Useful mappings: | `` `code` `` | `{{code}}` | | `- item` | `* item` | | `1. item` | `# item` | -| `[label](url)` | `[label\|url]` | +| `[label](https://example.com)` | `[label\|https://example.com]` | | fenced code | `{code}...{code}` | Our engine already implements `markdown_to_wiki()` for this path. diff --git a/docs/roadmap-milestones-and-session-notes.md b/docs/roadmap-milestones-and-session-notes.md index 7ef85395..2cbe692e 100644 --- a/docs/roadmap-milestones-and-session-notes.md +++ b/docs/roadmap-milestones-and-session-notes.md @@ -185,8 +185,8 @@ Keep the roadmap high level: ## Milestones - - [ ] [Milestone 1](milestone-1.md) - - [ ] [Milestone 2](milestone-2.md) + - [ ] Milestone 1 — requirements/milestones/milestone-1/MILESTONE-1.md + - [ ] Milestone 2 — requirements/milestones/milestone-2/MILESTONE-2.md ## Current Focus diff --git a/docs/sdlc-spdd-cheat-sheet.md b/docs/sdlc-spdd-cheat-sheet.md index f25e3872..2d2e9470 100644 --- a/docs/sdlc-spdd-cheat-sheet.md +++ b/docs/sdlc-spdd-cheat-sheet.md @@ -227,7 +227,7 @@ Create Jira draft in the milestone requirement file: requirements/milestones/.md → ## Jira (Key, Summary, Type, Acceptance, …) -See [requirements/milestones/README.md](../requirements/milestones/README.md). On claim, `./sdlc-spdd/scripts/sdlc.sh claim ` auto-links the Key into the registry event note. +See [requirements/milestones/README.md](../sdlc-spdd/requirements/milestones/README.md). On claim, `./sdlc-spdd/scripts/sdlc.sh claim ` auto-links the Key into the registry event note. Draft for Jira UI: diff --git a/docs/useful-concepts-and-commands.md b/docs/useful-concepts-and-commands.md index 0110c8cf..9786f646 100644 --- a/docs/useful-concepts-and-commands.md +++ b/docs/useful-concepts-and-commands.md @@ -138,7 +138,7 @@ The active Work ID on **this machine**. Stored in `.sdlc/pointer` (gitignored). ./sdlc-spdd/scripts/sdlc.sh claim # sets pointer + team registry ./agent-context/sdlc-pointer.sh get -See [agent-context/README.md](../agent-context/README.md#sdlc-pointer-current-choretask). +See [agent-context/README.md](../templates/agent-context/README.md#sdlc-pointer-current-choretask). ### Workflow CLI @@ -195,7 +195,7 @@ Common files: - `milestone-2.md` - `session-notes/YYYY-MM-DD.md` -Milestone requirements may include a `## Jira` draft section (`requirements/milestones/.md`). On claim, `./sdlc-spdd/scripts/sdlc.sh claim ` auto-links the Jira Key into the team registry. See [requirements/milestones/README.md](../requirements/milestones/README.md). +Milestone requirements may include a `## Jira` draft section (`requirements/milestones/.md`). On claim, `./sdlc-spdd/scripts/sdlc.sh claim ` auto-links the Jira Key into the team registry. See [requirements/milestones/README.md](../sdlc-spdd/requirements/milestones/README.md). Use roadmap and milestone docs to give planning agents delivery context. Use REASONS Canvas files to govern each Work ID. diff --git a/sdlc-spdd/docs/daily-runbook.md b/sdlc-spdd/docs/daily-runbook.md index 9e36014f..fae5278c 100644 --- a/sdlc-spdd/docs/daily-runbook.md +++ b/sdlc-spdd/docs/daily-runbook.md @@ -9,7 +9,7 @@ Use this runbook for **operational rhythm**: rules, script sequences, phase chec | Step order (1–13) | [Workflow](workflow.md) | | Planning → SPDD → SDLC loop | [Three-part operating path](three-part-operating-path.md) | | **Rules, scripts, checklists** | **This page** | -| Workflow CLI + team registry (reference) | [agent-context/README.md](../agent-context/README.md#sdlc-pointer-current-choretask) | +| Workflow CLI + team registry (reference) | `./sdlc-spdd/scripts/sdlc.sh --help` | ## Daily Operating Rules diff --git a/sdlc-spdd/docs/roadmap-milestones-and-session-notes.md b/sdlc-spdd/docs/roadmap-milestones-and-session-notes.md index 7ef85395..2cbe692e 100644 --- a/sdlc-spdd/docs/roadmap-milestones-and-session-notes.md +++ b/sdlc-spdd/docs/roadmap-milestones-and-session-notes.md @@ -185,8 +185,8 @@ Keep the roadmap high level: ## Milestones - - [ ] [Milestone 1](milestone-1.md) - - [ ] [Milestone 2](milestone-2.md) + - [ ] Milestone 1 — requirements/milestones/milestone-1/MILESTONE-1.md + - [ ] Milestone 2 — requirements/milestones/milestone-2/MILESTONE-2.md ## Current Focus diff --git a/sdlc-spdd/docs/useful-concepts-and-commands.md b/sdlc-spdd/docs/useful-concepts-and-commands.md index 0110c8cf..3d537ee1 100644 --- a/sdlc-spdd/docs/useful-concepts-and-commands.md +++ b/sdlc-spdd/docs/useful-concepts-and-commands.md @@ -138,7 +138,7 @@ The active Work ID on **this machine**. Stored in `.sdlc/pointer` (gitignored). ./sdlc-spdd/scripts/sdlc.sh claim # sets pointer + team registry ./agent-context/sdlc-pointer.sh get -See [agent-context/README.md](../agent-context/README.md#sdlc-pointer-current-choretask). +See `./sdlc-spdd/scripts/sdlc.sh --help`. ### Workflow CLI diff --git a/sdlc-spdd/session-notes/2026-07-15-catch-up-branch-evaluation.md b/sdlc-spdd/session-notes/2026-07-15-catch-up-branch-evaluation.md index 24abc3e6..80c4c1c6 100644 --- a/sdlc-spdd/session-notes/2026-07-15-catch-up-branch-evaluation.md +++ b/sdlc-spdd/session-notes/2026-07-15-catch-up-branch-evaluation.md @@ -1,6 +1,6 @@ # Catch-up: unmerged branches and what to do next -> **Superseded:** See [TESTING.md](../TESTING.md#integration-merge-gate) and `./tests/test-integration-merge.sh`. +> **Superseded:** See [TESTING.md](../../TESTING.md#integration-merge-gate) and `./tests/test-integration-merge.sh`. > integration PR [#27](https://github.com/jmjava/sdlc-spdd-orchestrator/pull/27). > Do not merge PR #25/#26 separately — workflow commands and FEAT-001–003 are on > `cursor/integration-981e`. Keep this note for the historical remote-branch table. @@ -26,7 +26,7 @@ Only **two branches** carry real unmerged work. The other **11** remote branches ## Delivery posture reminder -From [ROADMAP.md](../ROADMAP.md) and [milestone-1.md](../milestone-1.md): +From [ROADMAP.md](../ROADMAP.md) and [milestone-1.md](../requirements/milestones/milestone-1/MILESTONE-1.md): | Stage | Status | Focus | |-------|--------|-------| @@ -116,7 +116,7 @@ git checkout cursor/spike-guide-ingest-agent-context-17f4 ## Milestone 1 — make it right backlog (no branch yet) -From [milestone-1.md](../milestone-1.md), execution order: +From [milestone-1.md](../requirements/milestones/milestone-1/MILESTONE-1.md), execution order: | Work ID | Canvas | Status | Next step | |---------|--------|--------|-----------| @@ -205,7 +205,7 @@ gh issue list --state open ## Related artifacts - Roadmap: [ROADMAP.md](../ROADMAP.md) -- Active milestone: [milestone-1.md](../milestone-1.md) -- Issue spec (workflow commands): [issues/ENHANCEMENT-agent-commands-for-workflow.md](../issues/ENHANCEMENT-agent-commands-for-workflow.md) -- SPIKE canvas: [spdd/canvas/SPIKE-001-guide-rag-context-backend.md](../spdd/canvas/SPIKE-001-guide-rag-context-backend.md) -- FEAT-001 canvas: [spdd/canvas/FEAT-001-shared-script-library.md](../spdd/canvas/FEAT-001-shared-script-library.md) +- Active milestone: [milestone-1.md](../requirements/milestones/milestone-1/MILESTONE-1.md) +- Issue spec (workflow commands): `issues/ENHANCEMENT-agent-commands-for-workflow.md` (removed; superseded by `/sdlc-claim`, `/sdlc-next`, `/sdlc-team`) +- SPIKE canvas: `spdd/canvas/SPIKE-001-guide-rag-context-backend.md` (archived) +- FEAT-001 canvas: `spdd/canvas/FEAT-001-shared-script-library.md` (archived) From 8e66d41dbd901a3e7099f5d010b15cea3fe43a42 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 13 Sep 2026 23:41:47 +0000 Subject: [PATCH 08/22] =?UTF-8?q?Revert=20"CHORE-007=20slice:=20fix=20in-t?= =?UTF-8?q?ree=20broken=20doc=20links=20(95=20=E2=86=92=2070)."?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit ec067a45035410cd8e58c4210b747721d7d54d39. --- README.md | 2 +- docs/README.md | 6 +++--- docs/contributing-command-specs.md | 2 +- docs/daily-runbook.md | 4 ++-- docs/planning-prompt-standard.md | 2 +- docs/research/jira-adf-and-requirements-sync.md | 2 +- docs/roadmap-milestones-and-session-notes.md | 4 ++-- docs/sdlc-spdd-cheat-sheet.md | 2 +- docs/useful-concepts-and-commands.md | 4 ++-- sdlc-spdd/docs/daily-runbook.md | 2 +- .../docs/roadmap-milestones-and-session-notes.md | 4 ++-- sdlc-spdd/docs/useful-concepts-and-commands.md | 2 +- .../2026-07-15-catch-up-branch-evaluation.md | 14 +++++++------- 13 files changed, 25 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 848a49c8..e60d5fec 100644 --- a/README.md +++ b/README.md @@ -365,7 +365,7 @@ resumable, and reusable across assistants and teammates. We develop this framework through its own Work IDs (`sdlc-spdd/spdd/canvas/`, `sdlc-spdd/requirements/milestones/`). See [CONTRIBUTING.md](CONTRIBUTING.md) for script-path rules (orchestrator vs target) and -[ROADMAP.md](sdlc-spdd/ROADMAP.md) for delivery posture. +[ROADMAP.md](ROADMAP.md) for delivery posture. This checkout commits `.cursor/environment.json` so Cloud Agents run `.cursor/install.sh` and get `sdlc-engine`. `init` / `upgrade` do **not** copy diff --git a/docs/README.md b/docs/README.md index e22069aa..77ac8629 100644 --- a/docs/README.md +++ b/docs/README.md @@ -54,7 +54,7 @@ Jira link & sync / ADF viewer), [ADF templates + Vue3 console](adf-template-libr 5. [Maintaining your project](maintaining-your-project.md) — upgrades, memory hygiene, canvas sync, links, and session maintenance. 6. [Storage v3](storage-v3.md) — where memory lives: ledger, Guide working store, runtime, backends. -**Workflow CLI reference** (pointer, phase tracking, team registry): [agent-context/README.md](../templates/agent-context/README.md#sdlc-pointer-current-choretask) — installed in target projects at `sdlc-spdd/scripts/` with docs under `sdlc-spdd/docs/`. +**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 `sdlc-spdd/docs/`. **Python engine:** [SDLC Engine](engine-v2.md) — reusable `sdlc_engine` package; `SDLC_ENGINE=auto|python|shell` on `scripts/sdlc.sh`. @@ -94,7 +94,7 @@ Also useful day to day: | [Roadmap, milestones, and session notes](roadmap-milestones-and-session-notes.md) | You use `ROADMAP.md`, `milestone-*.md`, and `session-notes/` to track project progress | | [Top useful concepts and commands](useful-concepts-and-commands.md) | You want definitions for Work ID, canvas, sync, memory | | [Cheat sheet](sdlc-spdd-cheat-sheet.md) | You want a one-page PDF-friendly **command** reference | -| [agent-context/README.md](../templates/agent-context/README.md) | You need workflow CLI, pointer, or team registry detail | +| [agent-context/README.md](../agent-context/README.md) | You need workflow CLI, pointer, or team registry detail | ## If You Are Integrating with Jira or GitHub @@ -146,7 +146,7 @@ Also useful day to day: |-------|-------------| | [Java Spring Boot usage](java-spring-boot-usage.md) | You are applying SDLC-SPDD to a Spring Boot project | | [Tekton usage](tekton-usage.md) | You are applying SDLC-SPDD to Tekton pipelines | -| [Roadmap](../sdlc-spdd/ROADMAP.md) | Planned capabilities (repo root, not docs/) | +| [Roadmap](../ROADMAP.md) | Planned capabilities (repo root, not docs/) | ## Quick Start (one path) diff --git a/docs/contributing-command-specs.md b/docs/contributing-command-specs.md index 8c5aa212..97fc1fb0 100644 --- a/docs/contributing-command-specs.md +++ b/docs/contributing-command-specs.md @@ -144,4 +144,4 @@ If templates were edited by hand (avoid — prefer spec edits), refresh specs: - [spec/commands/README.md](../spec/commands/README.md) — format reference - [TESTING.md](../TESTING.md) — command and adapter test stack -- Canvas: `FEAT-002-command-spec-generation` (archived after Milestone 1; see the [ROADMAP Work Map](../sdlc-spdd/ROADMAP.md)) +- Canvas: [spdd/canvas/FEAT-002-command-spec-generation.md](../spdd/canvas/FEAT-002-command-spec-generation.md) diff --git a/docs/daily-runbook.md b/docs/daily-runbook.md index e1c196e1..9e36014f 100644 --- a/docs/daily-runbook.md +++ b/docs/daily-runbook.md @@ -9,7 +9,7 @@ Use this runbook for **operational rhythm**: rules, script sequences, phase chec | Step order (1–13) | [Workflow](workflow.md) | | Planning → SPDD → SDLC loop | [Three-part operating path](three-part-operating-path.md) | | **Rules, scripts, checklists** | **This page** | -| Workflow CLI + team registry (reference) | [agent-context/README.md](../templates/agent-context/README.md#sdlc-pointer-current-choretask) | +| Workflow CLI + team registry (reference) | [agent-context/README.md](../agent-context/README.md#sdlc-pointer-current-choretask) | ## Daily Operating Rules @@ -165,7 +165,7 @@ Create new Jira issue draft in the milestone requirement file first: requirements/milestones/.md → ## Jira section (Key, Summary, Type, …) -See [requirements/milestones/README.md](../sdlc-spdd/requirements/milestones/README.md) and [jira-runbook.md](jira-runbook.md). On claim, `./sdlc-spdd/scripts/sdlc.sh claim ` auto-links the Key into the registry event note (`spdd/memory/registry.jsonl`). +See [requirements/milestones/README.md](../requirements/milestones/README.md) and [jira-runbook.md](jira-runbook.md). On claim, `./sdlc-spdd/scripts/sdlc.sh claim ` auto-links the Key into the registry event note (`spdd/memory/registry.jsonl`). Then draft for Jira UI or automation: diff --git a/docs/planning-prompt-standard.md b/docs/planning-prompt-standard.md index 43f2424b..0d886f16 100644 --- a/docs/planning-prompt-standard.md +++ b/docs/planning-prompt-standard.md @@ -35,7 +35,7 @@ Single item: ./sdlc-spdd/scripts/create-work-from-milestone.sh --target . --milestone milestone-1.md --item "Add order status API" --type feature -The script prints **Next SPDD prompts** for each created Work ID. Each stub includes a scaffolded `## Jira` section — see [requirements/milestones/README.md](../sdlc-spdd/requirements/milestones/README.md). +The script prints **Next SPDD prompts** for each created Work ID. Each stub includes a scaffolded `## Jira` section — see [requirements/milestones/README.md](../requirements/milestones/README.md). ### Plan with milestone context diff --git a/docs/research/jira-adf-and-requirements-sync.md b/docs/research/jira-adf-and-requirements-sync.md index d6ebce40..1b989644 100644 --- a/docs/research/jira-adf-and-requirements-sync.md +++ b/docs/research/jira-adf-and-requirements-sync.md @@ -233,7 +233,7 @@ Useful mappings: | `` `code` `` | `{{code}}` | | `- item` | `* item` | | `1. item` | `# item` | -| `[label](https://example.com)` | `[label\|https://example.com]` | +| `[label](url)` | `[label\|url]` | | fenced code | `{code}...{code}` | Our engine already implements `markdown_to_wiki()` for this path. diff --git a/docs/roadmap-milestones-and-session-notes.md b/docs/roadmap-milestones-and-session-notes.md index 2cbe692e..7ef85395 100644 --- a/docs/roadmap-milestones-and-session-notes.md +++ b/docs/roadmap-milestones-and-session-notes.md @@ -185,8 +185,8 @@ Keep the roadmap high level: ## Milestones - - [ ] Milestone 1 — requirements/milestones/milestone-1/MILESTONE-1.md - - [ ] Milestone 2 — requirements/milestones/milestone-2/MILESTONE-2.md + - [ ] [Milestone 1](milestone-1.md) + - [ ] [Milestone 2](milestone-2.md) ## Current Focus diff --git a/docs/sdlc-spdd-cheat-sheet.md b/docs/sdlc-spdd-cheat-sheet.md index 2d2e9470..f25e3872 100644 --- a/docs/sdlc-spdd-cheat-sheet.md +++ b/docs/sdlc-spdd-cheat-sheet.md @@ -227,7 +227,7 @@ Create Jira draft in the milestone requirement file: requirements/milestones/.md → ## Jira (Key, Summary, Type, Acceptance, …) -See [requirements/milestones/README.md](../sdlc-spdd/requirements/milestones/README.md). On claim, `./sdlc-spdd/scripts/sdlc.sh claim ` auto-links the Key into the registry event note. +See [requirements/milestones/README.md](../requirements/milestones/README.md). On claim, `./sdlc-spdd/scripts/sdlc.sh claim ` auto-links the Key into the registry event note. Draft for Jira UI: diff --git a/docs/useful-concepts-and-commands.md b/docs/useful-concepts-and-commands.md index 9786f646..0110c8cf 100644 --- a/docs/useful-concepts-and-commands.md +++ b/docs/useful-concepts-and-commands.md @@ -138,7 +138,7 @@ The active Work ID on **this machine**. Stored in `.sdlc/pointer` (gitignored). ./sdlc-spdd/scripts/sdlc.sh claim # sets pointer + team registry ./agent-context/sdlc-pointer.sh get -See [agent-context/README.md](../templates/agent-context/README.md#sdlc-pointer-current-choretask). +See [agent-context/README.md](../agent-context/README.md#sdlc-pointer-current-choretask). ### Workflow CLI @@ -195,7 +195,7 @@ Common files: - `milestone-2.md` - `session-notes/YYYY-MM-DD.md` -Milestone requirements may include a `## Jira` draft section (`requirements/milestones/.md`). On claim, `./sdlc-spdd/scripts/sdlc.sh claim ` auto-links the Jira Key into the team registry. See [requirements/milestones/README.md](../sdlc-spdd/requirements/milestones/README.md). +Milestone requirements may include a `## Jira` draft section (`requirements/milestones/.md`). On claim, `./sdlc-spdd/scripts/sdlc.sh claim ` auto-links the Jira Key into the team registry. See [requirements/milestones/README.md](../requirements/milestones/README.md). Use roadmap and milestone docs to give planning agents delivery context. Use REASONS Canvas files to govern each Work ID. diff --git a/sdlc-spdd/docs/daily-runbook.md b/sdlc-spdd/docs/daily-runbook.md index fae5278c..9e36014f 100644 --- a/sdlc-spdd/docs/daily-runbook.md +++ b/sdlc-spdd/docs/daily-runbook.md @@ -9,7 +9,7 @@ Use this runbook for **operational rhythm**: rules, script sequences, phase chec | Step order (1–13) | [Workflow](workflow.md) | | Planning → SPDD → SDLC loop | [Three-part operating path](three-part-operating-path.md) | | **Rules, scripts, checklists** | **This page** | -| Workflow CLI + team registry (reference) | `./sdlc-spdd/scripts/sdlc.sh --help` | +| Workflow CLI + team registry (reference) | [agent-context/README.md](../agent-context/README.md#sdlc-pointer-current-choretask) | ## Daily Operating Rules diff --git a/sdlc-spdd/docs/roadmap-milestones-and-session-notes.md b/sdlc-spdd/docs/roadmap-milestones-and-session-notes.md index 2cbe692e..7ef85395 100644 --- a/sdlc-spdd/docs/roadmap-milestones-and-session-notes.md +++ b/sdlc-spdd/docs/roadmap-milestones-and-session-notes.md @@ -185,8 +185,8 @@ Keep the roadmap high level: ## Milestones - - [ ] Milestone 1 — requirements/milestones/milestone-1/MILESTONE-1.md - - [ ] Milestone 2 — requirements/milestones/milestone-2/MILESTONE-2.md + - [ ] [Milestone 1](milestone-1.md) + - [ ] [Milestone 2](milestone-2.md) ## Current Focus diff --git a/sdlc-spdd/docs/useful-concepts-and-commands.md b/sdlc-spdd/docs/useful-concepts-and-commands.md index 3d537ee1..0110c8cf 100644 --- a/sdlc-spdd/docs/useful-concepts-and-commands.md +++ b/sdlc-spdd/docs/useful-concepts-and-commands.md @@ -138,7 +138,7 @@ The active Work ID on **this machine**. Stored in `.sdlc/pointer` (gitignored). ./sdlc-spdd/scripts/sdlc.sh claim # sets pointer + team registry ./agent-context/sdlc-pointer.sh get -See `./sdlc-spdd/scripts/sdlc.sh --help`. +See [agent-context/README.md](../agent-context/README.md#sdlc-pointer-current-choretask). ### Workflow CLI diff --git a/sdlc-spdd/session-notes/2026-07-15-catch-up-branch-evaluation.md b/sdlc-spdd/session-notes/2026-07-15-catch-up-branch-evaluation.md index 80c4c1c6..24abc3e6 100644 --- a/sdlc-spdd/session-notes/2026-07-15-catch-up-branch-evaluation.md +++ b/sdlc-spdd/session-notes/2026-07-15-catch-up-branch-evaluation.md @@ -1,6 +1,6 @@ # Catch-up: unmerged branches and what to do next -> **Superseded:** See [TESTING.md](../../TESTING.md#integration-merge-gate) and `./tests/test-integration-merge.sh`. +> **Superseded:** See [TESTING.md](../TESTING.md#integration-merge-gate) and `./tests/test-integration-merge.sh`. > integration PR [#27](https://github.com/jmjava/sdlc-spdd-orchestrator/pull/27). > Do not merge PR #25/#26 separately — workflow commands and FEAT-001–003 are on > `cursor/integration-981e`. Keep this note for the historical remote-branch table. @@ -26,7 +26,7 @@ Only **two branches** carry real unmerged work. The other **11** remote branches ## Delivery posture reminder -From [ROADMAP.md](../ROADMAP.md) and [milestone-1.md](../requirements/milestones/milestone-1/MILESTONE-1.md): +From [ROADMAP.md](../ROADMAP.md) and [milestone-1.md](../milestone-1.md): | Stage | Status | Focus | |-------|--------|-------| @@ -116,7 +116,7 @@ git checkout cursor/spike-guide-ingest-agent-context-17f4 ## Milestone 1 — make it right backlog (no branch yet) -From [milestone-1.md](../requirements/milestones/milestone-1/MILESTONE-1.md), execution order: +From [milestone-1.md](../milestone-1.md), execution order: | Work ID | Canvas | Status | Next step | |---------|--------|--------|-----------| @@ -205,7 +205,7 @@ gh issue list --state open ## Related artifacts - Roadmap: [ROADMAP.md](../ROADMAP.md) -- Active milestone: [milestone-1.md](../requirements/milestones/milestone-1/MILESTONE-1.md) -- Issue spec (workflow commands): `issues/ENHANCEMENT-agent-commands-for-workflow.md` (removed; superseded by `/sdlc-claim`, `/sdlc-next`, `/sdlc-team`) -- SPIKE canvas: `spdd/canvas/SPIKE-001-guide-rag-context-backend.md` (archived) -- FEAT-001 canvas: `spdd/canvas/FEAT-001-shared-script-library.md` (archived) +- Active milestone: [milestone-1.md](../milestone-1.md) +- Issue spec (workflow commands): [issues/ENHANCEMENT-agent-commands-for-workflow.md](../issues/ENHANCEMENT-agent-commands-for-workflow.md) +- SPIKE canvas: [spdd/canvas/SPIKE-001-guide-rag-context-backend.md](../spdd/canvas/SPIKE-001-guide-rag-context-backend.md) +- FEAT-001 canvas: [spdd/canvas/FEAT-001-shared-script-library.md](../spdd/canvas/FEAT-001-shared-script-library.md) From a13f9a3496f1a48354e2007398b22f29bc4b2565 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 13 Sep 2026 23:41:47 +0000 Subject: [PATCH 09/22] Revert "DOC-004: docs describe the layout init actually creates." This reverts commit 11740cf7ade7665e9b92cc4a5b74b6968d736dd6. --- CONTRIBUTING.md | 10 +++++----- README.md | 6 +++--- docs/README.md | 6 +++--- docs/agent-session-scripts.md | 4 ++-- docs/demos/docgen.yaml | 2 +- docs/demos/hints/project-context.md | 2 +- docs/demos/hints/segment-02-install-and-workflow.md | 2 +- docs/demos/narration/02-install-and-workflow.md | 2 +- docs/first-day-with-sdlc-spdd.md | 9 +++++---- docs/framework-upgrade.md | 6 +++--- docs/installing-into-your-project.md | 8 ++++---- docs/sdlc-spdd-cheat-sheet.md | 2 +- sdlc-spdd/docs/agent-session-scripts.md | 4 ++-- sdlc-spdd/docs/first-day-with-sdlc-spdd.md | 9 +++++---- sdlc-spdd/docs/framework-upgrade.md | 6 +++--- sdlc-spdd/docs/installing-into-your-project.md | 8 ++++---- sdlc-spdd/docs/sdlc-spdd-cheat-sheet.md | 2 +- 17 files changed, 45 insertions(+), 43 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 92e61d79..06af79cc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -50,7 +50,7 @@ installs into a target project. - **Shipped surfaces (must stay neutral):** `templates/**`, every `docs/*.md` that is *not* listed as orchestrator-only in - `scripts/lib/shipped-docs-boundary.sh` (installed as `sdlc-spdd/docs/`), the + `scripts/lib/shipped-docs-boundary.sh` (installed as `docs/sdlc-spdd/`), the grounding files (`templates/{claude/CLAUDE.md,copilot/copilot-instructions.md,cursor/rules/sdlc-spdd.mdc}`), and the `templates/agent-context/` harness files that install copies. @@ -91,9 +91,9 @@ Doc paths follow the same pattern: | Context | Documentation | |---------|---------------| | This orchestrator repo | `docs/*.md` | -| Installed target application | `sdlc-spdd/docs/*.md` | +| Installed target application | `docs/sdlc-spdd/*.md` | -Generated session briefs reference `sdlc-spdd/docs/…` because they are written for target projects. +Generated session briefs reference `docs/sdlc-spdd/…` because they are written for target projects. ## Command adapters and extensions @@ -142,7 +142,7 @@ Before merging doc or script changes that touch the three-part model (Planning, - [ ] **Workflow CLI** — daily runbook, first-day walkthrough, cheat sheet, and session prompt standard teach `sdlc.sh` (`next`, `claim`, `capture`) not raw scripts alone - [ ] **Workflow chat commands** — `/sdlc-claim`, `/sdlc-shelf`, `/sdlc-advance`, `/sdlc-next`, `/sdlc-team` appear alongside shell equivalents where users start/claim/orient -- [ ] **Pointer + team registry** — docs explain `.sdlc/pointer` / `.sdlc/workflows/` (local) vs `spdd/memory/registry.jsonl` (committed); claim/release commit reminder present +- [ ] **Pointer + team registry** — docs explain `.sdlc/pointer` / `.sdlc/workflows/` (local) vs `work-registry.tsv` (committed); claim/release commit reminder present - [ ] **Whereami / next** — `/sdlc-next` and `/sdlc-spdd-whereami` in assistant orientation tables (README, cheat sheet, cursor/copilot/claude usage) - [ ] **Jira draft** — milestone `## Jira` convention referenced from runbook, cheat sheet, planning prompts where Jira drafts appear - [ ] **Three-part mandate** — no Planning, SPDD, or SDLC artifacts removed or conflated; see [design mandate](docs/three-part-operating-path.md#three-part-design-mandate) @@ -159,7 +159,7 @@ Before merging doc or script changes that touch the three-part model (Planning, - [ ] **Script output** — if a script prints “next step” prompts, they align with the matching prompt standard doc - [ ] **Diagrams** — if you changed PlantUML under `docs/diagrams/*.puml`, `./scripts/render-diagrams.sh --check` passes; regenerate committed SVG exports with `./scripts/render-diagrams.sh` - [ ] **Daily doc roles** — prompts stay in `session-prompt-standard.md`; step table in `workflow.md`; rules/checklists in `daily-runbook.md`; Cursor/Copilot/Claude Code syntax in `initialization-and-invocation.md`; concepts in `useful-concepts-and-commands.md`; commands in `sdlc-spdd-cheat-sheet.md` (link, do not duplicate) -- [ ] **Target docs hub** — `docs/README.md` is orchestrator-only; installed projects use `templates/project-docs/docs-sdlc-spdd-README.md` → `sdlc-spdd/docs/README.md` (do not copy orchestrator `docs/README.md` to targets) +- [ ] **Target docs hub** — `docs/README.md` is orchestrator-only; installed projects use `templates/project-docs/docs-sdlc-spdd-README.md` → `docs/sdlc-spdd/README.md` (do not copy orchestrator `docs/README.md` to targets) - [ ] **Assistant vs shell** — `/sdlc-spdd-*` is chat (link [How to run assistant commands](docs/initialization-and-invocation.md#how-to-run-assistant-commands)); `./scripts/` is terminal - [ ] **Script paths** — install/setup from orchestrator `./scripts/`; daily/runtime in target `./sdlc-spdd/scripts/`; label which context in examples diff --git a/README.md b/README.md index e60d5fec..23e4562e 100644 --- a/README.md +++ b/README.md @@ -303,9 +303,9 @@ your-app/ adf/ Optional checked-in ADF JSON ``` -Storage v3 is the only supported layout. Pre-v3 installs (framework folders at -the repo root, `agent-context/` trees, `work-registry.tsv`) are not migrated; -re-run `init-project.sh` on a clean checkout (Milestone 3, REF-002). +Legacy sprawled installs (framework folders at the repo root, `agent-context/` +trees, `work-registry.tsv`) keep working read-only and are consolidated by +`upgrade` + `sdlc-engine storage migrate`. | Path (this repo) | Purpose | | ---------------- | ------- | diff --git a/docs/README.md b/docs/README.md index 77ac8629..8169aa42 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,6 @@ # Documentation -**Orchestrator repo hub** — when SDLC-SPDD is installed into a target application, that project gets a leaner hub at `sdlc-spdd/docs/README.md` (same guides, clearer entry path). +**Orchestrator repo hub** — when SDLC-SPDD is installed into a target application, that project gets a leaner hub at `docs/sdlc-spdd/README.md` (same guides, clearer entry path). Use this hub to choose the right guide for your current task. If you are new, read the first section in order. If you are already operating a project, jump to the section that matches what you are doing. @@ -54,7 +54,7 @@ Jira link & sync / ADF viewer), [ADF templates + Vue3 console](adf-template-libr 5. [Maintaining your project](maintaining-your-project.md) — upgrades, memory hygiene, canvas sync, links, and session maintenance. 6. [Storage v3](storage-v3.md) — where memory lives: ledger, Guide working store, runtime, backends. -**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 `sdlc-spdd/docs/`. +**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`. @@ -152,4 +152,4 @@ Also useful day to day: Follow the adoption path in the repository [README](../README.md#the-adoption-path): install → first day → three-part operating path → daily prompts. -After install, target-local copies of these docs live at `sdlc-spdd/docs/` in your application. Cursor/Copilot/Claude Code slash-command examples: [Initialization and invocation](initialization-and-invocation.md). +After install, target-local copies of these docs live at `docs/sdlc-spdd/` in your application. Cursor/Copilot/Claude Code slash-command examples: [Initialization and invocation](initialization-and-invocation.md). diff --git a/docs/agent-session-scripts.md b/docs/agent-session-scripts.md index 28891745..c47d1747 100644 --- a/docs/agent-session-scripts.md +++ b/docs/agent-session-scripts.md @@ -63,7 +63,7 @@ repo root (see the [install layout diagram](diagrams/09-install-layout.svg) and `.github/prompts/`, `CLAUDE.md` + `.claude/commands/` - `.github/workflows/validate-sdlc-spdd-adapters.yml` when both Cursor and Copilot adapters are installed -- `sdlc-spdd/docs/` — target-local copies of these docs +- `docs/sdlc-spdd/` — target-local copies of these docs ## Upgrade an Older Installation @@ -75,7 +75,7 @@ Preview first: ./scripts/upgrade-project.sh --target /path/to/app --all --dry-run -The upgrade updates framework-owned prompts, harness/skills files, target-local docs under `sdlc-spdd/docs/`, and target-local runtime scripts. It preserves application source, application docs outside `sdlc-spdd/docs/`, requirements, canvases, reviews, sync logs, the lessons ledger, existing root `CLAUDE.md`, and target workflow customizations. See [Framework upgrade](framework-upgrade.md). +The upgrade updates framework-owned prompts, harness/skills files, target-local docs under `docs/sdlc-spdd/`, and target-local runtime scripts. It preserves application source, application docs outside `docs/sdlc-spdd/`, requirements, canvases, reviews, sync logs, the lessons ledger, existing root `CLAUDE.md`, and target workflow customizations. Legacy memory layouts are converted by `sdlc-engine storage migrate` — see [Framework upgrade](framework-upgrade.md). ## 2. Start a New Agent Session diff --git a/docs/demos/docgen.yaml b/docs/demos/docgen.yaml index 2b73e8ae..72c30c78 100644 --- a/docs/demos/docgen.yaml +++ b/docs/demos/docgen.yaml @@ -93,7 +93,7 @@ narration_from_source: run analysis with index-spdd-analysis.sh, then plan.' - Mention Work ID prefixes (FEAT, BUG, REF, SPIKE, DOC, TEST, CHORE) and that canvases must reach Ready For Coding before /sdlc-spdd-code. - - Close by noting deeper guides live in docs/README.md and sdlc-spdd/docs/ in target projects after install. + - Close by noting deeper guides live in docs/README.md and docs/sdlc-spdd/ in target projects after install. context: paths: - docs/first-day-with-sdlc-spdd.md diff --git a/docs/demos/hints/project-context.md b/docs/demos/hints/project-context.md index c95947d2..23723688 100644 --- a/docs/demos/hints/project-context.md +++ b/docs/demos/hints/project-context.md @@ -16,7 +16,7 @@ docgen: Plain spoken paragraphs suitable for TTS; no markdown headings in narration output. - >- Emphasize the REASONS Canvas, Work IDs, and the three-part operating path (Planning, - SPDD, SDLC). Target projects receive templates and sdlc-spdd/docs/ — not Guide RAG + SPDD, SDLC). Target projects receive templates and docs/sdlc-spdd/ — not Guide RAG or this docgen bundle. context: paths: diff --git a/docs/demos/hints/segment-02-install-and-workflow.md b/docs/demos/hints/segment-02-install-and-workflow.md index b03c9063..abc6c0dc 100644 --- a/docs/demos/hints/segment-02-install-and-workflow.md +++ b/docs/demos/hints/segment-02-install-and-workflow.md @@ -21,7 +21,7 @@ docgen: Mention Work ID prefixes (FEAT, BUG, REF, SPIKE, DOC, TEST, CHORE) and that canvases must reach Ready For Coding before /sdlc-spdd-code. - >- - Close by noting deeper guides live in docs/README.md and sdlc-spdd/docs/ in target + Close by noting deeper guides live in docs/README.md and docs/sdlc-spdd/ in target projects after install. context: paths: diff --git a/docs/demos/narration/02-install-and-workflow.md b/docs/demos/narration/02-install-and-workflow.md index 8ccad6b9..57732cb8 100644 --- a/docs/demos/narration/02-install-and-workflow.md +++ b/docs/demos/narration/02-install-and-workflow.md @@ -8,4 +8,4 @@ The workflow table in the docs gives you the canonical sequence. Step one is set Work IDs use prefixes like FEAT, BUG, REF, SPIKE, DOC, TEST, and CHORE. The REASONS Canvas should reach Ready For Coding before you run slash sdlc-spdd-code on an operation. -After install, your target project gets a sdlc-spdd slash docs hub and the same guides under a leaner entry path. The orchestrator docs README remains the full reference for contributors extending the framework itself. Deeper guides live in docs/README.md and sdlc-spdd/docs/ in target projects after install. +After install, your target project gets a docs slash sdlc-spdd hub and the same guides under a leaner entry path. The orchestrator docs README remains the full reference for contributors extending the framework itself. Deeper guides live in docs/README.md and docs/sdlc-spdd/ in target projects after install. diff --git a/docs/first-day-with-sdlc-spdd.md b/docs/first-day-with-sdlc-spdd.md index 3f3c9230..d335fe80 100644 --- a/docs/first-day-with-sdlc-spdd.md +++ b/docs/first-day-with-sdlc-spdd.md @@ -61,10 +61,11 @@ Then open the target application in Cursor, a Copilot-enabled editor, or Claude Expected result: -- `sdlc-spdd/` exists — the single framework home. -- `sdlc-spdd/requirements/milestones/milestone-1/`, `sdlc-spdd/spdd/{canvas,analysis,tasks,reviews,sync,memory}/`, `sdlc-spdd/harness/`, `sdlc-spdd/docs/`, `sdlc-spdd/scripts/`, and `sdlc-spdd/session-notes/` exist. -- `sdlc-spdd/ROADMAP.md` and `sdlc-spdd/spdd/memory/lessons.jsonl` exist when they were missing. -- adapter stubs exist at the repo root (`.cursor/`, `.github/`, `.claude/`, `CLAUDE.md`) for the assistants you selected. +- `requirements/` exists. +- `spdd/` exists. +- `agent-context/` exists. +- `ROADMAP.md`, `milestone-1.md`, `requirements/milestones/`, and `session-notes/` exist when they were missing. +- stack information is captured in project memory. - no application source code is changed. ## 3. Create or Choose a Work ID diff --git a/docs/framework-upgrade.md b/docs/framework-upgrade.md index 8702baf3..17a52d60 100644 --- a/docs/framework-upgrade.md +++ b/docs/framework-upgrade.md @@ -11,7 +11,7 @@ The upgrade script updates **framework-owned files only**. It does not overwrite: - application source code -- application docs outside `sdlc-spdd/docs/` +- application docs outside `docs/sdlc-spdd/` - existing `ROADMAP.md` - existing `milestone-*.md` - existing `session-notes/` @@ -30,7 +30,7 @@ It can update: - `.github/prompts/sdlc-spdd-*.prompt.md` - `.claude/commands/sdlc-spdd-*.md` - `harness/*.md` and `harness/skills/*.md` -- `sdlc-spdd/docs/*.md` +- `docs/sdlc-spdd/*.md` - `sdlc-spdd/scripts/*.sh` (including `sdlc.sh`) It can create when missing: @@ -144,7 +144,7 @@ After upgrade, review: - `.github/copilot-instructions.md` if your project had custom Copilot rules - `CLAUDE.md` if the upgrade created it for the first time - `harness/skills/` if your team added custom `#SkillName` files -- SDLC-SPDD docs if your team edited `sdlc-spdd/docs/` +- SDLC-SPDD docs if your team edited `docs/sdlc-spdd/` - `spdd/memory/registry.jsonl` claims after template merge - backup folder for any local framework changes worth reapplying diff --git a/docs/installing-into-your-project.md b/docs/installing-into-your-project.md index 91696923..3b24e689 100644 --- a/docs/installing-into-your-project.md +++ b/docs/installing-into-your-project.md @@ -64,7 +64,7 @@ This installs a single framework home at `/sdlc-spdd/`. See the - `.github/copilot-instructions.md` and `.github/prompts/` - `CLAUDE.md` and `.claude/commands/` - `.github/workflows/validate-sdlc-spdd-adapters.yml` (when both Cursor and Copilot adapters are installed) -- `sdlc-spdd/docs/` — target-local doc hub (`sdlc-spdd/docs/README.md`) +- `docs/sdlc-spdd/` — target-local doc hub (`docs/sdlc-spdd/README.md`) **Gitignored runtime** (created on first use, not committed): @@ -217,17 +217,17 @@ Avoid hand-editing generated framework prompt files unless you intend to keep lo - `.claude/commands/sdlc-spdd-*.md` - `CLAUDE.md` - `sdlc-spdd/scripts/*.sh` -- `sdlc-spdd/docs/*.md` +- `docs/sdlc-spdd/*.md` - `spdd/memory/lessons.jsonl` and `spdd/memory/registry.jsonl` (written only by `accept` and `claim`/`release`) Team-specific process guidance and custom skills belong in: - `sdlc-spdd/harness/skills/` — add `.md` skill files; request with `#SkillName` -- project docs outside `sdlc-spdd/docs/` +- project docs outside `docs/sdlc-spdd/` **Local agent state** (gitignored, do not commit): `.sdlc/pointer`, `.sdlc/workflows/`, `.sdlc/sessions/`, `.sdlc/staged/`. -Keep application-specific documentation outside `sdlc-spdd/docs/` so framework upgrades can refresh SDLC-SPDD docs safely. +Keep application-specific documentation outside `docs/sdlc-spdd/` so framework upgrades can refresh SDLC-SPDD docs safely. ## Troubleshooting diff --git a/docs/sdlc-spdd-cheat-sheet.md b/docs/sdlc-spdd-cheat-sheet.md index f25e3872..70d1daa2 100644 --- a/docs/sdlc-spdd-cheat-sheet.md +++ b/docs/sdlc-spdd-cheat-sheet.md @@ -35,7 +35,7 @@ One assistant only (advanced): Target-local docs: - sdlc-spdd/docs/ + docs/sdlc-spdd/ Project planning: diff --git a/sdlc-spdd/docs/agent-session-scripts.md b/sdlc-spdd/docs/agent-session-scripts.md index 28891745..c47d1747 100644 --- a/sdlc-spdd/docs/agent-session-scripts.md +++ b/sdlc-spdd/docs/agent-session-scripts.md @@ -63,7 +63,7 @@ repo root (see the [install layout diagram](diagrams/09-install-layout.svg) and `.github/prompts/`, `CLAUDE.md` + `.claude/commands/` - `.github/workflows/validate-sdlc-spdd-adapters.yml` when both Cursor and Copilot adapters are installed -- `sdlc-spdd/docs/` — target-local copies of these docs +- `docs/sdlc-spdd/` — target-local copies of these docs ## Upgrade an Older Installation @@ -75,7 +75,7 @@ Preview first: ./scripts/upgrade-project.sh --target /path/to/app --all --dry-run -The upgrade updates framework-owned prompts, harness/skills files, target-local docs under `sdlc-spdd/docs/`, and target-local runtime scripts. It preserves application source, application docs outside `sdlc-spdd/docs/`, requirements, canvases, reviews, sync logs, the lessons ledger, existing root `CLAUDE.md`, and target workflow customizations. See [Framework upgrade](framework-upgrade.md). +The upgrade updates framework-owned prompts, harness/skills files, target-local docs under `docs/sdlc-spdd/`, and target-local runtime scripts. It preserves application source, application docs outside `docs/sdlc-spdd/`, requirements, canvases, reviews, sync logs, the lessons ledger, existing root `CLAUDE.md`, and target workflow customizations. Legacy memory layouts are converted by `sdlc-engine storage migrate` — see [Framework upgrade](framework-upgrade.md). ## 2. Start a New Agent Session diff --git a/sdlc-spdd/docs/first-day-with-sdlc-spdd.md b/sdlc-spdd/docs/first-day-with-sdlc-spdd.md index 3f3c9230..d335fe80 100644 --- a/sdlc-spdd/docs/first-day-with-sdlc-spdd.md +++ b/sdlc-spdd/docs/first-day-with-sdlc-spdd.md @@ -61,10 +61,11 @@ Then open the target application in Cursor, a Copilot-enabled editor, or Claude Expected result: -- `sdlc-spdd/` exists — the single framework home. -- `sdlc-spdd/requirements/milestones/milestone-1/`, `sdlc-spdd/spdd/{canvas,analysis,tasks,reviews,sync,memory}/`, `sdlc-spdd/harness/`, `sdlc-spdd/docs/`, `sdlc-spdd/scripts/`, and `sdlc-spdd/session-notes/` exist. -- `sdlc-spdd/ROADMAP.md` and `sdlc-spdd/spdd/memory/lessons.jsonl` exist when they were missing. -- adapter stubs exist at the repo root (`.cursor/`, `.github/`, `.claude/`, `CLAUDE.md`) for the assistants you selected. +- `requirements/` exists. +- `spdd/` exists. +- `agent-context/` exists. +- `ROADMAP.md`, `milestone-1.md`, `requirements/milestones/`, and `session-notes/` exist when they were missing. +- stack information is captured in project memory. - no application source code is changed. ## 3. Create or Choose a Work ID diff --git a/sdlc-spdd/docs/framework-upgrade.md b/sdlc-spdd/docs/framework-upgrade.md index 8702baf3..17a52d60 100644 --- a/sdlc-spdd/docs/framework-upgrade.md +++ b/sdlc-spdd/docs/framework-upgrade.md @@ -11,7 +11,7 @@ The upgrade script updates **framework-owned files only**. It does not overwrite: - application source code -- application docs outside `sdlc-spdd/docs/` +- application docs outside `docs/sdlc-spdd/` - existing `ROADMAP.md` - existing `milestone-*.md` - existing `session-notes/` @@ -30,7 +30,7 @@ It can update: - `.github/prompts/sdlc-spdd-*.prompt.md` - `.claude/commands/sdlc-spdd-*.md` - `harness/*.md` and `harness/skills/*.md` -- `sdlc-spdd/docs/*.md` +- `docs/sdlc-spdd/*.md` - `sdlc-spdd/scripts/*.sh` (including `sdlc.sh`) It can create when missing: @@ -144,7 +144,7 @@ After upgrade, review: - `.github/copilot-instructions.md` if your project had custom Copilot rules - `CLAUDE.md` if the upgrade created it for the first time - `harness/skills/` if your team added custom `#SkillName` files -- SDLC-SPDD docs if your team edited `sdlc-spdd/docs/` +- SDLC-SPDD docs if your team edited `docs/sdlc-spdd/` - `spdd/memory/registry.jsonl` claims after template merge - backup folder for any local framework changes worth reapplying diff --git a/sdlc-spdd/docs/installing-into-your-project.md b/sdlc-spdd/docs/installing-into-your-project.md index 91696923..3b24e689 100644 --- a/sdlc-spdd/docs/installing-into-your-project.md +++ b/sdlc-spdd/docs/installing-into-your-project.md @@ -64,7 +64,7 @@ This installs a single framework home at `/sdlc-spdd/`. See the - `.github/copilot-instructions.md` and `.github/prompts/` - `CLAUDE.md` and `.claude/commands/` - `.github/workflows/validate-sdlc-spdd-adapters.yml` (when both Cursor and Copilot adapters are installed) -- `sdlc-spdd/docs/` — target-local doc hub (`sdlc-spdd/docs/README.md`) +- `docs/sdlc-spdd/` — target-local doc hub (`docs/sdlc-spdd/README.md`) **Gitignored runtime** (created on first use, not committed): @@ -217,17 +217,17 @@ Avoid hand-editing generated framework prompt files unless you intend to keep lo - `.claude/commands/sdlc-spdd-*.md` - `CLAUDE.md` - `sdlc-spdd/scripts/*.sh` -- `sdlc-spdd/docs/*.md` +- `docs/sdlc-spdd/*.md` - `spdd/memory/lessons.jsonl` and `spdd/memory/registry.jsonl` (written only by `accept` and `claim`/`release`) Team-specific process guidance and custom skills belong in: - `sdlc-spdd/harness/skills/` — add `.md` skill files; request with `#SkillName` -- project docs outside `sdlc-spdd/docs/` +- project docs outside `docs/sdlc-spdd/` **Local agent state** (gitignored, do not commit): `.sdlc/pointer`, `.sdlc/workflows/`, `.sdlc/sessions/`, `.sdlc/staged/`. -Keep application-specific documentation outside `sdlc-spdd/docs/` so framework upgrades can refresh SDLC-SPDD docs safely. +Keep application-specific documentation outside `docs/sdlc-spdd/` so framework upgrades can refresh SDLC-SPDD docs safely. ## Troubleshooting diff --git a/sdlc-spdd/docs/sdlc-spdd-cheat-sheet.md b/sdlc-spdd/docs/sdlc-spdd-cheat-sheet.md index f25e3872..70d1daa2 100644 --- a/sdlc-spdd/docs/sdlc-spdd-cheat-sheet.md +++ b/sdlc-spdd/docs/sdlc-spdd-cheat-sheet.md @@ -35,7 +35,7 @@ One assistant only (advanced): Target-local docs: - sdlc-spdd/docs/ + docs/sdlc-spdd/ Project planning: From ea4b339f62e43153e034cd1f8fc815b344aab014 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 14 Sep 2026 00:01:14 +0000 Subject: [PATCH 10/22] REF-002: engine is storage v3 only - Project.home is SDLC_HOME or /sdlc-spdd; drop root/agent-context fallbacks - TeamRegistry reads registry.jsonl only; remove work-registry.tsv path - Delete storage_migrate and agent_context_upgrade (no legacy migration) - Remove legacy markdown parsers from context_model - installer.detect reports pre-v3 layouts as unsupported (reinit) - integration_config drops issue-tracker-config.json fallback Co-authored-by: John Menke --- engine/src/sdlc_engine/adf_work.py | 7 +- .../src/sdlc_engine/agent_context_upgrade.py | 93 ----- engine/src/sdlc_engine/context_model.py | 159 +-------- engine/src/sdlc_engine/db.py | 2 +- engine/src/sdlc_engine/db_query.py | 2 +- engine/src/sdlc_engine/db_schema.py | 2 +- engine/src/sdlc_engine/installer/detect.py | 23 +- engine/src/sdlc_engine/integration_config.py | 13 +- engine/src/sdlc_engine/project.py | 27 +- engine/src/sdlc_engine/quiet.py | 6 +- engine/src/sdlc_engine/registry.py | 33 +- engine/src/sdlc_engine/storage_migrate.py | 317 ------------------ 12 files changed, 31 insertions(+), 653 deletions(-) delete mode 100644 engine/src/sdlc_engine/agent_context_upgrade.py delete mode 100644 engine/src/sdlc_engine/storage_migrate.py diff --git a/engine/src/sdlc_engine/adf_work.py b/engine/src/sdlc_engine/adf_work.py index df6599ed..751d905e 100644 --- a/engine/src/sdlc_engine/adf_work.py +++ b/engine/src/sdlc_engine/adf_work.py @@ -101,14 +101,13 @@ def _next_number(self, prefix: str) -> int: max_n = 0 pattern = re.compile(rf"^{re.escape(prefix)}-(\d+)-", re.IGNORECASE) roots = [ - self.project.root / "agent-context" / "features", - self.project.root / "spdd" / "canvas", - self.project.root / "requirements" / "milestones", + self.project.spdd_dir / "canvas", + self.project.requirements_dir / "milestones", ] for base in roots: if not base.is_dir(): continue - for child in base.iterdir(): + for child in base.rglob("*"): name = child.stem if child.is_file() else child.name m = pattern.match(name) if m: diff --git a/engine/src/sdlc_engine/agent_context_upgrade.py b/engine/src/sdlc_engine/agent_context_upgrade.py deleted file mode 100644 index 4c9c0c1c..00000000 --- a/engine/src/sdlc_engine/agent_context_upgrade.py +++ /dev/null @@ -1,93 +0,0 @@ -"""Upgrade/re-init noisy agent-context runtime off git (#80, #86). - -Prefer ``sdlc-engine storage migrate`` for storage v3; this class remains for -``agent-context upgrade`` CLI compatibility and delegates to :class:`StorageMigration`. -""" - -from __future__ import annotations - -from dataclasses import dataclass, field -from typing import Any - -from .db import LocalIndex -from .project import Project -from .storage_migrate import StorageMigration - - -@dataclass -class UpgradeResult: - ok: bool - dry_run: bool - export_dir: str = "" - moved: list[str] = field(default_factory=list) - copied_memory: list[str] = field(default_factory=list) - created: list[str] = field(default_factory=list) - notes: list[str] = field(default_factory=list) - errors: list[str] = field(default_factory=list) - - def as_dict(self) -> dict[str, Any]: - return { - "ok": self.ok, - "dry_run": self.dry_run, - "export_dir": self.export_dir, - "moved": list(self.moved), - "copied_memory": list(self.copied_memory), - "created": list(self.created), - "notes": list(self.notes), - "errors": list(self.errors), - } - - -class AgentContextUpgrade: - """Delegate to storage v3 migration; keep legacy CLI surface.""" - - def __init__(self, project: Project | None = None) -> None: - self.project = project or Project.resolve() - self._migration = StorageMigration(self.project) - - def detect(self) -> dict[str, Any]: - mig = self._migration.detect() - found: dict[str, Any] = { - "noise": mig.get("legacy_present") or [], - "memory_files": [ - p for p in (mig.get("legacy_present") or []) if "memory" in p - ], - "lean_present": [], - "needs_upgrade": mig.get("needs_migration", False), - "already_upgraded": mig.get("migrated", False), - "storage_v3": mig, - "prefer_command": "storage migrate", - } - home = self.project.home - for rel in ( - "agent-context/harness", - "agent-context/playbooks", - "agent-context/extensions", - "harness", - ): - if (home / rel).is_dir(): - found["lean_present"].append(rel) - return found - - def run(self, *, dry_run: bool = False, rebuild_db: bool = True) -> UpgradeResult: - result = UpgradeResult(ok=True, dry_run=dry_run) - mig_out = self._migration.run(dry_run=dry_run) - result.export_dir = mig_out.get("export_dir") or "" - result.moved = list(mig_out.get("exported") or []) - result.notes.append("Delegated to storage v3 migration.") - if mig_out.get("records_migrated"): - result.notes.append(f"records_migrated={mig_out['records_migrated']}") - self.project.ensure_runtime_dirs() - result.created = [ - str(self.project.hot_session_dir().relative_to(self.project.root)), - ] - if rebuild_db and not dry_run and not mig_out.get("sqlite_rebuilt"): - try: - stats = LocalIndex(self.project).rebuild() - result.notes.append( - f"Rebuilt SQLite: work_items={stats.work_items} lessons={stats.lessons}" - ) - except Exception as exc: # noqa: BLE001 - result.ok = False - result.errors.append(f"db rebuild: {exc}") - return result diff --git a/engine/src/sdlc_engine/context_model.py b/engine/src/sdlc_engine/context_model.py index 4e8be75a..de743698 100644 --- a/engine/src/sdlc_engine/context_model.py +++ b/engine/src/sdlc_engine/context_model.py @@ -1,12 +1,9 @@ -"""Context capability model (storage v3) + legacy markdown parsers. +"""Context capability model (storage v3). The committed memory surface is one ledger (``spdd/memory/lessons.jsonl``) holding the pared-down, highest-value kinds. Governance documents (analysis/review/sync) stay first-class git artifacts and are indexed as entries. Everything else is runtime state (``.sdlc/`` or SQLite-only). - -The markdown parsing helpers below exist for the one-shot legacy migration -(``sdlc-engine storage migrate``) and are not part of the live write path. """ from __future__ import annotations @@ -14,7 +11,6 @@ import hashlib import re from pathlib import Path -from typing import Any, Iterable # Ledger kinds — committed memory records. LEDGER_KINDS = frozenset({"decision", "pitfall", "pattern", "session", "analysis"}) @@ -84,156 +80,3 @@ def capability_matrix() -> list[dict[str, str]]: def assert_kinds_covered(present_kinds: set[str]) -> list[str]: """Return missing CONTEXT_KINDS (empty means full coverage).""" return sorted(CONTEXT_KINDS - present_kinds) - - -# --- legacy parsers (migration only) --- - - -def parse_md_table(text: str) -> list[dict[str, str]]: - """Parse a simple GitHub-style markdown table into row dicts.""" - lines = [ln.rstrip() for ln in text.splitlines()] - header: list[str] | None = None - rows: list[dict[str, str]] = [] - for ln in lines: - if not ln.strip().startswith("|"): - continue - cells = [c.strip() for c in ln.strip().strip("|").split("|")] - if header is None: - header = [c.lower() for c in cells] - continue - if all(re.fullmatch(r":?-+:?", c or "") for c in cells): - continue - if len(cells) < len(header): - cells.extend([""] * (len(header) - len(cells))) - rows.append({header[i]: cells[i] for i in range(len(header))}) - return rows - - -def iter_code_areas(text: str) -> Iterable[str]: - for ln in text.splitlines(): - s = ln.strip() - if s.startswith("- "): - area = s[2:].strip() - if area: - area = area.split(" — ")[0].split(" - ")[0].strip() - yield area - - -def extract_prompt_entries(text: str) -> list[dict[str, Any]]: - """Split prompt-optimization-log style headed sections into entries.""" - entries: list[dict[str, Any]] = [] - blocks = re.split(r"\n(?=## )", text) - for block in blocks: - if not block.strip().startswith("## "): - continue - first, _, rest = block.partition("\n") - title = first.lstrip("# ").strip() - entries.append( - { - "title": title, - "body": rest.strip()[:2000], - "kind": "prompt", - } - ) - return entries - - -def extract_memory_facts(text: str) -> list[dict[str, Any]]: - """Parse ## Recent Learnings subsections from project-memory.md.""" - facts: list[dict[str, Any]] = [] - if "## Recent Learnings" not in text: - return facts - section = text.split("## Recent Learnings", 1)[1] - for m in re.finditer( - r"###\s+(\S+)\s+-\s+(\S+)\s*\n(.*?)(?=\n### |\Z)", - section, - re.DOTALL, - ): - ts, work_id, body = m.group(1), m.group(2), m.group(3) - phase = "" - summary = "" - nxt = "" - for ln in body.splitlines(): - if ln.strip().startswith("- Phase:"): - phase = ln.split(":", 1)[1].strip() - elif ln.strip().startswith("- Summary:"): - summary = ln.split(":", 1)[1].strip() - elif ln.strip().startswith("- Next:"): - nxt = ln.split(":", 1)[1].strip() - facts.append( - { - "ts": ts, - "work_id": work_id, - "phase": phase, - "summary": summary or body.strip()[:500], - "next_step": nxt, - } - ) - return facts - - -def extract_session_history(text: str) -> list[dict[str, Any]]: - """Parse legacy session-history.md ``### - - `` blocks.""" - sessions: list[dict[str, Any]] = [] - for m in re.finditer( - r"(?m)^###\s+(\S+)\s+-\s+(\S+?)(?:\s+-\s+(\S+))?\s*\n(.*?)(?=^###\s+|\Z)", - text, - re.DOTALL, - ): - ts, work_id, phase, body = ( - m.group(1), - m.group(2), - m.group(3) or "", - m.group(4), - ) - summary = "" - for ln in body.splitlines(): - if ln.strip().startswith("- Summary:"): - summary = ln.split(":", 1)[1].strip() - break - sessions.append( - { - "ts": ts, - "work_id": work_id, - "phase": phase, - "summary": summary, - "body": body.strip()[:4000], - } - ) - return sessions - - -def extract_lesson_blocks(text: str) -> list[dict[str, Any]]: - """Parse legacy lesson files (known-pitfalls / reusable-patterns / - architecture-decisions and spdd lesson files) into records. - - Supports both ``## — `` blocks written by the old - ContextStore and free-form ``## `` sections. - """ - out: list[dict[str, Any]] = [] - wid_re = r"((?:FEAT|BUG|SPIKE|REF|DOC|TEST|CHORE|LOCAL)-[A-Za-z0-9][\w.-]*)" - for block in re.split(r"(?m)^##\s+", text): - if not block.strip(): - continue - first = block.splitlines()[0].strip() - if first.startswith("#"): - continue - body = "\n".join(block.splitlines()[1:]).strip() - m = re.match(rf"^{wid_re}", first) - wid = m.group(1) if m else "" - ts_m = re.search(r"(\d{4}-\d{2}-\d{2}(?:T[\d:]+Z)?)", first) - area_m = re.search(r"(?m)^-\s*Area:\s*(.+)$", block) - id_m = re.search(r"<!--\s*id:\s*([^>]+?)\s*-->", block) - if not body and not first: - continue - out.append( - { - "work_id": wid, - "title": first[:120], - "body": body[:4000], - "ts": ts_m.group(1) if ts_m else "", - "area": area_m.group(1).strip() if area_m else "", - "legacy_id": id_m.group(1).strip() if id_m else "", - } - ) - return out diff --git a/engine/src/sdlc_engine/db.py b/engine/src/sdlc_engine/db.py index 199f3699..f3f1fdcd 100644 --- a/engine/src/sdlc_engine/db.py +++ b/engine/src/sdlc_engine/db.py @@ -586,7 +586,7 @@ def upsert_context_entry( ts: str = "", entry_id: str = "", ) -> str: - """Upsert any agent-context capability row + section/area edges.""" + """Upsert a context capability row + section/area edges.""" from .context_model import CONTEXT_KINDS kind_n = (kind or "").strip().lower() diff --git a/engine/src/sdlc_engine/db_query.py b/engine/src/sdlc_engine/db_query.py index 71ab210e..927b4996 100644 --- a/engine/src/sdlc_engine/db_query.py +++ b/engine/src/sdlc_engine/db_query.py @@ -75,7 +75,7 @@ def capability_coverage(self) -> dict[str, Any]: } def graph_for_work(self, work_id: str) -> dict[str, Any]: - """Full subgraph: sections + all agent-context entries + typed edges.""" + """Full subgraph: sections + all context entries + typed edges.""" self.ensure_schema() wid = (work_id or "").strip() with self.connect() as conn: diff --git a/engine/src/sdlc_engine/db_schema.py b/engine/src/sdlc_engine/db_schema.py index dd850432..0eb5cac6 100644 --- a/engine/src/sdlc_engine/db_schema.py +++ b/engine/src/sdlc_engine/db_schema.py @@ -9,7 +9,7 @@ DEFAULT_DB_NAME = "index.sqlite" # Typed edge kinds (src/dst) and relationship names — aligned with Guide DICE -# plus requirement↔REASONS and full agent-context part links. +# plus requirement↔REASONS and context-entry links. NODE_WORK = "work" NODE_REQUIREMENT = "requirement" NODE_CANVAS = "canvas" diff --git a/engine/src/sdlc_engine/installer/detect.py b/engine/src/sdlc_engine/installer/detect.py index cd256957..a214d4de 100644 --- a/engine/src/sdlc_engine/installer/detect.py +++ b/engine/src/sdlc_engine/installer/detect.py @@ -6,22 +6,22 @@ from typing import Any +# Storage v3 single-folder home plus the adapter stubs init writes at the root. MARKERS = ( - # storage v3 single-folder home "sdlc-spdd/scripts/sdlc.sh", "sdlc-spdd/spdd/memory/lessons.jsonl", - "spdd/memory/lessons.jsonl", - "scripts/sdlc-spdd/sdlc.sh", ".cursor/commands/sdlc-spdd-init.md", ".github/prompts/sdlc-spdd-init.prompt.md", ".claude/commands/sdlc-spdd-init.md", ) -# Pre-v3 sprawled layouts, detected only so upgrade (and storage migrate) -# can be recommended for old installs. -LEGACY_MARKERS = ( +# Pre-v3 layouts are not migrated. They are detected only so the console can +# say "unsupported layout; re-init on a clean checkout" instead of upgrading. +UNSUPPORTED_MARKERS = ( "agent-context/sdlc-workflow.sh", "agent-context/work-registry.tsv", + "scripts/sdlc-spdd/sdlc.sh", + "spdd/memory/lessons.jsonl", ) @@ -29,11 +29,8 @@ def detect_target(target: Path | str) -> dict[str, Any]: """Return install-mode diagnosis for ``target``.""" root = Path(target).expanduser().resolve() exists = root.is_dir() - markers_found: list[str] = [] - if exists: - for rel in (*MARKERS, *LEGACY_MARKERS): - if (root / rel).exists(): - markers_found.append(rel) + markers_found = [rel for rel in MARKERS if exists and (root / rel).exists()] + unsupported_found = [rel for rel in UNSUPPORTED_MARKERS if exists and (root / rel).exists()] has_cursor = (root / ".cursor/commands/sdlc-spdd-init.md").is_file() if exists else False has_copilot = (root / ".github/prompts/sdlc-spdd-init.prompt.md").is_file() if exists else False @@ -42,6 +39,9 @@ def detect_target(target: Path | str) -> dict[str, Any]: if not exists: mode = "missing" recommendation = "create" + elif unsupported_found and not (root / "sdlc-spdd").is_dir(): + mode = "unsupported" + recommendation = "reinit" elif markers_found: mode = "upgrade" recommendation = "upgrade" @@ -55,6 +55,7 @@ def detect_target(target: Path | str) -> dict[str, Any]: "mode": mode, "recommendation": recommendation, "markers": markers_found, + "unsupported_markers": unsupported_found, "assistants": { "cursor": has_cursor, "copilot": has_copilot, diff --git a/engine/src/sdlc_engine/integration_config.py b/engine/src/sdlc_engine/integration_config.py index 19ab8249..5084ccc3 100644 --- a/engine/src/sdlc_engine/integration_config.py +++ b/engine/src/sdlc_engine/integration_config.py @@ -1,7 +1,6 @@ """Per-project issue tracker + Jira/GitHub credentials (gitignored runtime). -Config file: ``.sdlc/integrations-config.json`` (falls back to legacy -``.sdlc/issue-tracker-config.json`` for tracker-only fields). +Config file: ``<home>/.sdlc/integrations-config.json``. Environment variables always win over file values when set. Secrets are never returned from :func:`status_dict` — only ``*_set`` booleans. @@ -20,7 +19,6 @@ from .project import Project CONFIG_REL = Path(".sdlc") / "integrations-config.json" -LEGACY_TRACKER_REL = Path(".sdlc") / "issue-tracker-config.json" TRACKER_JIRA = "jira" TRACKER_GITHUB = "github" @@ -111,15 +109,6 @@ def github_configured(self) -> bool: def load_config(project: Project) -> dict[str, Any]: path = config_path(project) data = _read_json(path) - if not data: - legacy = _read_json(project.sdlc_dir / LEGACY_TRACKER_REL.name) - if legacy: - data = { - "tracker": legacy.get("tracker", DEFAULT_TRACKER), - "notes": legacy.get("notes", ""), - "jira": {}, - "github": {}, - } tracker = normalize_tracker(str(data.get("tracker") or DEFAULT_TRACKER)) if tracker not in ALL_TRACKERS: tracker = DEFAULT_TRACKER diff --git a/engine/src/sdlc_engine/project.py b/engine/src/sdlc_engine/project.py index 9595e03c..b06a0d36 100644 --- a/engine/src/sdlc_engine/project.py +++ b/engine/src/sdlc_engine/project.py @@ -11,9 +11,8 @@ scripts/ installed workflow CLI .sdlc/ gitignored runtime (sessions, staged, sqlite) -Legacy sprawled layouts (framework dirs at repo root) resolve ``home == root`` -so every path helper keeps working until ``upgrade-project.sh`` consolidates -(and archives leftovers) into ``sdlc-spdd/``. +This is the only supported layout. ``SDLC_HOME`` overrides the home folder; +there is no fallback to framework folders at the repo root. """ from __future__ import annotations @@ -54,18 +53,11 @@ def resolve(cls, root: str | Path | None = None) -> "Project": @property def home(self) -> Path: - """Single framework folder; falls back to root for legacy layouts.""" + """The single framework folder: ``$SDLC_HOME`` or ``<root>/sdlc-spdd``.""" env = os.environ.get("SDLC_HOME") if env: return Path(env).expanduser().resolve() - candidate = self.root / HOME_DIR_NAME - if candidate.is_dir(): - return candidate - return self.root - - @property - def is_single_folder(self) -> bool: - return self.home != self.root + return self.root / HOME_DIR_NAME @property def sdlc_dir(self) -> Path: @@ -97,11 +89,9 @@ def staged_ledger_path(self) -> Path: @property def registry_jsonl_path(self) -> Path: - """Committed claim/release event log (replaces the legacy TSV registry).""" + """Committed claim/release event log.""" return self.memory_dir / "registry.jsonl" - # Kept name for compatibility with existing callers; now points at the - # lean JSONL registry. @property def registry_path(self) -> Path: return self.registry_jsonl_path @@ -126,11 +116,8 @@ def session_notes_dir(self) -> Path: @property def harness_dir(self) -> Path: - """Install-time harness. Single-folder: <home>/harness; legacy: agent-context/harness.""" - direct = self.home / "harness" - if direct.is_dir(): - return direct - return self.home / "agent-context" / "harness" + """Install-time harness and #SkillName playbooks.""" + return self.home / "harness" @property def skills_dir(self) -> Path: diff --git a/engine/src/sdlc_engine/quiet.py b/engine/src/sdlc_engine/quiet.py index 652a347e..c526e868 100644 --- a/engine/src/sdlc_engine/quiet.py +++ b/engine/src/sdlc_engine/quiet.py @@ -13,7 +13,7 @@ def is_quiet( quiet_flag: bool = False, env: dict[str, str] | None = None, ) -> bool: - """True when SDLC_QUIET=1, --quiet, or agent-context/harness/quiet-mode.md exists.""" + """True when SDLC_QUIET=1, --quiet, or <home>/harness/quiet-mode.md exists.""" if quiet_flag: return True environ = env if env is not None else os.environ @@ -24,8 +24,8 @@ def is_quiet( "on", }: return True - root = (project or Project.resolve()).root - return (root / "agent-context" / "harness" / "quiet-mode.md").is_file() + proj = project or Project.resolve() + return (proj.harness_dir / "quiet-mode.md").is_file() def quiet_resume_blurb(*, guide_live: bool = False) -> str: diff --git a/engine/src/sdlc_engine/registry.py b/engine/src/sdlc_engine/registry.py index 3caa39fc..d0772082 100644 --- a/engine/src/sdlc_engine/registry.py +++ b/engine/src/sdlc_engine/registry.py @@ -4,9 +4,6 @@ ``{"event","work_id","status","phase","operation","owner","note","ts"}``. ``rows()`` derives current state = latest event per ``work_id``. -During transition, if ``registry.jsonl`` is missing and legacy -``agent-context/work-registry.tsv`` exists, ``rows()`` falls back to a -read-only TSV parse (no writes to TSV). """ from __future__ import annotations @@ -55,11 +52,6 @@ def __init__(self, project: Project | None = None, workflow: WorkflowEngine | No def path(self) -> Path: return self.project.registry_jsonl_path - @property - def legacy_tsv_path(self) -> Path: - # Read-only transition fallback (see module docstring). - return self.project.home / "agent-context" / "work-registry.tsv" - def _owner(self) -> str: if os.environ.get("SDLC_USER"): return os.environ["SDLC_USER"] @@ -96,30 +88,7 @@ def _read_events(self) -> list[dict[str, str]]: except json.JSONDecodeError: continue return out - # Transition read-only fallback: legacy TSV when JSONL absent. - tsv = self.legacy_tsv_path - if not tsv.is_file(): - return [] - out = [] - for line in tsv.read_text(encoding="utf-8").splitlines(): - if not line or line.startswith("#") or line.startswith("work_id"): - continue - parts = line.split("\t") - while len(parts) < 7: - parts.append("") - out.append( - { - "event": "legacy-tsv", - "work_id": parts[0], - "status": parts[1], - "phase": parts[2], - "operation": parts[3], - "owner": parts[4], - "ts": parts[5], - "note": parts[6], - } - ) - return out + return [] def rows(self) -> list[RegistryRow]: """Current registry state = latest event per work_id.""" diff --git a/engine/src/sdlc_engine/storage_migrate.py b/engine/src/sdlc_engine/storage_migrate.py deleted file mode 100644 index c2371f64..00000000 --- a/engine/src/sdlc_engine/storage_migrate.py +++ /dev/null @@ -1,317 +0,0 @@ -"""One-shot storage v3 migration: legacy agent-context → ledger + export.""" - -from __future__ import annotations - -import shutil -from dataclasses import dataclass -from pathlib import Path -from typing import Any - -from . import context_model as cm -from .db import LocalIndex -from .lessons_ledger import LessonRecord, LessonsLedger, lesson_id -from .persistence import BACKEND_SQLITE, enabled as backend_enabled -from .project import Project -from .registry import RegistryRow, TeamRegistry -from .timeutil import utc_now as _utc_now -from .timeutil import utc_stamp as _utc_stamp - - -EXPORT_PATHS = ( - Path("agent-context/memory"), - Path("agent-context/sessions"), - Path("agent-context/features"), - Path("agent-context/work-registry.tsv"), - Path("spdd/memory/lessons"), - Path("spdd/memory/context-index.md"), - Path("spdd/memory/pointers.jsonl"), - Path("spdd/memory/entries"), - Path("spdd/memory/sessions"), - Path("spdd/memory/domain-index.md"), -) - -LESSON_BODY_FILES: tuple[tuple[str, Path], ...] = ( - ("decision", Path("agent-context/memory/architecture-decisions.md")), - ("pitfall", Path("agent-context/memory/known-pitfalls.md")), - ("pattern", Path("agent-context/memory/reusable-patterns.md")), - ("decision", Path("spdd/memory/lessons/decisions.md")), - ("pitfall", Path("spdd/memory/lessons/pitfalls.md")), - ("pattern", Path("spdd/memory/lessons/patterns.md")), -) - - -@dataclass -class StorageMigration: - project: Project - - def detect(self) -> dict[str, Any]: - home = self.project.home - root = self.project.root - marker = self.project.sdlc_dir / "storage-v3-migrated" - legacy: list[str] = [] - for rel in EXPORT_PATHS: - p = home / rel if (home / rel).exists() else root / rel - if p.exists(): - legacy.append(str(rel)) - return { - "migrated": marker.is_file(), - "marker": str(marker) if marker.is_file() else "", - "legacy_present": legacy, - "ledger_exists": self.project.ledger_path.is_file(), - "registry_jsonl_exists": self.project.registry_jsonl_path.is_file(), - "needs_migration": bool(legacy) and not marker.is_file(), - } - - def run(self, *, dry_run: bool = False) -> dict[str, Any]: - counts: dict[str, int] = { - "context_index": 0, - "lesson_files": 0, - "session_history": 0, - "domain_index": 0, - "prompt_log": 0, - "registry_tsv": 0, - } - exported: list[str] = [] - home = self.project.home - root = self.project.root - ledger = LessonsLedger(self.project) - existing_ids = ledger.accepted_ids() | ledger.staged_ids() - - def _append(record: LessonRecord) -> None: - if record.id in existing_ids: - return - record.validate() - if not dry_run: - ledger.append_accepted(record) - existing_ids.add(record.id) - return None - - # a) context-index.md - for rel in ( - Path("agent-context/memory/context-index.md"), - Path("spdd/memory/context-index.md"), - ): - path = home / rel if (home / rel).is_file() else root / rel - if not path.is_file(): - continue - try: - rows = cm.parse_md_table(path.read_text(encoding="utf-8")) - except OSError: - continue - for row in rows: - kind = (row.get("kind") or "").strip().lower() - if kind in {"session", "metric"} or not kind: - continue - area = (row.get("area") or "").strip() - wid = (row.get("work id") or row.get("work_id") or "_memory").strip() - phase = (row.get("phase") or "").strip() - ts = (row.get("timestamp") or "").strip() - source = (row.get("source") or "context-index").strip() - entry = (row.get("entry") or "").strip() - if kind in {"decision", "pitfall", "pattern", "analysis"}: - rid = lesson_id(kind, wid, area, source) - _append( - LessonRecord( - id=rid, - kind=kind, - work_id=wid, - area=area, - phase=phase, - ts=ts, - title=entry[:120], - body=entry, - source=source, - ) - ) - counts["context_index"] += 1 - - # b) lesson body files - for kind, rel in LESSON_BODY_FILES: - path = home / rel if (home / rel).is_file() else root / rel - if not path.is_file(): - continue - try: - blocks = cm.extract_lesson_blocks(path.read_text(encoding="utf-8")) - except OSError: - continue - for block in blocks: - wid = (block.get("work_id") or "_memory").strip() - source = str(rel).replace("\\", "/") - rid = (block.get("legacy_id") or "").strip() or lesson_id( - kind, wid, block.get("area") or "", source - ) - _append( - LessonRecord( - id=rid, - kind=kind, - work_id=wid, - area=block.get("area") or "", - ts=block.get("ts") or "", - title=block.get("title") or "", - body=block.get("body") or "", - source=source, - ) - ) - counts["lesson_files"] += 1 - - # c) session-history.md - for rel in (Path("agent-context/memory/session-history.md"),): - path = home / rel if (home / rel).is_file() else root / rel - if not path.is_file(): - continue - try: - sessions = cm.extract_session_history(path.read_text(encoding="utf-8")) - except OSError: - continue - for sess in sessions: - wid = (sess.get("work_id") or "_memory").strip() - source = "session-history" - rid = lesson_id("session", wid, "", source + ":" + (sess.get("ts") or "")) - _append( - LessonRecord( - id=rid, - kind="session", - work_id=wid, - phase=sess.get("phase") or "", - ts=sess.get("ts") or "", - title=sess.get("summary") or "", - body=sess.get("body") or "", - source=source, - ) - ) - counts["session_history"] += 1 - - # d) domain-index.md — merge keywords into analysis records - analysis_by_key: dict[tuple[str, str], LessonRecord] = {} - for rel in ( - Path("agent-context/memory/domain-index.md"), - Path("spdd/memory/domain-index.md"), - ): - path = home / rel if (home / rel).is_file() else root / rel - if not path.is_file(): - continue - try: - rows = cm.parse_md_table(path.read_text(encoding="utf-8")) - except OSError: - continue - for row in rows: - keyword = (row.get("keyword") or "").strip().lower() - if not keyword: - continue - area = (row.get("area") or "").strip() - wid = (row.get("work id") or row.get("work_id") or "_memory").strip() - key = (wid, area) - if key not in analysis_by_key: - analysis_by_key[key] = LessonRecord( - id=lesson_id("analysis", wid, area, "domain-index"), - kind="analysis", - work_id=wid, - area=area, - source="domain-index", - title=f"Domain keywords for {area or wid}", - body="", - keywords=[], - ) - if keyword not in analysis_by_key[key].keywords: - analysis_by_key[key].keywords.append(keyword) - counts["domain_index"] += 1 - for rec in analysis_by_key.values(): - _append(rec) - - # e) prompt-optimization-log.md - for rel in (Path("agent-context/memory/prompt-optimization-log.md"),): - path = home / rel if (home / rel).is_file() else root / rel - if not path.is_file(): - continue - try: - entries = cm.extract_prompt_entries(path.read_text(encoding="utf-8")) - except OSError: - continue - for ent in entries: - title = ent.get("title") or "prompt" - rid = lesson_id("decision", "_memory", "", "prompt-log:" + title[:40]) - _append( - LessonRecord( - id=rid, - kind="decision", - work_id="_memory", - title=title, - body=ent.get("body") or "", - source="prompt-log", - ) - ) - counts["prompt_log"] += 1 - - # f) work-registry.tsv → registry.jsonl events - tsv = None - for base in (root, home): - candidate = base / "agent-context" / "work-registry.tsv" - if candidate.is_file(): - tsv = candidate - break - if tsv is not None and not self.project.registry_jsonl_path.is_file(): - reg = TeamRegistry(self.project) - if not dry_run: - reg.ensure() - for line in tsv.read_text(encoding="utf-8").splitlines(): - if not line or line.startswith("#") or line.startswith("work_id"): - continue - parts = line.split("\t") - while len(parts) < 7: - parts.append("") - row = RegistryRow( - work_id=parts[0], - status=parts[1], - phase=parts[2], - operation=parts[3], - owner=parts[4], - updated=parts[5] or _utc_now(), - note=parts[6], - ) - if not dry_run: - reg.upsert(row, event="migrate") - counts["registry_tsv"] += 1 - - # Export legacy trees - stamp = _utc_stamp() - export_root = self.project.sdlc_dir / "legacy-export" / stamp - for rel in EXPORT_PATHS: - src = home / rel if (home / rel).exists() else root / rel - if not src.exists(): - continue - dest = export_root / rel - if dry_run: - exported.append(str(rel)) - continue - dest.parent.mkdir(parents=True, exist_ok=True) - if src.is_dir(): - if dest.exists(): - shutil.rmtree(dest) - shutil.move(str(src), str(dest)) - else: - shutil.move(str(src), str(dest)) - exported.append(str(rel)) - - sqlite_rebuilt = False - if not dry_run and backend_enabled(self.project, BACKEND_SQLITE): - try: - LocalIndex(self.project).rebuild() - sqlite_rebuilt = True - except Exception: - pass - - marker = self.project.sdlc_dir / "storage-v3-migrated" - if not dry_run: - marker.parent.mkdir(parents=True, exist_ok=True) - marker.write_text(stamp + "\n", encoding="utf-8") - - return { - "ok": True, - "dry_run": dry_run, - "stamp": stamp, - "records_migrated": counts, - "exported": exported, - "export_dir": str(export_root) if exported else "", - "sqlite_rebuilt": sqlite_rebuilt, - "marker": str(marker), - } From ee6fecb91e7c7dc2bc344142c203894245c4ebcc Mon Sep 17 00:00:00 2001 From: Cursor Agent <cursoragent@cursor.com> Date: Mon, 14 Sep 2026 00:01:14 +0000 Subject: [PATCH 11/22] REF-003/REF-005: session bridge and commands package - Add sdlc_engine.session (brief, touch, record-capture, recommend, jira status/ask) - New top-level verbs: quiet-status, start, capture, complete, accept, session - Drop storage/agent-context CLI verbs - Split cli_commands.py into commands/{state,team,context,integrations,storage} - cli.py exports main/build_parser only; delete issue_tracker re-export shim Co-authored-by: John Menke <jmjava@gmail.com> --- engine/src/sdlc_engine/cli.py | 39 +- engine/src/sdlc_engine/cli_commands.py | 999 ------------------ engine/src/sdlc_engine/cli_parser.py | 92 +- engine/src/sdlc_engine/commands/__init__.py | 95 ++ engine/src/sdlc_engine/commands/context.py | 329 ++++++ .../src/sdlc_engine/commands/integrations.py | 211 ++++ engine/src/sdlc_engine/commands/state.py | 196 ++++ engine/src/sdlc_engine/commands/storage.py | 299 ++++++ engine/src/sdlc_engine/commands/team.py | 65 ++ engine/src/sdlc_engine/issue_tracker.py | 54 - engine/src/sdlc_engine/issues.py | 5 +- engine/src/sdlc_engine/session.py | 217 ++++ 12 files changed, 1477 insertions(+), 1124 deletions(-) delete mode 100644 engine/src/sdlc_engine/cli_commands.py create mode 100644 engine/src/sdlc_engine/commands/__init__.py create mode 100644 engine/src/sdlc_engine/commands/context.py create mode 100644 engine/src/sdlc_engine/commands/integrations.py create mode 100644 engine/src/sdlc_engine/commands/state.py create mode 100644 engine/src/sdlc_engine/commands/storage.py create mode 100644 engine/src/sdlc_engine/commands/team.py delete mode 100644 engine/src/sdlc_engine/issue_tracker.py create mode 100644 engine/src/sdlc_engine/session.py diff --git a/engine/src/sdlc_engine/cli.py b/engine/src/sdlc_engine/cli.py index ceed610a..2073d5b2 100644 --- a/engine/src/sdlc_engine/cli.py +++ b/engine/src/sdlc_engine/cli.py @@ -6,44 +6,7 @@ from .cli_parser import build_parser -# Keep handler names importable from sdlc_engine.cli for compatibility. -from .cli_commands import ( # noqa: F401 - _project, - cmd_advance, - cmd_agent_context, - cmd_archive, - cmd_claim, - cmd_commit_message, - cmd_context, - cmd_db, - cmd_gate, - cmd_installer, - cmd_issues, - cmd_links, - cmd_list_shelved, - cmd_list_work, - cmd_local, - cmd_next, - cmd_pointer, - cmd_quick, - cmd_release, - cmd_resume, - cmd_shelf, - cmd_shell, - cmd_skip, - cmd_status, - cmd_storage, - cmd_sunset, - cmd_sync, - cmd_sync_links, - cmd_sync_roadmap, - cmd_sync_team, - cmd_team, - cmd_template, - cmd_version, - cmd_viewer, - cmd_work, -) +from .commands import cmd_next, cmd_version def main(argv: list[str] | None = None) -> int: diff --git a/engine/src/sdlc_engine/cli_commands.py b/engine/src/sdlc_engine/cli_commands.py deleted file mode 100644 index 643b6ec5..00000000 --- a/engine/src/sdlc_engine/cli_commands.py +++ /dev/null @@ -1,999 +0,0 @@ -"""CLI command handlers for sdlc-engine.""" - -from __future__ import annotations - -import argparse -import json -import subprocess -import sys -from pathlib import Path - -from . import __version__ -from .adf_templates import AdfTemplateLibrary, TemplateError -from .adf_work import AdfWorkService -from .archive import ArchiveService -from .commit_message import CommitMessageError, CommitMessageService -from .context_store import ContextStore -from .db import LocalIndex, format_rows -from .issues import IssueSyncService -from .local_sessions import LocalSessionService -from .phases import GATE_LABELS, advisory_gates_for_phase, format_advisory_gate_rows -from .pointer import PointerError, PointerStore -from .project import Project -from .registry import TeamRegistry -from .sunset import SunsetError, SunsetService -from .sync_local import LocalSyncService -from .workflow import WorkflowEngine - - -def _project(args: argparse.Namespace) -> Project: - return Project.resolve(getattr(args, "root", None)) - - -def cmd_next(args: argparse.Namespace) -> int: - print(WorkflowEngine(_project(args)).next_text(), end="") - return 0 - - -def cmd_status(args: argparse.Namespace) -> int: - eng = WorkflowEngine(_project(args)) - if args.json: - print(eng.status_json(args.work_id)) - else: - wid = args.work_id or eng.pointer.get() or "(none)" - print(f"Pointer: {eng.pointer.get() or '(none)'}") - print(f"Work ID: {wid}") - if eng.pointer.get() or args.work_id: - print(eng.status_json(args.work_id)) - return 0 - - -def cmd_resume(args: argparse.Namespace) -> int: - state = WorkflowEngine(_project(args)).resume(args.work_id, phase=args.phase, force=args.force) - print(f"Resumed {state.work_id} at phase: {state.phase}") - return 0 - - -def cmd_advance(args: argparse.Namespace) -> int: - try: - state = WorkflowEngine(_project(args)).advance( - to=args.to, force=getattr(args, "force", False) - ) - except ValueError as exc: - print(str(exc), file=sys.stderr) - return 1 - print(f"Advanced to phase: {state.phase}") - return 0 - - -def cmd_gate(args: argparse.Namespace) -> int: - eng = WorkflowEngine(_project(args)) - work_id = args.work_id or eng.pointer.get() - if not work_id: - print( - "gate: no Work ID (pass --work-id or set the pointer via claim/resume)", - file=sys.stderr, - ) - return 2 - try: - ok, failures = eng.gate_check(work_id, args.phase) - except ValueError as exc: - print(str(exc), file=sys.stderr) - return 2 - advisory = [ - {"gate": name, "label": GATE_LABELS[name], "ran": False} - for name in advisory_gates_for_phase(args.phase) - ] - advisory_rows = format_advisory_gate_rows(args.phase) - if args.json: - print( - json.dumps( - { - "work_id": work_id, - "phase": args.phase, - "ok": ok, - "failures": failures, - "advisory": advisory, - }, - indent=2, - ) - ) - else: - if ok: - print(f"gate {args.phase}: OK for {work_id}") - for row in advisory_rows: - print(row) - else: - print(f"gate {args.phase}: BLOCKED for {work_id}", file=sys.stderr) - for failure in failures: - print(f" - {failure}", file=sys.stderr) - for row in advisory_rows: - print(row, file=sys.stderr) - return 0 if ok else 1 - - -def cmd_skip(args: argparse.Namespace) -> int: - state = WorkflowEngine(_project(args)).skip(args.phase, reason=args.reason) - print(f"Skipped {args.phase}; now at {state.phase}") - return 0 - - -def cmd_shelf(args: argparse.Namespace) -> int: - project = _project(args) - eng = WorkflowEngine(project) - wid = eng.pointer.get() - if wid and wid.upper().startswith("LOCAL-"): - session = LocalSessionService(project).shelf(args.reason, session_id=wid) - print(f"Shelved local session {session.id}: {args.reason}") - return 0 - state = eng.shelf(reason=args.reason) - if state is None: - print("No active pointer to shelf", file=sys.stderr) - return 1 - print(f"Shelved {state.work_id}: {args.reason}") - return 0 - - -def cmd_sync(args: argparse.Namespace) -> int: - state = WorkflowEngine(_project(args)).sync(args.work_id) - print(f"Synced {state.work_id} -> phase {state.phase}") - return 0 - - -def cmd_list_shelved(args: argparse.Namespace) -> int: - rows = WorkflowEngine(_project(args)).list_shelved() - if not rows: - print("(no shelved work)") - return 0 - for wid, phase, at, reason in rows: - print(f"{wid}\t{phase}\t{at}\t{reason}") - return 0 - - -def cmd_claim(args: argparse.Namespace) -> int: - reg = TeamRegistry(_project(args)) - try: - row = reg.claim( - args.work_id, - force=args.force, - phase=args.phase, - branch=args.branch or "", - pr=args.pr or "", - jira=args.jira or "", - note=args.note or "", - ) - except PermissionError as exc: - print(str(exc), file=sys.stderr) - return 1 - print(f"Claimed {row.work_id} as {row.owner} (phase={row.phase})") - print("Team registry updated — commit spdd/memory/registry.jsonl to share with teammates.") - return 0 - - -def cmd_release(args: argparse.Namespace) -> int: - TeamRegistry(_project(args)).release(reason=args.reason) - print("Released / shelved active work") - return 0 - - -def cmd_team(args: argparse.Namespace) -> int: - print(TeamRegistry(_project(args)).team_text(), end="") - return 0 - - -def cmd_list_work(args: argparse.Namespace) -> int: - print(TeamRegistry(_project(args)).list_work_text(), end="") - return 0 - - -def cmd_sync_team(args: argparse.Namespace) -> int: - TeamRegistry(_project(args)).refresh_done_status() - print("Team registry refreshed from canvas Final Status.") - return 0 - - -def cmd_archive(args: argparse.Namespace) -> int: - svc = ArchiveService(_project(args)) - try: - if args.all: - svc.archive_eligible(dry_run=args.dry_run) - else: - svc.archive_work(args.work_id, dry_run=args.dry_run, force=args.force) - except ValueError as exc: - print(str(exc), file=sys.stderr) - return 1 - return 0 - - -def cmd_pointer(args: argparse.Namespace) -> int: - store = PointerStore(_project(args)) - try: - if args.pointer_cmd == "get": - print(store.get()) - elif args.pointer_cmd == "set": - store.set(args.work_id) - print(f"pointer set to: {args.work_id}") - elif args.pointer_cmd == "reset": - store.reset() - print("pointer cleared") - else: - return 2 - except PointerError as exc: - print(str(exc), file=sys.stderr) - return 2 - return 0 - - -def cmd_context(args: argparse.Namespace) -> int: - from .lessons_ledger import LEDGER_KINDS - - store = ContextStore(_project(args)) - action = args.context_cmd - if action == "persist-lesson": - body = args.body - if body == "-": - body = sys.stdin.read() - work_id = getattr(args, "work_id", "") or "" - area = getattr(args, "area", "") or "" - if not work_id and not area: - print("persist-lesson: --work-id or --area is required", file=sys.stderr) - return 2 - keywords = [k.strip() for k in (args.keywords or "").split(",") if k.strip()] - try: - result = store.persist_lesson( - kind=args.kind, - work_id=work_id, - body=body, - title=getattr(args, "title", "") or "", - area=area, - source=args.source or "cli", - phase=args.phase or "", - keywords=keywords or None, - accept=bool(getattr(args, "accept", False)), - project_guide=not args.no_guide, - readiness=getattr(args, "readiness", "") or None, - review_result=getattr(args, "review_result", "") or None, - rework=getattr(args, "rework", None), - context_files=getattr(args, "context_files", None), - validate_cycles=getattr(args, "validate_cycles", None), - review_cycles=getattr(args, "review_cycles", None), - ) - except ValueError as exc: - print(f"persist-lesson: {exc}", file=sys.stderr) - return 2 - print(json.dumps(result.as_dict(), indent=2)) - return 0 if result.git.get("ok") else 1 - if action == "persist-entry": - print( - "warning: persist-entry is deprecated; use context persist-lesson", - file=sys.stderr, - ) - body = args.body - if body == "-": - body = sys.stdin.read() - result = store.persist_lesson( - kind=args.kind if args.kind in LEDGER_KINDS else "decision", - work_id=getattr(args, "work_id", "") or "", - body=body, - area=args.area or "", - source=args.source or "cli", - phase=args.phase or "", - project_guide=not args.no_guide, - ) - print(json.dumps(result.as_dict(), indent=2)) - return 0 if result.git.get("ok") else 1 - if action == "accept": - out = store.accept( - work_id=getattr(args, "work_id", "") or "", - ids=[i.strip() for i in (getattr(args, "ids", "") or "").split(",") if i.strip()] - or None, - discard_rest=bool(getattr(args, "discard_rest", False)), - project_guide=not getattr(args, "no_guide", False), - ) - print(json.dumps(out, indent=2)) - return 0 - if action == "show": - rec = store.show(args.record_id) - if rec is None: - print(json.dumps({"error": "not found", "id": args.record_id}, indent=2)) - return 1 - print(json.dumps(rec, indent=2)) - return 0 - if action == "parity": - out = store.parity(repair=bool(getattr(args, "repair", False))) - print(json.dumps(out, indent=2)) - return 0 if out.get("ok") else 1 - if action == "digest": - areas = [a.strip() for a in (getattr(args, "areas", "") or "").split(",") if a.strip()] - keywords = [k.strip() for k in (getattr(args, "keywords", "") or "").split(",") if k.strip()] - print( - json.dumps( - store.digest( - work_id=getattr(args, "work_id", "") or "", - areas=areas or None, - keywords=keywords or None, - limit=int(getattr(args, "limit", 8) or 8), - ), - indent=2, - ) - ) - return 0 - if action == "retrieve": - print( - json.dumps( - store.retrieve( - work_id=args.work_id or "", - area=args.area or "", - kind=getattr(args, "kind", "") or "", - keyword=getattr(args, "keyword", "") or "", - query=getattr(args, "query", "") or "", - rank=getattr(args, "rank", "") or "", - include_staged=not bool(getattr(args, "no_staged", False)), - limit=int(getattr(args, "limit", 50) or 50), - ), - indent=2, - ) - ) - return 0 - if action == "eval-retrieve": - from pathlib import Path - - from .retrieve import load_and_evaluate - - fixture = Path(args.fixture) - if not fixture.is_file(): - print(f"eval-retrieve: fixture not found: {fixture}", file=sys.stderr) - return 2 - print(json.dumps(load_and_evaluate(fixture), indent=2)) - return 0 - if action == "metrics": - try: - payload = store.metrics_query( - construct=getattr(args, "construct", "") or "", - work_id=getattr(args, "work_id", "") or "", - phase=getattr(args, "phase", "") or "", - include_staged=not bool(getattr(args, "no_staged", False)), - ) - except ValueError as exc: - print(f"metrics: {exc}", file=sys.stderr) - return 2 - print(json.dumps(payload, indent=2)) - return 0 - if action == "coverage": - from .db import LocalIndex - - print(json.dumps(LocalIndex(_project(args)).capability_coverage(), indent=2)) - return 0 - if action == "backends": - from .persistence import load_config, save_config, status_dict - - project = _project(args) - if getattr(args, "set_backends", None) is not None: - raw = str(args.set_backends or "").strip() - if not raw: - print("error: --set requires a non-empty backend list", file=sys.stderr) - return 2 - backends = [p.strip() for p in raw.replace(";", ",").split(",") if p.strip()] - cfg = load_config(project) - cfg["backends"] = backends - if getattr(args, "guide_base_url", None): - cfg["guide_base_url"] = args.guide_base_url - if getattr(args, "notes", None) is not None: - cfg["notes"] = args.notes - try: - print(json.dumps(save_config(project, cfg), indent=2)) - except ValueError as exc: - print(f"error: {exc}", file=sys.stderr) - return 2 - return 0 - print(json.dumps(status_dict(project), indent=2)) - return 0 - if action == "guide-query": - from .guide_client import GuideClient, resolve_guide_base_url - from .guide_query import format_guide_answer, run_guide_query - from .persistence import load_config as load_persist_cfg - - project = _project(args) - cfg = load_persist_cfg(project) - base = resolve_guide_base_url( - explicit=getattr(args, "guide_url", None) or None, - project_url=str(cfg.get("guide_base_url") or ""), - ) - client = GuideClient(base, timeout=float(getattr(args, "timeout", 30) or 30)) - tool_args = {} - if getattr(args, "tool_json", None): - tool_args = json.loads(args.tool_json) - try: - payload = run_guide_query( - client, - work_id=getattr(args, "work_id", "") or "", - area=getattr(args, "area", "") or "", - lesson_id=getattr(args, "lesson_id", "") or "", - label=getattr(args, "label", "") or "", - question=getattr(args, "question", "") or "", - stats=bool(getattr(args, "stats", False)), - tool=getattr(args, "tool", "") or "", - tool_args=tool_args, - limit=int(getattr(args, "limit", 20) or 20), - ) - except ValueError as exc: - print(f"error: {exc}", file=sys.stderr) - return 2 - if getattr(args, "text", False): - print(format_guide_answer(payload)) - else: - print(json.dumps(payload, indent=2)) - return 0 if payload.get("ok") else 1 - if action == "mcp-call": - from .guide_client import GuideClient, resolve_guide_base_url - from .persistence import load_config as load_persist_cfg - - project = _project(args) - cfg = load_persist_cfg(project) - base = resolve_guide_base_url( - explicit=getattr(args, "guide_url", None) or None, - project_url=str(cfg.get("guide_base_url") or ""), - ) - client = GuideClient(base, timeout=float(getattr(args, "timeout", 30) or 30)) - tool = str(getattr(args, "tool", "") or "").strip() - if not tool: - print("error: --tool required (spdd_workSubgraph, …)", file=sys.stderr) - return 2 - raw = getattr(args, "json", None) or getattr(args, "tool_json", None) or "{}" - try: - arguments = json.loads(raw) - except json.JSONDecodeError as exc: - print(f"error: invalid JSON arguments: {exc}", file=sys.stderr) - return 2 - payload = client.call_mcp_tool(tool, arguments) - print(json.dumps(payload, indent=2)) - return 0 if payload.get("ok") else 1 - return 2 - - -def cmd_storage(args: argparse.Namespace) -> int: - from .storage_migrate import StorageMigration - - mig = StorageMigration(_project(args)) - if args.storage_cmd == "status": - print(json.dumps(mig.detect(), indent=2)) - return 0 - if args.storage_cmd == "migrate": - print(json.dumps(mig.run(dry_run=bool(args.dry_run)), indent=2)) - return 0 - return 2 - - -def cmd_agent_context(args: argparse.Namespace) -> int: - """Upgrade/re-init and quiet-mode helpers for agent-context cleanup.""" - from .agent_context_upgrade import AgentContextUpgrade - from .quiet import is_quiet, quiet_resume_blurb - - project = _project(args) - action = args.agent_context_cmd - if action == "detect": - print(json.dumps(AgentContextUpgrade(project).detect(), indent=2)) - return 0 - if action == "upgrade": - result = AgentContextUpgrade(project).run( - dry_run=bool(args.dry_run), - rebuild_db=not bool(args.no_rebuild), - ) - print(json.dumps(result.as_dict(), indent=2)) - return 0 if result.ok else 1 - if action == "quiet-status": - quiet = is_quiet(project, quiet_flag=bool(getattr(args, "quiet", False))) - print( - json.dumps( - { - "quiet": quiet, - "blurb": quiet_resume_blurb(guide_live=bool(args.guide_live)), - "hot_session_dir": str(project.hot_session_dir()), - "current_session": str(project.current_session_path()), - }, - indent=2, - ) - ) - return 0 - return 2 - - -def cmd_version(_: argparse.Namespace) -> int: - print(__version__) - return 0 - - -def cmd_shell(args: argparse.Namespace) -> int: - """Bridge to remaining v1 shell scripts under scripts/.""" - 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(): - print(f"shell bridge: script not found: {args.script}", file=sys.stderr) - return 1 - return subprocess.call([str(script), *args.script_args], cwd=root) - - -def cmd_links(args: argparse.Namespace) -> int: - print(LocalSyncService(_project(args)).links_report(args.work_id), end="") - return 0 - - -def cmd_sync_links(args: argparse.Namespace) -> int: - svc = LocalSyncService(_project(args)) - work_id = args.work_id or getattr(args, "work_id_pos", None) - if args.repair: - actions = svc.repair_links(work_id, dry_run=args.dry_run) - if not actions: - print("sync-links: nothing to repair") - else: - for a in actions: - print(a) - return 0 - findings = svc.check_links(work_id) - if not findings: - print("sync-links: no drift detected") - return 0 - repairable = [f for f in findings if f.repairable] - manual = [f for f in findings if not f.repairable] - for f in findings: - flag = "repairable" if f.repairable else "manual" - print(f"[{flag}] {f.work_id}: {f.code} — {f.message}") - if repairable: - print( - f"\n{len(findings)} finding(s) ({len(repairable)} repairable). " - "Re-run with --repair to apply safe fixes." - ) - return 1 - print( - f"\n{len(manual)} manual finding(s) (TBD keys / planning gaps). " - "Use `issues draft|push` or edit milestone ## Jira / ## GitHub." - ) - return 0 - - -def cmd_sync_roadmap(args: argparse.Namespace) -> int: - block = LocalSyncService(_project(args)).sync_roadmap( - roadmap=args.roadmap, dry_run=args.dry_run - ) - if args.dry_run: - print(block) - else: - print(f"Updated {args.roadmap} SDLC-SPDD summary from canvases.") - return 0 - - -def cmd_issues(args: argparse.Namespace) -> int: - svc = IssueSyncService(_project(args)) - action = args.issues_cmd - if action == "draft": - fmt = getattr(args, "format", "markdown") or "markdown" - for draft in svc.draft(args.work_id, system=args.system): - print(f"=== {draft.system} draft for {draft.work_id} ===") - print(f"title: {draft.title}") - print(f"labels: {', '.join(draft.labels) or '-'}") - if draft.system == "jira": - print(f"description_format: {draft.extra.get('description_format', 'adf')}") - if fmt == "adf": - print("body (ADF):") - print(json.dumps(draft.extra.get("description_adf"), indent=2)) - elif fmt == "wiki": - print("body (wiki markup):") - print(draft.extra.get("description_wiki") or "") - else: - print("body (markdown — source for ADF/wiki conversion):") - print(draft.body) - else: - print(f"extra: {draft.extra}") - print("body:") - print(draft.body) - print() - return 0 - if action == "push": - if args.system == "both": - print("issues push requires --system jira|github", file=sys.stderr) - return 2 - desc_fmt = getattr(args, "description_format", None) - print( - svc.push( - args.work_id, - args.system, - apply=args.apply, - description_format=desc_fmt, - ) - ) - return 0 - if action == "pull": - if args.system == "both": - print("issues pull requires --system jira|github", file=sys.stderr) - return 2 - print(svc.pull(args.work_id, args.system, apply=args.apply)) - return 0 - if action == "upload-adf": - issue_key = getattr(args, "issue", None) or args.work_id - adf_file = getattr(args, "adf_file", None) - if not adf_file: - print("issues upload-adf requires --file PATH", file=sys.stderr) - return 2 - print( - svc.upload_adf( - issue_key, - Path(adf_file), - apply=args.apply, - description_format=getattr(args, "description_format", None), - ) - ) - return 0 - if action == "download-adf": - issue_key = getattr(args, "issue", None) or args.work_id - adf_file = getattr(args, "adf_file", None) - print( - svc.download_adf( - issue_key, - adf_path=Path(adf_file) if adf_file else None, - apply=args.apply, - ) - ) - return 0 - if action == "link": - jira_key = getattr(args, "jira_key", None) or "" - if not jira_key: - print("issues link requires JIRA-KEY positional argument", file=sys.stderr) - return 2 - result = svc.link_jira_local( - args.work_id, - jira_key, - summary=getattr(args, "summary", None) or None, - issue_type=getattr(args, "issue_type", None) or None, - apply=args.apply, - ) - for line in result.get("actions") or []: - print(line) - if not args.apply: - print("\nRe-run with --apply to write local links.") - return 0 - return 2 - - -def cmd_commit_message(args: argparse.Namespace) -> int: - svc = CommitMessageService(_project(args)) - try: - if args.json: - print( - svc.report_json( - base=args.base, - work_id=args.work_id or "", - hint=args.hint or "", - max_diff_chars=args.max_diff, - ), - end="", - ) - else: - print( - svc.report_text( - base=args.base, - work_id=args.work_id or "", - hint=args.hint or "", - max_diff_chars=args.max_diff, - ), - end="", - ) - except CommitMessageError as exc: - print(f"commit-message: {exc}", file=sys.stderr) - return 1 - return 0 - - -def cmd_sunset(args: argparse.Namespace) -> int: - svc = SunsetService(_project(args)) - try: - if args.json: - print( - svc.report_json( - args.work_id or None, - apply=bool(args.apply), - accept=bool(args.accept), - ), - end="", - ) - else: - print( - svc.report_text( - args.work_id or None, - apply=bool(args.apply), - accept=bool(args.accept), - ), - end="", - ) - except SunsetError as exc: - print(f"sunset: {exc}", file=sys.stderr) - return 1 - return 0 - - -def cmd_db(args: argparse.Namespace) -> int: - idx = LocalIndex(_project(args)) - action = args.db_cmd - if action == "rebuild": - print(idx.rebuild().as_text(), end="") - return 0 - if action == "status": - print(idx.status_text(), end="") - return 0 - if action == "path": - print(idx.db_path) - return 0 - if action == "query": - if args.sql: - rows = idx.query_sql(args.sql) - else: - rows = idx.find( - work_id=args.work_id or "", - status=args.status or "", - search=args.search or "", - limit=args.limit, - ) - cols = args.columns.split(",") if args.columns else None - if cols: - cols = [c.strip() for c in cols if c.strip()] - if args.json: - print(json.dumps(rows, indent=2)) - else: - print(format_rows(rows, cols), end="") - return 0 - if action == "lookup": - work_id = (args.work_id or "").strip() - if not work_id: - print("db lookup: --work-id is required", file=sys.stderr) - return 2 - search = args.search or "" - if args.markdown: - print( - idx.lookup_markdown( - work_id, - search=search, - search_limit=args.limit, - ), - end="", - ) - else: - print( - json.dumps( - idx.lookup( - work_id, - search=search, - search_limit=args.limit, - ), - indent=2, - ) - ) - return 0 - if action == "export": - out = Path(args.output) if args.output else None - if args.format == "sql": - text = idx.export_sql(out) - else: - text = idx.export_json(out) - if out: - print(f"Wrote {args.format} export to {out}") - else: - print(text, end="") - return 0 - return 2 - - -def cmd_quick(args: argparse.Namespace) -> int: - """Zero-ceremony LOCAL-* session start (alias for local start).""" - intent = (args.intent or "").strip() - if not intent: - print("quick requires an intent string", file=sys.stderr) - return 2 - svc = LocalSessionService(_project(args)) - session = svc.start(intent=intent, title=intent) - print(f"Started local session {session.id}") - print(f"Pointer set. Artifacts: .sdlc/local-sessions/{session.id}/") - print("Brief: .sdlc/current-local-session.md") - print("This work stays offline until: ./scripts/sdlc.sh local promote --type feature --name \"...\"") - return 0 - - -def cmd_local(args: argparse.Namespace) -> int: - svc = LocalSessionService(_project(args)) - action = args.local_cmd - if action == "start": - session = svc.start( - name=args.name or "", - title=args.title or "", - intent=args.intent or "", - branch=args.branch or "", - ) - print(f"Started local session {session.id}") - print(f"Pointer set. Artifacts: .sdlc/local-sessions/{session.id}/") - print("This work stays offline until: ./scripts/sdlc.sh local promote --type feature --name \"...\"") - return 0 - if action == "list": - print(svc.list_text(include_closed=args.all), end="") - return 0 - if action == "status": - print(svc.status_text(args.session), end="") - return 0 - if action == "capture": - session = svc.capture(args.summary, session_id=args.session) - print(f"Captured into {session.id}") - return 0 - if action == "shelf": - session = svc.shelf(args.reason, session_id=args.session) - print(f"Shelved local session {session.id}: {args.reason}") - return 0 - if action == "resume": - session = svc.resume(args.session_id) - print(f"Resumed local session {session.id}") - return 0 - if action == "abandon": - session = svc.abandon(session_id=args.session, force=args.force) - print(f"Abandoned local session {session.id}") - return 0 - if action == "promote": - session, work_id = svc.promote( - work_type=args.type, - name=args.name or "", - session_id=args.session, - milestone=args.milestone or "", - claim=not args.no_claim, - dry_run=args.dry_run, - from_git=args.from_git or "", - ) - if args.dry_run: - print(f"[dry-run] would promote {session.id} -> {work_id}") - return 0 - print(f"Promoted {session.id} -> {work_id}") - print(f" canvas: spdd/canvas/{work_id}.md") - print(f" requirement: requirements/milestones/{work_id}.md") - if not args.no_claim: - print(f"Claimed {work_id} — commit spdd/memory/registry.jsonl when sharing.") - return 0 - return 2 - - -def cmd_work(args: argparse.Namespace) -> int: - """Work helpers that are engine-backed (init from ADF, etc.).""" - action = args.work_cmd - if action == "init-from-adf": - svc = AdfWorkService(_project(args)) - try: - result = svc.init_from_adf( - args.path, - work_type=args.type, - title=args.title or "", - work_id=args.work_id or "", - claim=not args.no_claim, - dry_run=args.dry_run, - ) - except (OSError, ValueError, PermissionError, FileExistsError) as exc: - print(str(exc), file=sys.stderr) - return 1 - prefix = "[dry-run] would create" if result.dry_run else "Created" - print(f"{prefix} {result.work_id} from {result.adf_path}") - print(f" title: {result.title}") - print(f" canvas: {result.canvas_path}") - print(f" requirement: {result.requirement_path}") - if result.source_issue: - print(f" source issue: {result.source_issue}") - print(f" next: {result.next_command}") - return 0 - return 2 - - -def cmd_viewer(args: argparse.Namespace) -> int: - """Launch the ADF WYSIWYG viewer (binds localhost by default).""" - try: - from .viewer.app import run_viewer - except ImportError as exc: - print( - "viewer requires Flask. Install with: python3 -m pip install -e './engine[viewer]'", - file=sys.stderr, - ) - print(str(exc), file=sys.stderr) - return 1 - project = _project(args) - host = "0.0.0.0" if getattr(args, "lan", False) else args.host - run_viewer(project.root, host=host, port=args.port, debug=bool(args.debug)) - return 0 - - -def cmd_installer(args: argparse.Namespace) -> int: - """Launch the ops console (install/upgrade, SQLite, rollback, Guide).""" - try: - from .installer.app import run_installer - except ImportError as exc: - print( - "installer/console requires Flask. Install with: " - "python3 -m pip install -e './engine[viewer]'", - file=sys.stderr, - ) - print(str(exc), file=sys.stderr) - return 1 - project = _project(args) - host = "0.0.0.0" if getattr(args, "lan", False) else args.host - target = getattr(args, "target", None) or str(project.root) - if getattr(args, "playground", False): - from .installer.playground import materialize_playground - - dest = materialize_playground(getattr(args, "playground_dir", None)) - print(f"Playground seeded at {dest}") - target = str(dest) - run_installer( - target, - host=host, - port=args.port, - debug=bool(args.debug), - open_browser=not bool(getattr(args, "no_browser", False)), - ) - return 0 - - -def cmd_template(args: argparse.Namespace) -> int: - """List / render / validate ADF template combos.""" - lib = AdfTemplateLibrary() - action = args.template_cmd - try: - if action == "list": - combos = [c.to_dict() for c in lib.list_combos()] - if args.json: - print(json.dumps({"ok": True, "combos": combos}, indent=2)) - else: - for c in combos: - parts = ",".join(c["parts"]) - print(f"{c['id']}\t{c['title']}\tparts={parts}") - return 0 - if action == "validate": - combos = lib.list_combos() - errors: list[str] = [] - for combo in combos: - try: - lib.load_combo(combo.id) - except TemplateError as exc: - errors.append(str(exc)) - # Also validate stock schemas exist and accept a trivial doc - try: - sample = {"type": "doc", "version": 1, "content": [{"type": "paragraph"}]} - schema_errs = lib.validate_adf(sample) - errors.extend(schema_errs) - except TemplateError as exc: - errors.append(str(exc)) - if errors: - print(json.dumps({"ok": False, "errors": errors}, indent=2)) - return 1 - print(json.dumps({"ok": True, "combos": len(combos)}, indent=2)) - return 0 - if action == "render": - project = _project(args) - work_id = (args.work_id or "").strip() - if not work_id: - print("template render requires --work-id", file=sys.stderr) - return 1 - combo_id = (args.combo or "").strip() or lib.suggest_combo( - work_id, getattr(args, "type", "") or "" - ) - result = lib.render( - project, - work_id, - combo_id, - work_type=getattr(args, "type", "") or "", - output=args.output, - ) - if args.json: - print(json.dumps(result.to_dict(), indent=2)) - elif args.output: - print(result.output_path) - else: - print(json.dumps(result.adf, indent=2)) - return 0 - except (TemplateError, FileNotFoundError, OSError) as exc: - print(f"sdlc-engine template: {exc}", file=sys.stderr) - return 1 - print(f"unknown template command: {action}", file=sys.stderr) - return 1 diff --git a/engine/src/sdlc_engine/cli_parser.py b/engine/src/sdlc_engine/cli_parser.py index 60d3fee8..20db6cdc 100644 --- a/engine/src/sdlc_engine/cli_parser.py +++ b/engine/src/sdlc_engine/cli_parser.py @@ -6,7 +6,7 @@ from .metrics import CAPTURE_CONSTRUCTS, NON_CAPTURE_CONSTRUCTS -from .cli_commands import ( +from .commands import ( cmd_next, cmd_status, cmd_resume, @@ -24,8 +24,12 @@ cmd_archive, cmd_pointer, cmd_context, - cmd_storage, - cmd_agent_context, + cmd_quiet_status, + cmd_start, + cmd_capture, + cmd_complete, + cmd_accept, + cmd_session, cmd_version, cmd_shell, cmd_links, @@ -600,36 +604,62 @@ def build_parser() -> argparse.ArgumentParser: cmc.add_argument("--timeout", type=float, default=30.0) cmc.set_defaults(func=cmd_context) - st = sub.add_parser("storage", help="Storage v3 migration and status") - st_sub = st.add_subparsers(dest="storage_cmd", required=True) - st_sub.add_parser("status", help="Detect legacy layout / migration state").set_defaults( - func=cmd_storage - ) - stm = st_sub.add_parser("migrate", help="Migrate legacy agent-context to ledger v3") - stm.add_argument("--dry-run", action="store_true") - stm.set_defaults(func=cmd_storage) - - ac = sub.add_parser( - "agent-context", - help="Upgrade/re-init noisy agent-context runtime + quiet mode (#80/#91)", - ) - ac_sub = ac.add_subparsers(dest="agent_context_cmd", required=True) - ac_sub.add_parser("detect", help="Detect legacy sessions/features noise").set_defaults( - func=cmd_agent_context - ) - acu = ac_sub.add_parser( - "upgrade", - help="Archive sessions/features to .sdlc/legacy-export and seed lean runtime", - ) - acu.add_argument("--dry-run", action="store_true") - acu.add_argument("--no-rebuild", action="store_true") - acu.set_defaults(func=cmd_agent_context) - aqs = ac_sub.add_parser("quiet-status", help="Show quiet/product-test mode status") + aqs = sub.add_parser("quiet-status", help="Show quiet/product-test mode status") aqs.add_argument("--quiet", action="store_true", help="Treat as --quiet flag set") aqs.add_argument("--guide-live", action="store_true") - aqs.set_defaults(func=cmd_agent_context) - - shell = sub.add_parser("shell", help="Run a v1 scripts/*.sh via bridge") + aqs.set_defaults(func=cmd_quiet_status) + + st = sub.add_parser("start", help="Open the session brief for the active Work ID at its phase") + st.add_argument("--work-id", default="") + st.add_argument("--phase", default="") + st.set_defaults(func=cmd_start) + + cap = sub.add_parser("capture", help="Guarded capture (pointer must match); stages lessons") + cap.add_argument("--work-id", default="") + cap.add_argument("--phase", default="") + cap.add_argument("script_args", nargs=argparse.REMAINDER, help="capture-session-memory.sh options, e.g. --summary ...") + cap.set_defaults(func=cmd_capture) + + comp = sub.add_parser("complete", help="Capture --complete with a required verify receipt") + comp.add_argument("script_args", nargs=argparse.REMAINDER) + comp.set_defaults(func=cmd_complete) + + acc = sub.add_parser("accept", help="Promote staged lessons to spdd/memory/lessons.jsonl") + acc.add_argument("--work-id", default="") + acc.add_argument("--ids", default="", help="Comma-separated record ids") + acc.add_argument("--discard-rest", action="store_true") + acc.add_argument("--no-guide", action="store_true") + acc.add_argument("--commit", action="store_true", help="git add + commit the ledger") + acc.add_argument("--list", action="store_true", help="Show staged records") + acc.set_defaults(func=cmd_accept) + + ses = sub.add_parser("session", help="Engine callbacks used by the session scripts") + ses_sub = ses.add_subparsers(dest="session_cmd", required=True) + sb = ses_sub.add_parser("brief", help="Workflow brief table (markdown)") + sb.add_argument("--work-id", default="") + sb.set_defaults(func=cmd_session) + stt = ses_sub.add_parser("touch", help="Mark a session start for a Work ID/phase") + stt.add_argument("--work-id", required=True) + stt.add_argument("--phase", required=True) + stt.add_argument("--milestone", default="") + stt.set_defaults(func=cmd_session) + src = ses_sub.add_parser("record-capture", help="Record a capture on the workflow state") + src.add_argument("--work-id", required=True) + src.add_argument("--phase", default="resume") + src.set_defaults(func=cmd_session) + srec = ses_sub.add_parser("recommend", help="Assistant command for a phase (architecture-first)") + srec.add_argument("--work-id", required=True) + srec.add_argument("--phase", required=True) + srec.add_argument("--operation", default="") + srec.set_defaults(func=cmd_session) + sjs = ses_sub.add_parser("jira-status", help="<KEY> | draft | missing") + sjs.add_argument("--work-id", required=True) + sjs.set_defaults(func=cmd_session) + sja = ses_sub.add_parser("jira-ask", help="Agent instruction when the Jira key is unset") + sja.add_argument("--work-id", required=True) + sja.set_defaults(func=cmd_session) + + shell = sub.add_parser("shell", help="Run a session script from <home>/scripts via bridge") shell.add_argument("script") shell.add_argument("script_args", nargs=argparse.REMAINDER) shell.set_defaults(func=cmd_shell) diff --git a/engine/src/sdlc_engine/commands/__init__.py b/engine/src/sdlc_engine/commands/__init__.py new file mode 100644 index 00000000..a739e373 --- /dev/null +++ b/engine/src/sdlc_engine/commands/__init__.py @@ -0,0 +1,95 @@ +"""CLI command handlers, grouped by concern.""" + +from __future__ import annotations + +from .state import ( + _project, + cmd_advance, + cmd_gate, + cmd_list_shelved, + cmd_next, + cmd_pointer, + cmd_quiet_status, + cmd_resume, + cmd_shelf, + cmd_shell, + cmd_skip, + cmd_status, + cmd_sync, + cmd_version, +) +from .team import ( + cmd_archive, + cmd_claim, + cmd_list_work, + cmd_release, + cmd_sync_team, + cmd_team, +) +from .context import ( + cmd_accept, + cmd_capture, + cmd_complete, + cmd_context, + cmd_session, + cmd_start, +) +from .integrations import ( + cmd_commit_message, + cmd_issues, + cmd_links, + cmd_sunset, + cmd_sync_links, + cmd_sync_roadmap, +) +from .storage import ( + cmd_db, + cmd_installer, + cmd_local, + cmd_quick, + cmd_template, + cmd_viewer, + cmd_work, +) + +__all__ = [ + "_project", + "cmd_accept", + "cmd_advance", + "cmd_archive", + "cmd_capture", + "cmd_claim", + "cmd_commit_message", + "cmd_complete", + "cmd_context", + "cmd_db", + "cmd_gate", + "cmd_installer", + "cmd_issues", + "cmd_links", + "cmd_list_shelved", + "cmd_list_work", + "cmd_local", + "cmd_next", + "cmd_pointer", + "cmd_quick", + "cmd_quiet_status", + "cmd_release", + "cmd_resume", + "cmd_session", + "cmd_shelf", + "cmd_shell", + "cmd_skip", + "cmd_start", + "cmd_status", + "cmd_sunset", + "cmd_sync", + "cmd_sync_links", + "cmd_sync_roadmap", + "cmd_sync_team", + "cmd_team", + "cmd_template", + "cmd_version", + "cmd_viewer", + "cmd_work", +] diff --git a/engine/src/sdlc_engine/commands/context.py b/engine/src/sdlc_engine/commands/context.py new file mode 100644 index 00000000..327800f9 --- /dev/null +++ b/engine/src/sdlc_engine/commands/context.py @@ -0,0 +1,329 @@ +"""Context store, session bridge, and lessons ledger commands.""" + +from __future__ import annotations + +import argparse +import json +import subprocess +import sys +from ..context_store import ContextStore + +from .state import _project + + +def cmd_context(args: argparse.Namespace) -> int: + from ..lessons_ledger import LEDGER_KINDS + + store = ContextStore(_project(args)) + action = args.context_cmd + if action == "persist-lesson": + body = args.body + if body == "-": + body = sys.stdin.read() + work_id = getattr(args, "work_id", "") or "" + area = getattr(args, "area", "") or "" + if not work_id and not area: + print("persist-lesson: --work-id or --area is required", file=sys.stderr) + return 2 + keywords = [k.strip() for k in (args.keywords or "").split(",") if k.strip()] + try: + result = store.persist_lesson( + kind=args.kind, + work_id=work_id, + body=body, + title=getattr(args, "title", "") or "", + area=area, + source=args.source or "cli", + phase=args.phase or "", + keywords=keywords or None, + accept=bool(getattr(args, "accept", False)), + project_guide=not args.no_guide, + readiness=getattr(args, "readiness", "") or None, + review_result=getattr(args, "review_result", "") or None, + rework=getattr(args, "rework", None), + context_files=getattr(args, "context_files", None), + validate_cycles=getattr(args, "validate_cycles", None), + review_cycles=getattr(args, "review_cycles", None), + ) + except ValueError as exc: + print(f"persist-lesson: {exc}", file=sys.stderr) + return 2 + print(json.dumps(result.as_dict(), indent=2)) + return 0 if result.git.get("ok") else 1 + if action == "persist-entry": + print( + "warning: persist-entry is deprecated; use context persist-lesson", + file=sys.stderr, + ) + body = args.body + if body == "-": + body = sys.stdin.read() + result = store.persist_lesson( + kind=args.kind if args.kind in LEDGER_KINDS else "decision", + work_id=getattr(args, "work_id", "") or "", + body=body, + area=args.area or "", + source=args.source or "cli", + phase=args.phase or "", + project_guide=not args.no_guide, + ) + print(json.dumps(result.as_dict(), indent=2)) + return 0 if result.git.get("ok") else 1 + if action == "accept": + out = store.accept( + work_id=getattr(args, "work_id", "") or "", + ids=[i.strip() for i in (getattr(args, "ids", "") or "").split(",") if i.strip()] + or None, + discard_rest=bool(getattr(args, "discard_rest", False)), + project_guide=not getattr(args, "no_guide", False), + ) + print(json.dumps(out, indent=2)) + return 0 + if action == "show": + rec = store.show(args.record_id) + if rec is None: + print(json.dumps({"error": "not found", "id": args.record_id}, indent=2)) + return 1 + print(json.dumps(rec, indent=2)) + return 0 + if action == "parity": + out = store.parity(repair=bool(getattr(args, "repair", False))) + print(json.dumps(out, indent=2)) + return 0 if out.get("ok") else 1 + if action == "digest": + areas = [a.strip() for a in (getattr(args, "areas", "") or "").split(",") if a.strip()] + keywords = [k.strip() for k in (getattr(args, "keywords", "") or "").split(",") if k.strip()] + print( + json.dumps( + store.digest( + work_id=getattr(args, "work_id", "") or "", + areas=areas or None, + keywords=keywords or None, + limit=int(getattr(args, "limit", 8) or 8), + ), + indent=2, + ) + ) + return 0 + if action == "retrieve": + print( + json.dumps( + store.retrieve( + work_id=args.work_id or "", + area=args.area or "", + kind=getattr(args, "kind", "") or "", + keyword=getattr(args, "keyword", "") or "", + query=getattr(args, "query", "") or "", + rank=getattr(args, "rank", "") or "", + include_staged=not bool(getattr(args, "no_staged", False)), + limit=int(getattr(args, "limit", 50) or 50), + ), + indent=2, + ) + ) + return 0 + if action == "eval-retrieve": + from pathlib import Path + + from ..retrieve import load_and_evaluate + + fixture = Path(args.fixture) + if not fixture.is_file(): + print(f"eval-retrieve: fixture not found: {fixture}", file=sys.stderr) + return 2 + print(json.dumps(load_and_evaluate(fixture), indent=2)) + return 0 + if action == "metrics": + try: + payload = store.metrics_query( + construct=getattr(args, "construct", "") or "", + work_id=getattr(args, "work_id", "") or "", + phase=getattr(args, "phase", "") or "", + include_staged=not bool(getattr(args, "no_staged", False)), + ) + except ValueError as exc: + print(f"metrics: {exc}", file=sys.stderr) + return 2 + print(json.dumps(payload, indent=2)) + return 0 + if action == "coverage": + from ..db import LocalIndex + + print(json.dumps(LocalIndex(_project(args)).capability_coverage(), indent=2)) + return 0 + if action == "backends": + from ..persistence import load_config, save_config, status_dict + + project = _project(args) + if getattr(args, "set_backends", None) is not None: + raw = str(args.set_backends or "").strip() + if not raw: + print("error: --set requires a non-empty backend list", file=sys.stderr) + return 2 + backends = [p.strip() for p in raw.replace(";", ",").split(",") if p.strip()] + cfg = load_config(project) + cfg["backends"] = backends + if getattr(args, "guide_base_url", None): + cfg["guide_base_url"] = args.guide_base_url + if getattr(args, "notes", None) is not None: + cfg["notes"] = args.notes + try: + print(json.dumps(save_config(project, cfg), indent=2)) + except ValueError as exc: + print(f"error: {exc}", file=sys.stderr) + return 2 + return 0 + print(json.dumps(status_dict(project), indent=2)) + return 0 + if action == "guide-query": + from ..guide_client import GuideClient, resolve_guide_base_url + from ..guide_query import format_guide_answer, run_guide_query + from ..persistence import load_config as load_persist_cfg + + project = _project(args) + cfg = load_persist_cfg(project) + base = resolve_guide_base_url( + explicit=getattr(args, "guide_url", None) or None, + project_url=str(cfg.get("guide_base_url") or ""), + ) + client = GuideClient(base, timeout=float(getattr(args, "timeout", 30) or 30)) + tool_args = {} + if getattr(args, "tool_json", None): + tool_args = json.loads(args.tool_json) + try: + payload = run_guide_query( + client, + work_id=getattr(args, "work_id", "") or "", + area=getattr(args, "area", "") or "", + lesson_id=getattr(args, "lesson_id", "") or "", + label=getattr(args, "label", "") or "", + question=getattr(args, "question", "") or "", + stats=bool(getattr(args, "stats", False)), + tool=getattr(args, "tool", "") or "", + tool_args=tool_args, + limit=int(getattr(args, "limit", 20) or 20), + ) + except ValueError as exc: + print(f"error: {exc}", file=sys.stderr) + return 2 + if getattr(args, "text", False): + print(format_guide_answer(payload)) + else: + print(json.dumps(payload, indent=2)) + return 0 if payload.get("ok") else 1 + if action == "mcp-call": + from ..guide_client import GuideClient, resolve_guide_base_url + from ..persistence import load_config as load_persist_cfg + + project = _project(args) + cfg = load_persist_cfg(project) + base = resolve_guide_base_url( + explicit=getattr(args, "guide_url", None) or None, + project_url=str(cfg.get("guide_base_url") or ""), + ) + client = GuideClient(base, timeout=float(getattr(args, "timeout", 30) or 30)) + tool = str(getattr(args, "tool", "") or "").strip() + if not tool: + print("error: --tool required (spdd_workSubgraph, …)", file=sys.stderr) + return 2 + raw = getattr(args, "json", None) or getattr(args, "tool_json", None) or "{}" + try: + arguments = json.loads(raw) + except json.JSONDecodeError as exc: + print(f"error: invalid JSON arguments: {exc}", file=sys.stderr) + return 2 + payload = client.call_mcp_tool(tool, arguments) + print(json.dumps(payload, indent=2)) + return 0 if payload.get("ok") else 1 + return 2 + + +def cmd_start(args: argparse.Namespace) -> int: + from ..session import SessionService + + return SessionService(_project(args)).start( + work_id=getattr(args, "work_id", "") or "", phase=getattr(args, "phase", "") or "" + ) + + +def cmd_capture(args: argparse.Namespace) -> int: + from ..session import SessionService + + return SessionService(_project(args)).capture( + list(args.script_args or []), + work_id=getattr(args, "work_id", "") or "", + phase=getattr(args, "phase", "") or "", + ) + + +def cmd_complete(args: argparse.Namespace) -> int: + from ..session import SessionService + + return SessionService(_project(args)).complete(list(args.script_args or [])) + + +def cmd_accept(args: argparse.Namespace) -> int: + """Promote staged lessons to the committed ledger (optionally git-commit it).""" + + from ..lessons_ledger import LessonsLedger + + project = _project(args) + ledger = LessonsLedger(project) + if getattr(args, "list", False): + staged = [r for r in ledger.records(include_staged=True) if r.id in ledger.staged_ids()] + if not staged: + print("No staged records.") + return 0 + for rec in staged: + print(f"{rec.id}\t{rec.kind}\t{rec.work_id}\t{rec.title}") + return 0 + store = ContextStore(project) + out = store.accept( + work_id=getattr(args, "work_id", "") or "", + ids=[i.strip() for i in (getattr(args, "ids", "") or "").split(",") if i.strip()] or None, + discard_rest=bool(getattr(args, "discard_rest", False)), + project_guide=not getattr(args, "no_guide", False), + ) + print(json.dumps(out, indent=2)) + count = int(out.get("accepted_count", 0) or 0) + if getattr(args, "commit", False): + if count == 0 or not project.ledger_path.is_file(): + print("Nothing to commit (no records promoted).") + return 0 + rel = project.rel(project.ledger_path) + wid = getattr(args, "work_id", "") or "all" + subprocess.check_call(["git", "-C", str(project.root), "add", rel]) + subprocess.check_call(["git", "-C", str(project.root), "commit", "-m", f"memory: accept {count} lessons for {wid}"]) + print(f"Committed {rel}") + return 0 + + +def cmd_session(args: argparse.Namespace) -> int: + """Engine callbacks for the shell session scripts (one workflow implementation).""" + from ..session import SessionService, jira_ask_prompt, jira_status + + project = _project(args) + svc = SessionService(project) + action = args.session_cmd + wid = getattr(args, "work_id", "") or "" + if action == "brief": + print(svc.brief_markdown(wid), end="") + return 0 + if action == "touch": + svc.touch_session(wid, args.phase, getattr(args, "milestone", "") or "") + return 0 + if action == "record-capture": + svc.record_capture(wid, getattr(args, "phase", "") or "resume") + return 0 + if action == "recommend": + print(svc.recommend(wid, args.phase, getattr(args, "operation", "") or "")) + return 0 + if action == "jira-status": + print(jira_status(project, wid)) + return 0 + if action == "jira-ask": + text = jira_ask_prompt(project, wid) + if text: + print(text) + return 0 + return 2 diff --git a/engine/src/sdlc_engine/commands/integrations.py b/engine/src/sdlc_engine/commands/integrations.py new file mode 100644 index 00000000..e457c4e5 --- /dev/null +++ b/engine/src/sdlc_engine/commands/integrations.py @@ -0,0 +1,211 @@ +"""Link, roadmap, issue-tracker, commit-message, and sunset commands.""" + +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path +from ..commit_message import CommitMessageError, CommitMessageService +from ..issues import IssueSyncService +from ..sunset import SunsetError, SunsetService +from ..sync_local import LocalSyncService + +from .state import _project + + +def cmd_links(args: argparse.Namespace) -> int: + print(LocalSyncService(_project(args)).links_report(args.work_id), end="") + return 0 + + +def cmd_sync_links(args: argparse.Namespace) -> int: + svc = LocalSyncService(_project(args)) + work_id = args.work_id or getattr(args, "work_id_pos", None) + if args.repair: + actions = svc.repair_links(work_id, dry_run=args.dry_run) + if not actions: + print("sync-links: nothing to repair") + else: + for a in actions: + print(a) + return 0 + findings = svc.check_links(work_id) + if not findings: + print("sync-links: no drift detected") + return 0 + repairable = [f for f in findings if f.repairable] + manual = [f for f in findings if not f.repairable] + for f in findings: + flag = "repairable" if f.repairable else "manual" + print(f"[{flag}] {f.work_id}: {f.code} — {f.message}") + if repairable: + print( + f"\n{len(findings)} finding(s) ({len(repairable)} repairable). " + "Re-run with --repair to apply safe fixes." + ) + return 1 + print( + f"\n{len(manual)} manual finding(s) (TBD keys / planning gaps). " + "Use `issues draft|push` or edit milestone ## Jira / ## GitHub." + ) + return 0 + + +def cmd_sync_roadmap(args: argparse.Namespace) -> int: + block = LocalSyncService(_project(args)).sync_roadmap( + roadmap=args.roadmap, dry_run=args.dry_run + ) + if args.dry_run: + print(block) + else: + print(f"Updated {args.roadmap} SDLC-SPDD summary from canvases.") + return 0 + + +def cmd_issues(args: argparse.Namespace) -> int: + svc = IssueSyncService(_project(args)) + action = args.issues_cmd + if action == "draft": + fmt = getattr(args, "format", "markdown") or "markdown" + for draft in svc.draft(args.work_id, system=args.system): + print(f"=== {draft.system} draft for {draft.work_id} ===") + print(f"title: {draft.title}") + print(f"labels: {', '.join(draft.labels) or '-'}") + if draft.system == "jira": + print(f"description_format: {draft.extra.get('description_format', 'adf')}") + if fmt == "adf": + print("body (ADF):") + print(json.dumps(draft.extra.get("description_adf"), indent=2)) + elif fmt == "wiki": + print("body (wiki markup):") + print(draft.extra.get("description_wiki") or "") + else: + print("body (markdown — source for ADF/wiki conversion):") + print(draft.body) + else: + print(f"extra: {draft.extra}") + print("body:") + print(draft.body) + print() + return 0 + if action == "push": + if args.system == "both": + print("issues push requires --system jira|github", file=sys.stderr) + return 2 + desc_fmt = getattr(args, "description_format", None) + print( + svc.push( + args.work_id, + args.system, + apply=args.apply, + description_format=desc_fmt, + ) + ) + return 0 + if action == "pull": + if args.system == "both": + print("issues pull requires --system jira|github", file=sys.stderr) + return 2 + print(svc.pull(args.work_id, args.system, apply=args.apply)) + return 0 + if action == "upload-adf": + issue_key = getattr(args, "issue", None) or args.work_id + adf_file = getattr(args, "adf_file", None) + if not adf_file: + print("issues upload-adf requires --file PATH", file=sys.stderr) + return 2 + print( + svc.upload_adf( + issue_key, + Path(adf_file), + apply=args.apply, + description_format=getattr(args, "description_format", None), + ) + ) + return 0 + if action == "download-adf": + issue_key = getattr(args, "issue", None) or args.work_id + adf_file = getattr(args, "adf_file", None) + print( + svc.download_adf( + issue_key, + adf_path=Path(adf_file) if adf_file else None, + apply=args.apply, + ) + ) + return 0 + if action == "link": + jira_key = getattr(args, "jira_key", None) or "" + if not jira_key: + print("issues link requires JIRA-KEY positional argument", file=sys.stderr) + return 2 + result = svc.link_jira_local( + args.work_id, + jira_key, + summary=getattr(args, "summary", None) or None, + issue_type=getattr(args, "issue_type", None) or None, + apply=args.apply, + ) + for line in result.get("actions") or []: + print(line) + if not args.apply: + print("\nRe-run with --apply to write local links.") + return 0 + return 2 + + +def cmd_commit_message(args: argparse.Namespace) -> int: + svc = CommitMessageService(_project(args)) + try: + if args.json: + print( + svc.report_json( + base=args.base, + work_id=args.work_id or "", + hint=args.hint or "", + max_diff_chars=args.max_diff, + ), + end="", + ) + else: + print( + svc.report_text( + base=args.base, + work_id=args.work_id or "", + hint=args.hint or "", + max_diff_chars=args.max_diff, + ), + end="", + ) + except CommitMessageError as exc: + print(f"commit-message: {exc}", file=sys.stderr) + return 1 + return 0 + + +def cmd_sunset(args: argparse.Namespace) -> int: + svc = SunsetService(_project(args)) + try: + if args.json: + print( + svc.report_json( + args.work_id or None, + apply=bool(args.apply), + accept=bool(args.accept), + ), + end="", + ) + else: + print( + svc.report_text( + args.work_id or None, + apply=bool(args.apply), + accept=bool(args.accept), + ), + end="", + ) + except SunsetError as exc: + print(f"sunset: {exc}", file=sys.stderr) + return 1 + return 0 diff --git a/engine/src/sdlc_engine/commands/state.py b/engine/src/sdlc_engine/commands/state.py new file mode 100644 index 00000000..4aed3c4b --- /dev/null +++ b/engine/src/sdlc_engine/commands/state.py @@ -0,0 +1,196 @@ +"""Workflow state commands: next/status/advance/gate/pointer/shell.""" + +from __future__ import annotations + +import argparse +import json +import subprocess +import sys +from .. import __version__ +from ..local_sessions import LocalSessionService +from ..phases import GATE_LABELS, advisory_gates_for_phase, format_advisory_gate_rows +from ..pointer import PointerError, PointerStore +from ..project import Project +from ..workflow import WorkflowEngine + + +def _project(args: argparse.Namespace) -> Project: + return Project.resolve(getattr(args, "root", None)) + + +def cmd_next(args: argparse.Namespace) -> int: + print(WorkflowEngine(_project(args)).next_text(), end="") + return 0 + + +def cmd_status(args: argparse.Namespace) -> int: + eng = WorkflowEngine(_project(args)) + if args.json: + print(eng.status_json(args.work_id)) + else: + wid = args.work_id or eng.pointer.get() or "(none)" + print(f"Pointer: {eng.pointer.get() or '(none)'}") + print(f"Work ID: {wid}") + if eng.pointer.get() or args.work_id: + print(eng.status_json(args.work_id)) + return 0 + + +def cmd_resume(args: argparse.Namespace) -> int: + state = WorkflowEngine(_project(args)).resume(args.work_id, phase=args.phase, force=args.force) + print(f"Resumed {state.work_id} at phase: {state.phase}") + return 0 + + +def cmd_advance(args: argparse.Namespace) -> int: + try: + state = WorkflowEngine(_project(args)).advance( + to=args.to, force=getattr(args, "force", False) + ) + except ValueError as exc: + print(str(exc), file=sys.stderr) + return 1 + print(f"Advanced to phase: {state.phase}") + return 0 + + +def cmd_gate(args: argparse.Namespace) -> int: + eng = WorkflowEngine(_project(args)) + work_id = args.work_id or eng.pointer.get() + if not work_id: + print( + "gate: no Work ID (pass --work-id or set the pointer via claim/resume)", + file=sys.stderr, + ) + return 2 + try: + ok, failures = eng.gate_check(work_id, args.phase) + except ValueError as exc: + print(str(exc), file=sys.stderr) + return 2 + advisory = [ + {"gate": name, "label": GATE_LABELS[name], "ran": False} + for name in advisory_gates_for_phase(args.phase) + ] + advisory_rows = format_advisory_gate_rows(args.phase) + if args.json: + print( + json.dumps( + { + "work_id": work_id, + "phase": args.phase, + "ok": ok, + "failures": failures, + "advisory": advisory, + }, + indent=2, + ) + ) + else: + if ok: + print(f"gate {args.phase}: OK for {work_id}") + for row in advisory_rows: + print(row) + else: + print(f"gate {args.phase}: BLOCKED for {work_id}", file=sys.stderr) + for failure in failures: + print(f" - {failure}", file=sys.stderr) + for row in advisory_rows: + print(row, file=sys.stderr) + return 0 if ok else 1 + + +def cmd_skip(args: argparse.Namespace) -> int: + state = WorkflowEngine(_project(args)).skip(args.phase, reason=args.reason) + print(f"Skipped {args.phase}; now at {state.phase}") + return 0 + + +def cmd_shelf(args: argparse.Namespace) -> int: + project = _project(args) + eng = WorkflowEngine(project) + wid = eng.pointer.get() + if wid and wid.upper().startswith("LOCAL-"): + session = LocalSessionService(project).shelf(args.reason, session_id=wid) + print(f"Shelved local session {session.id}: {args.reason}") + return 0 + state = eng.shelf(reason=args.reason) + if state is None: + print("No active pointer to shelf", file=sys.stderr) + return 1 + print(f"Shelved {state.work_id}: {args.reason}") + return 0 + + +def cmd_sync(args: argparse.Namespace) -> int: + state = WorkflowEngine(_project(args)).sync(args.work_id) + print(f"Synced {state.work_id} -> phase {state.phase}") + return 0 + + +def cmd_list_shelved(args: argparse.Namespace) -> int: + rows = WorkflowEngine(_project(args)).list_shelved() + if not rows: + print("(no shelved work)") + return 0 + for wid, phase, at, reason in rows: + print(f"{wid}\t{phase}\t{at}\t{reason}") + return 0 + + +def cmd_pointer(args: argparse.Namespace) -> int: + store = PointerStore(_project(args)) + try: + if args.pointer_cmd == "get": + print(store.get()) + elif args.pointer_cmd == "set": + store.set(args.work_id) + print(f"pointer set to: {args.work_id}") + elif args.pointer_cmd == "reset": + store.reset() + print("pointer cleared") + else: + return 2 + except PointerError as exc: + print(str(exc), file=sys.stderr) + return 2 + return 0 + + +def cmd_quiet_status(args: argparse.Namespace) -> int: + """Show quiet/product-test mode status and the hot session paths.""" + from ..quiet import is_quiet, quiet_resume_blurb + + project = _project(args) + quiet = is_quiet(project, quiet_flag=bool(getattr(args, "quiet", False))) + print( + json.dumps( + { + "quiet": quiet, + "blurb": quiet_resume_blurb(guide_live=bool(args.guide_live)), + "hot_session_dir": str(project.hot_session_dir()), + "current_session": str(project.current_session_path()), + }, + indent=2, + ) + ) + return 0 + + +def cmd_version(_: argparse.Namespace) -> int: + print(__version__) + return 0 + + +def cmd_shell(args: argparse.Namespace) -> int: + """Bridge to the remaining shell session scripts (installed home first).""" + project = _project(args) + name = args.script + candidates = [] + for base in (project.home / "scripts", project.root / "scripts"): + candidates += [base / name, base / f"{name}.sh"] + script = next((c for c in candidates if c.is_file()), None) + if script is None: + print(f"shell bridge: script not found: {name}", file=sys.stderr) + return 1 + return subprocess.call([str(script), *args.script_args], cwd=project.root) diff --git a/engine/src/sdlc_engine/commands/storage.py b/engine/src/sdlc_engine/commands/storage.py new file mode 100644 index 00000000..916fad28 --- /dev/null +++ b/engine/src/sdlc_engine/commands/storage.py @@ -0,0 +1,299 @@ +"""SQLite index, local sessions, work items, viewer, installer, templates.""" + +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path +from ..adf_templates import AdfTemplateLibrary, TemplateError +from ..adf_work import AdfWorkService +from ..db import LocalIndex, format_rows +from ..local_sessions import LocalSessionService + +from .state import _project + + +def cmd_db(args: argparse.Namespace) -> int: + idx = LocalIndex(_project(args)) + action = args.db_cmd + if action == "rebuild": + print(idx.rebuild().as_text(), end="") + return 0 + if action == "status": + print(idx.status_text(), end="") + return 0 + if action == "path": + print(idx.db_path) + return 0 + if action == "query": + if args.sql: + rows = idx.query_sql(args.sql) + else: + rows = idx.find( + work_id=args.work_id or "", + status=args.status or "", + search=args.search or "", + limit=args.limit, + ) + cols = args.columns.split(",") if args.columns else None + if cols: + cols = [c.strip() for c in cols if c.strip()] + if args.json: + print(json.dumps(rows, indent=2)) + else: + print(format_rows(rows, cols), end="") + return 0 + if action == "lookup": + work_id = (args.work_id or "").strip() + if not work_id: + print("db lookup: --work-id is required", file=sys.stderr) + return 2 + search = args.search or "" + if args.markdown: + print( + idx.lookup_markdown( + work_id, + search=search, + search_limit=args.limit, + ), + end="", + ) + else: + print( + json.dumps( + idx.lookup( + work_id, + search=search, + search_limit=args.limit, + ), + indent=2, + ) + ) + return 0 + if action == "export": + out = Path(args.output) if args.output else None + if args.format == "sql": + text = idx.export_sql(out) + else: + text = idx.export_json(out) + if out: + print(f"Wrote {args.format} export to {out}") + else: + print(text, end="") + return 0 + return 2 + + +def cmd_quick(args: argparse.Namespace) -> int: + """Zero-ceremony LOCAL-* session start (alias for local start).""" + intent = (args.intent or "").strip() + if not intent: + print("quick requires an intent string", file=sys.stderr) + return 2 + svc = LocalSessionService(_project(args)) + session = svc.start(intent=intent, title=intent) + print(f"Started local session {session.id}") + print(f"Pointer set. Artifacts: .sdlc/local-sessions/{session.id}/") + print("Brief: .sdlc/current-local-session.md") + print("This work stays offline until: ./scripts/sdlc.sh local promote --type feature --name \"...\"") + return 0 + + +def cmd_local(args: argparse.Namespace) -> int: + svc = LocalSessionService(_project(args)) + action = args.local_cmd + if action == "start": + session = svc.start( + name=args.name or "", + title=args.title or "", + intent=args.intent or "", + branch=args.branch or "", + ) + print(f"Started local session {session.id}") + print(f"Pointer set. Artifacts: .sdlc/local-sessions/{session.id}/") + print("This work stays offline until: ./scripts/sdlc.sh local promote --type feature --name \"...\"") + return 0 + if action == "list": + print(svc.list_text(include_closed=args.all), end="") + return 0 + if action == "status": + print(svc.status_text(args.session), end="") + return 0 + if action == "capture": + session = svc.capture(args.summary, session_id=args.session) + print(f"Captured into {session.id}") + return 0 + if action == "shelf": + session = svc.shelf(args.reason, session_id=args.session) + print(f"Shelved local session {session.id}: {args.reason}") + return 0 + if action == "resume": + session = svc.resume(args.session_id) + print(f"Resumed local session {session.id}") + return 0 + if action == "abandon": + session = svc.abandon(session_id=args.session, force=args.force) + print(f"Abandoned local session {session.id}") + return 0 + if action == "promote": + session, work_id = svc.promote( + work_type=args.type, + name=args.name or "", + session_id=args.session, + milestone=args.milestone or "", + claim=not args.no_claim, + dry_run=args.dry_run, + from_git=args.from_git or "", + ) + if args.dry_run: + print(f"[dry-run] would promote {session.id} -> {work_id}") + return 0 + print(f"Promoted {session.id} -> {work_id}") + print(f" canvas: spdd/canvas/{work_id}.md") + print(f" requirement: requirements/milestones/{work_id}.md") + if not args.no_claim: + print(f"Claimed {work_id} — commit spdd/memory/registry.jsonl when sharing.") + return 0 + return 2 + + +def cmd_work(args: argparse.Namespace) -> int: + """Work helpers that are engine-backed (init from ADF, etc.).""" + action = args.work_cmd + if action == "init-from-adf": + svc = AdfWorkService(_project(args)) + try: + result = svc.init_from_adf( + args.path, + work_type=args.type, + title=args.title or "", + work_id=args.work_id or "", + claim=not args.no_claim, + dry_run=args.dry_run, + ) + except (OSError, ValueError, PermissionError, FileExistsError) as exc: + print(str(exc), file=sys.stderr) + return 1 + prefix = "[dry-run] would create" if result.dry_run else "Created" + print(f"{prefix} {result.work_id} from {result.adf_path}") + print(f" title: {result.title}") + print(f" canvas: {result.canvas_path}") + print(f" requirement: {result.requirement_path}") + if result.source_issue: + print(f" source issue: {result.source_issue}") + print(f" next: {result.next_command}") + return 0 + return 2 + + +def cmd_viewer(args: argparse.Namespace) -> int: + """Launch the ADF WYSIWYG viewer (binds localhost by default).""" + try: + from ..viewer.app import run_viewer + except ImportError as exc: + print( + "viewer requires Flask. Install with: python3 -m pip install -e './engine[viewer]'", + file=sys.stderr, + ) + print(str(exc), file=sys.stderr) + return 1 + project = _project(args) + host = "0.0.0.0" if getattr(args, "lan", False) else args.host + run_viewer(project.root, host=host, port=args.port, debug=bool(args.debug)) + return 0 + + +def cmd_installer(args: argparse.Namespace) -> int: + """Launch the ops console (install/upgrade, SQLite, rollback, Guide).""" + try: + from ..installer.app import run_installer + except ImportError as exc: + print( + "installer/console requires Flask. Install with: " + "python3 -m pip install -e './engine[viewer]'", + file=sys.stderr, + ) + print(str(exc), file=sys.stderr) + return 1 + project = _project(args) + host = "0.0.0.0" if getattr(args, "lan", False) else args.host + target = getattr(args, "target", None) or str(project.root) + if getattr(args, "playground", False): + from ..installer.playground import materialize_playground + + dest = materialize_playground(getattr(args, "playground_dir", None)) + print(f"Playground seeded at {dest}") + target = str(dest) + run_installer( + target, + host=host, + port=args.port, + debug=bool(args.debug), + open_browser=not bool(getattr(args, "no_browser", False)), + ) + return 0 + + +def cmd_template(args: argparse.Namespace) -> int: + """List / render / validate ADF template combos.""" + lib = AdfTemplateLibrary() + action = args.template_cmd + try: + if action == "list": + combos = [c.to_dict() for c in lib.list_combos()] + if args.json: + print(json.dumps({"ok": True, "combos": combos}, indent=2)) + else: + for c in combos: + parts = ",".join(c["parts"]) + print(f"{c['id']}\t{c['title']}\tparts={parts}") + return 0 + if action == "validate": + combos = lib.list_combos() + errors: list[str] = [] + for combo in combos: + try: + lib.load_combo(combo.id) + except TemplateError as exc: + errors.append(str(exc)) + # Also validate stock schemas exist and accept a trivial doc + try: + sample = {"type": "doc", "version": 1, "content": [{"type": "paragraph"}]} + schema_errs = lib.validate_adf(sample) + errors.extend(schema_errs) + except TemplateError as exc: + errors.append(str(exc)) + if errors: + print(json.dumps({"ok": False, "errors": errors}, indent=2)) + return 1 + print(json.dumps({"ok": True, "combos": len(combos)}, indent=2)) + return 0 + if action == "render": + project = _project(args) + work_id = (args.work_id or "").strip() + if not work_id: + print("template render requires --work-id", file=sys.stderr) + return 1 + combo_id = (args.combo or "").strip() or lib.suggest_combo( + work_id, getattr(args, "type", "") or "" + ) + result = lib.render( + project, + work_id, + combo_id, + work_type=getattr(args, "type", "") or "", + output=args.output, + ) + if args.json: + print(json.dumps(result.to_dict(), indent=2)) + elif args.output: + print(result.output_path) + else: + print(json.dumps(result.adf, indent=2)) + return 0 + except (TemplateError, FileNotFoundError, OSError) as exc: + print(f"sdlc-engine template: {exc}", file=sys.stderr) + return 1 + print(f"unknown template command: {action}", file=sys.stderr) + return 1 diff --git a/engine/src/sdlc_engine/commands/team.py b/engine/src/sdlc_engine/commands/team.py new file mode 100644 index 00000000..86a03054 --- /dev/null +++ b/engine/src/sdlc_engine/commands/team.py @@ -0,0 +1,65 @@ +"""Team registry and archive commands.""" + +from __future__ import annotations + +import argparse +import sys +from ..archive import ArchiveService +from ..registry import TeamRegistry + +from .state import _project + + +def cmd_claim(args: argparse.Namespace) -> int: + reg = TeamRegistry(_project(args)) + try: + row = reg.claim( + args.work_id, + force=args.force, + phase=args.phase, + branch=args.branch or "", + pr=args.pr or "", + jira=args.jira or "", + note=args.note or "", + ) + except PermissionError as exc: + print(str(exc), file=sys.stderr) + return 1 + print(f"Claimed {row.work_id} as {row.owner} (phase={row.phase})") + print("Team registry updated — commit spdd/memory/registry.jsonl to share with teammates.") + return 0 + + +def cmd_release(args: argparse.Namespace) -> int: + TeamRegistry(_project(args)).release(reason=args.reason) + print("Released / shelved active work") + return 0 + + +def cmd_team(args: argparse.Namespace) -> int: + print(TeamRegistry(_project(args)).team_text(), end="") + return 0 + + +def cmd_list_work(args: argparse.Namespace) -> int: + print(TeamRegistry(_project(args)).list_work_text(), end="") + return 0 + + +def cmd_sync_team(args: argparse.Namespace) -> int: + TeamRegistry(_project(args)).refresh_done_status() + print("Team registry refreshed from canvas Final Status.") + return 0 + + +def cmd_archive(args: argparse.Namespace) -> int: + svc = ArchiveService(_project(args)) + try: + if args.all: + svc.archive_eligible(dry_run=args.dry_run) + else: + svc.archive_work(args.work_id, dry_run=args.dry_run, force=args.force) + except ValueError as exc: + print(str(exc), file=sys.stderr) + return 1 + return 0 diff --git a/engine/src/sdlc_engine/issue_tracker.py b/engine/src/sdlc_engine/issue_tracker.py deleted file mode 100644 index 8c00ae56..00000000 --- a/engine/src/sdlc_engine/issue_tracker.py +++ /dev/null @@ -1,54 +0,0 @@ -"""Issue tracker selection — re-exports from :mod:`integration_config`.""" - -from __future__ import annotations - -from .integration_config import ( - ALL_TRACKERS, - CONFIG_REL, - DEFAULT_TRACKER, - TRACKER_GITHUB, - TRACKER_JIRA, - TRACKER_NONE, - config_path, - effective_tracker, - load_config, - normalize_tracker, - save_config as save_tracker_only, - status_dict as tracker_status_dict, -) - -__all__ = [ - "ALL_TRACKERS", - "CONFIG_REL", - "DEFAULT_TRACKER", - "TRACKER_GITHUB", - "TRACKER_JIRA", - "TRACKER_NONE", - "config_path", - "effective_tracker", - "load_config", - "normalize_tracker", - "save_tracker_only", - "tracker_status_dict", -] - - -def save_config(project, *, tracker: str, notes: str = "") -> dict: # noqa: ANN001 - """Legacy save API (tracker + notes only).""" - from .integration_config import save_config as _save - - return _save(project, {"tracker": tracker, "notes": notes}) - - -def status_dict(project) -> dict: # noqa: ANN001 - """Legacy status API (tracker fields only).""" - full = tracker_status_dict(project) - return { - "tracker": full.get("tracker"), - "effective_tracker": full.get("effective_tracker"), - "notes": full.get("notes"), - "config_path": full.get("config_path"), - "config_exists": full.get("config_exists"), - "env_override": full.get("env_tracker_override", ""), - "source": "env" if full.get("env_tracker_override") else "file", - } diff --git a/engine/src/sdlc_engine/issues.py b/engine/src/sdlc_engine/issues.py index dd75d8a7..932ace51 100644 --- a/engine/src/sdlc_engine/issues.py +++ b/engine/src/sdlc_engine/issues.py @@ -25,13 +25,14 @@ markdown_to_adf, markdown_to_wiki, ) -from .integration_config import integration_env, resolve_integrations -from .issue_tracker import ( +from .integration_config import ( TRACKER_GITHUB, TRACKER_JIRA, TRACKER_NONE, effective_tracker, + integration_env, load_config as load_tracker_config, + resolve_integrations, ) from .links import ( _JIRA_KEY_RE, diff --git a/engine/src/sdlc_engine/session.py b/engine/src/sdlc_engine/session.py new file mode 100644 index 00000000..0a76d7fc --- /dev/null +++ b/engine/src/sdlc_engine/session.py @@ -0,0 +1,217 @@ +"""Session-facing workflow operations (one engine, REF-003). + +These used to live in the bash workflow twin (``sdlc-workflow.sh`` / +``sdlc-team-registry.sh``). The remaining shell session scripts +(``start-agent-session.sh``, ``capture-session-memory.sh``) call back into the +engine through ``sdlc-engine session ...`` for anything that touches workflow +state, the registry, or the pointer, so there is exactly one implementation. +""" + +from __future__ import annotations + +import os +import subprocess +from dataclasses import dataclass +from pathlib import Path + +from . import canvas as canvas_mod +from .links import collect_links +from .phases import GATE_LABELS, PHASE_ORDER, gates_for_phase, recommended_command +from .project import Project +from .registry import TeamRegistry +from .timeutil import utc_now as _utc_now +from .workflow import WorkflowEngine + + +def jira_status(project: Project, work_id: str) -> str: + """``<KEY>`` when a real key is known (claim note or requirement), ``draft`` + when a ``## Jira`` section exists without a key, else ``missing``.""" + wid = (work_id or "").strip() + if not wid: + return "missing" + row = next((r for r in TeamRegistry(project).rows() if r.work_id == wid), None) + links = collect_links(project, wid, row) + if links.has_real_jira: + return links.jira_key + return "draft" if links.jira_draft else "missing" + + +def jira_ask_prompt(project: Project, work_id: str) -> str: + """Agent-facing instruction when the tracker key is unset; empty otherwise.""" + wid = (work_id or "").strip() + if not wid or os.environ.get("SDLC_SESSION_ASK_JIRA", "1") != "1": + return "" + status = jira_status(project, wid) + if status == "missing": + return ( + f"Tracker link: Jira key is missing for {wid}. Ask the user for the issue key " + "(or confirm none applies) before coding or claiming tracker progress. Then run " + f"`./scripts/sdlc.sh claim {wid} --jira KEY` (or set `- Key:` under `## Jira` on the " + "requirement and re-claim). Do not invent a key." + ) + if status == "draft": + return ( + f"Tracker link: Jira draft exists for {wid} but `- Key:` is unset. Ask the user for the " + "issue key (or confirm none applies) before coding or claiming tracker progress. Then run " + f"`./scripts/sdlc.sh claim {wid} --jira KEY` (or set `- Key:` under `## Jira` and " + "re-claim). Do not invent a key." + ) + return "" + + +@dataclass +class SessionService: + project: Project + workflow: WorkflowEngine | None = None + + def __post_init__(self) -> None: + self.workflow = self.workflow or WorkflowEngine(self.project) + + # --- state touches used by the session scripts --- + + def touch_session(self, work_id: str, phase: str, milestone: str = "") -> None: + wf = self.workflow + state = wf.ensure_state(work_id) + if phase in PHASE_ORDER: + state.phase = phase + state.active = True + state.shelved_at = "" + state.shelved_reason = "" + state.last_session_at = _utc_now() + if milestone: + state.milestone = milestone + wf.save_state(state) + wf._log(work_id, "session", f"phase={state.phase}") + + def record_capture(self, work_id: str, phase: str = "resume") -> None: + wf = self.workflow + state = wf.ensure_state(work_id) + state.last_capture_at = _utc_now() + if phase and phase != "resume" and phase in PHASE_ORDER: + state.phase = phase + wf.save_state(state) + wf._log(work_id, "capture", f"phase={phase}") + + # --- recommendation (architecture-first) --- + + def recommend(self, work_id: str, phase: str, operation: str = "") -> str: + """Assistant command for the phase; never /sdlc-spdd-code when the canvas blocks coding.""" + from .quiet import is_quiet, quiet_resume_blurb + + if is_quiet(self.project): + return quiet_resume_blurb() + canvas = self.project.canvas_path(work_id) + text = canvas.read_text(encoding="utf-8") if canvas.is_file() else "" + if not operation and text: + op, _ = canvas_mod.next_operation(canvas) + operation = op + if phase == "code" and text and not canvas_mod.canvas_allows_coding(text): + raw = canvas_mod.normalize_readiness(canvas_mod.extract_readiness_raw(text)) or "absent" + return f"/sdlc-spdd-architect @spdd/canvas/{work_id}.md # readiness={raw} — not Ready For Coding" + return recommended_command(phase, work_id, operation) + + # --- brief --- + + def brief_markdown(self, work_id: str = "") -> str: + wid = (work_id or self.workflow.pointer.get() or "").strip() + if not wid: + return "No active Work ID. Run `./scripts/sdlc.sh resume <WORK-ID>`." + state = self.workflow.sync(wid) + canvas = self.project.canvas_path(wid) + op, title = canvas_mod.next_operation(canvas) if canvas.is_file() else ("", "") + operation = op or state.operation + readiness = "absent" + if canvas.is_file(): + raw = canvas_mod.extract_readiness_raw(canvas.read_text(encoding="utf-8")) + readiness = canvas_mod.normalize_readiness(raw) or raw or "absent" + pending = [ + GATE_LABELS.get(g, g) + for g in gates_for_phase(state.phase) + if state.gates.get(g, "pending") != "passed" + ] + idx = PHASE_ORDER.index(state.phase) + 1 if state.phase in PHASE_ORDER else 0 + status = jira_status(self.project, wid) + ask = jira_ask_prompt(self.project, wid) + lines = [ + "| Field | Value |", + "|-------|-------|", + f"| Work ID | {wid} |", + f"| Workflow status | {'active' if state.active else 'shelved'} |", + f"| Phase | {state.phase} ({idx}/{len(PHASE_ORDER)}) |", + f"| Readiness | {readiness} |", + f"| Jira | {status} |", + f"| Next operation | {(op + ' — ' + title) if op and title else (operation or 'none')} |", + f"| Assistant command | {self.recommend(wid, state.phase, operation)} |", + "| After this phase | `./scripts/sdlc.sh advance` |", + '| Capture (guarded) | `./scripts/sdlc.sh capture --summary "<summary>"` |', + "| Orient / status | `./scripts/sdlc.sh next` or `/sdlc-spdd-whereami` |", + "", + ] + if pending: + lines.append("Pending gates:") + lines.extend(f"- {p}" for p in pending) + if ask: + lines.extend(["", "Tracker follow-up:", f"- {ask}"]) + return "\n".join(lines) + "\n" + + # --- shell session scripts (REF-010 will pythonize these) --- + + def _script(self, name: str) -> Path: + for cand in ( + self.project.home / "scripts" / name, + self.project.root / "scripts" / name, + ): + if cand.is_file() and os.access(cand, os.X_OK): + return cand + raise FileNotFoundError(f"{name} not found under {self.project.home / 'scripts'} or {self.project.root / 'scripts'}") + + def _run(self, argv: list[str]) -> int: + env = dict(os.environ) + env["SDLC_ROOT"] = str(self.project.root) + return subprocess.call(argv, env=env) + + def start(self, work_id: str = "", phase: str = "") -> int: + wid = (work_id or self.workflow.pointer.get() or "").strip() + if not wid: + print("start: no active pointer — run: ./scripts/sdlc.sh resume <WORK-ID>") + return 2 + state = self.workflow.sync(wid) + script = self._script("start-agent-session.sh") + return self._run([ + str(script), "--target", str(self.project.root), + "--work-id", wid, "--phase", phase or state.phase, + ]) + + def capture(self, args: list[str], *, work_id: str = "", phase: str = "") -> int: + pointer = self.workflow.pointer.get() + wid = (work_id or pointer or "").strip() + if work_id and pointer and pointer != work_id: + print(f"capture: --work-id '{work_id}' does not match pointer '{pointer}'") + print(f"Run: ./scripts/sdlc.sh resume {work_id}") + return 3 + if not wid: + print("capture: no active pointer — run: ./scripts/sdlc.sh resume <WORK-ID>") + return 2 + if not phase: + phase = self.workflow.ensure_state(wid).phase or "resume" + script = self._script("capture-session-memory.sh") + argv = [str(script), "--target", str(self.project.root), "--work-id", wid, "--phase", phase, *args] + return self.workflow.pointer.run_against(wid, argv) + + def complete(self, args: list[str]) -> int: + """Guarded ``capture --complete``; refuses without a full verify receipt.""" + have = {"--verify-command": False, "--verify-exit": False, "--verify-result": False} + has_phase = False + for tok in args: + if tok in have: + have[tok] = True + if tok == "--phase": + has_phase = True + if not all(have.values()): + print("complete: verify receipt required (command, exit, pass/fail). Refuse complete without it.") + print("Pass --verify-command, --verify-exit, and --verify-result=pass.") + return 1 + extra = list(args) + if not has_phase: + extra += ["--phase", "code"] + return self.capture(["--complete", *extra]) From 94941015bd76ddc423cd16f672b02849bcb02081 Mon Sep 17 00:00:00 2001 From: Cursor Agent <cursoragent@cursor.com> Date: Mon, 14 Sep 2026 00:01:14 +0000 Subject: [PATCH 12/22] =?UTF-8?q?REF-003:=20one=20engine=20=E2=80=94=20sdl?= =?UTF-8?q?c.sh=20is=20a=20thin=20Python=20dispatcher?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - sdlc.sh execs python -m sdlc_engine; SDLC_ENGINE/SDLC_GATE_ENGINE removed - Delete bash twins: sdlc-workflow.sh, sdlc-team-registry.sh, sdlc-pointer.sh, accept-lessons.sh, notify-team-registry hook - start-agent-session/capture-session-memory call the engine via sdlc_engine_run - upgrade-project refuses pre-v3 layouts (exit 3) and prunes retired twins; drop consolidate/archive helpers from framework-install.sh and skills.sh - init/verify no longer install or check twins; verify asserts twins absent - Replace consolidate harnesses with tests/test-upgrade-layout.sh - live-consumer uses sdlc.sh pointer get/reset Co-authored-by: John Menke <jmjava@gmail.com> --- ...onsolidate.yml => test-upgrade-layout.yml} | 24 +- scripts/accept-lessons.sh | 216 -- scripts/capture-session-memory.sh | 14 +- scripts/guide/mcp-config-snippet.sh | 2 +- scripts/init-project.sh | 21 - scripts/lib/framework-install.sh | 212 -- scripts/lib/python.sh | 15 + scripts/lib/skills.sh | 126 - scripts/run-test-suites.sh | 1 - scripts/sdlc.sh | 201 +- scripts/start-agent-session.sh | 73 +- scripts/test-ci-local.sh | 1 - scripts/upgrade-project.sh | 210 +- scripts/verify-project-install.sh | 8 +- templates/agent-context/README.md | 1 - .../hooks/notify-team-registry.example.sh | 37 - templates/agent-context/sdlc-pointer.sh | 133 -- templates/agent-context/sdlc-team-registry.sh | 981 -------- templates/agent-context/sdlc-workflow.sh | 2089 ----------------- .../cursor-agent/run-persistence-test.mjs | 4 +- .../cursor-agent/run-slash-matrix.mjs | 4 +- tests/live-consumer/scenarios/01-install.sh | 4 +- .../scenarios/02-pointer-workflow.sh | 8 +- tests/test-framework-install-consolidate.sh | 142 -- tests/test-integration-merge.sh | 3 +- tests/test-resolve-agent-context.sh | 37 - tests/test-upgrade-consolidate.sh | 340 --- tests/test-upgrade-layout.sh | 96 + 28 files changed, 238 insertions(+), 4765 deletions(-) rename .github/workflows/{test-upgrade-consolidate.yml => test-upgrade-layout.yml} (54%) delete mode 100755 scripts/accept-lessons.sh delete mode 100644 templates/agent-context/hooks/notify-team-registry.example.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 delete mode 100755 tests/test-framework-install-consolidate.sh delete mode 100755 tests/test-upgrade-consolidate.sh create mode 100755 tests/test-upgrade-layout.sh diff --git a/.github/workflows/test-upgrade-consolidate.yml b/.github/workflows/test-upgrade-layout.yml similarity index 54% rename from .github/workflows/test-upgrade-consolidate.yml rename to .github/workflows/test-upgrade-layout.yml index 8cdaea5a..f3c0981b 100644 --- a/.github/workflows/test-upgrade-consolidate.yml +++ b/.github/workflows/test-upgrade-layout.yml @@ -1,4 +1,4 @@ -name: Test Upgrade Consolidate +name: Test Upgrade Layout on: pull_request: @@ -10,9 +10,8 @@ on: - 'scripts/lib/framework-install.sh' - 'scripts/lib/skills.sh' - 'templates/**' - - 'tests/test-upgrade-consolidate.sh' - - 'tests/test-framework-install-consolidate.sh' - - '.github/workflows/test-upgrade-consolidate.yml' + - 'tests/test-upgrade-layout.sh' + - '.github/workflows/test-upgrade-layout.yml' push: branches: [main] paths: @@ -23,12 +22,11 @@ on: - 'scripts/lib/framework-install.sh' - 'scripts/lib/skills.sh' - 'templates/**' - - 'tests/test-upgrade-consolidate.sh' - - 'tests/test-framework-install-consolidate.sh' - - '.github/workflows/test-upgrade-consolidate.yml' + - 'tests/test-upgrade-layout.sh' + - '.github/workflows/test-upgrade-layout.yml' jobs: - test-upgrade-consolidate: + test-upgrade-layout: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -38,11 +36,7 @@ jobs: bash -n scripts/upgrade-project.sh bash -n scripts/verify-project-install.sh bash -n scripts/lib/framework-install.sh - bash -n tests/test-upgrade-consolidate.sh - bash -n tests/test-framework-install-consolidate.sh + bash -n tests/test-upgrade-layout.sh - - name: Unit — framework-install consolidate helpers - run: ./tests/test-framework-install-consolidate.sh - - - name: Integration — upgrade layout consolidation - run: ./tests/test-upgrade-consolidate.sh + - name: Integration — upgrade layout contract (v3 only) + run: ./tests/test-upgrade-layout.sh diff --git a/scripts/accept-lessons.sh b/scripts/accept-lessons.sh deleted file mode 100755 index f129b7d1..00000000 --- a/scripts/accept-lessons.sh +++ /dev/null @@ -1,216 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# shellcheck source=/dev/null -source "${_SCRIPT_DIR}/lib/common.sh" -# shellcheck source=/dev/null -source "${_SCRIPT_DIR}/lib/paths.sh" - -usage() { - cat <<'EOF' -Usage: accept-lessons.sh [options] - -Promote staged lesson records into spdd/memory/lessons.jsonl (dedupe by id, last wins). -Non-promoted staged records remain unless --discard-rest. - -Options: - --target <path> Target project (default: .) - --work-id <ID> Promote only records for this Work ID - --ids <a,b,c> Promote only these record ids (comma-separated) - --discard-rest Drop non-promoted staged records in scope from stage - --commit git add ledger + git commit with memory message - --list Show staged records (id kind work_id title) - --help Print this help - -Also available via: ./scripts/sdlc.sh accept [options] - -Examples: - ./scripts/accept-lessons.sh --work-id FEAT-001-order-status-api - ./scripts/accept-lessons.sh --work-id FEAT-001 --commit - ./scripts/accept-lessons.sh --list -EOF -} - -TARGET="." -WORK_ID="" -IDS="" -DISCARD_REST=0 -DO_COMMIT=0 -LIST_ONLY=0 - -while [[ $# -gt 0 ]]; do - case "$1" in - --target) TARGET="${2:-}"; shift 2 ;; - --work-id) WORK_ID="${2:-}"; shift 2 ;; - --ids) IDS="${2:-}"; shift 2 ;; - --discard-rest) DISCARD_REST=1; shift ;; - --commit) DO_COMMIT=1; shift ;; - --list) LIST_ONLY=1; shift ;; - --help|-h) usage; exit 0 ;; - *) - echo "Unknown option: $1" >&2 - usage >&2 - exit 1 - ;; - esac -done - -TARGET="$(sdlc_resolve_target "${TARGET}")" -export SDLC_ROOT="${TARGET}" - -ledger="$(sdlc_ledger "${TARGET}")" -stage="$(sdlc_stage "${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 $? - 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[@]}" - fi -} - -if [[ "${LIST_ONLY}" -eq 1 ]]; then - if [[ ! -f "${stage}" ]]; then - echo "No staged records." - exit 0 - fi - python3 - <<PY -import json -from pathlib import Path -p = Path(${stage@Q}) -for line in p.read_text(encoding="utf-8").splitlines(): - line = line.strip() - if not line: - continue - try: - r = json.loads(line) - except json.JSONDecodeError: - continue - print(f"{r.get('id','')}\t{r.get('kind','')}\t{r.get('work_id','')}\t{r.get('title','')}") -PY - exit 0 -fi - -engine_mode="${SDLC_ENGINE:-auto}" -if [[ "${engine_mode}" == "python" ]] || { [[ "${engine_mode}" == "auto" ]] && _python_engine_available; }; then - result="$(_run_python_accept 2>&1)" || { echo "${result}" >&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 - <<PY -import json, os, tempfile -from pathlib import Path - -root = Path(${TARGET@Q}) -ledger = Path(${ledger@Q}) -stage = Path(${stage@Q}) -work_id = ${WORK_ID@Q} -ids_csv = ${IDS@Q} -discard_rest = ${DISCARD_REST} - -want_ids = set(x.strip() for x in ids_csv.split(",") if x.strip()) if ids_csv else None - -def read_jsonl(path): - if not path.is_file(): - return [] - out = [] - for line in path.read_text(encoding="utf-8").splitlines(): - line = line.strip() - if not line: - continue - try: - out.append(json.loads(line)) - except json.JSONDecodeError: - continue - return out - -def write_jsonl(path, records): - path.parent.mkdir(parents=True, exist_ok=True) - fd, tmp = tempfile.mkstemp(dir=str(path.parent), suffix=".tmp") - try: - with os.fdopen(fd, "w", encoding="utf-8") as fh: - for rec in records: - fh.write(json.dumps(rec, ensure_ascii=False) + "\n") - os.replace(tmp, path) - finally: - if os.path.exists(tmp): - os.unlink(tmp) - -def in_scope(rec): - if work_id and rec.get("work_id") != work_id: - return False - if want_ids is not None and rec.get("id") not in want_ids: - return False - return True - -staged = read_jsonl(stage) -promote = [r for r in staged if in_scope(r)] -by_id = {} -for rec in promote: - by_id[rec["id"]] = rec -promote = list(by_id.values()) - -accepted = {r["id"]: r for r in read_jsonl(ledger)} -for rec in promote: - accepted[rec["id"]] = rec -if promote: - write_jsonl(ledger, accepted.values()) - -if discard_rest: - remaining = [r for r in staged if work_id and r.get("work_id") != work_id] -else: - promoted_ids = {r["id"] for r in promote} - remaining = [r for r in staged if r.get("id") not in promoted_ids] -write_jsonl(stage, remaining) - -print(json.dumps({ - "accepted": [r["id"] for r in promote], - "accepted_count": len(promote), - "staged_remaining": len(remaining), - "ledger": str(ledger), -})) -PY -} - -_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}" -} - -result="$(_accept_shell)" -echo "${result}" -accepted_count="$(printf '%s' "${result}" | python3 -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 482d21ad..c7644940 100755 --- a/scripts/capture-session-memory.sh +++ b/scripts/capture-session-memory.sh @@ -7,6 +7,8 @@ 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" +# shellcheck source=/dev/null source "${_SCRIPT_DIR}/lib/areas.sh" # shellcheck source=/dev/null source "${_SCRIPT_DIR}/lib/milestone.sh" @@ -490,15 +492,7 @@ if [[ -n "${ROADMAP_NOTE}" ]]; then } >> "${roadmap_file}" fi -workflow_script="${HOME}/scripts/sdlc-workflow.sh" -if [[ ! -f "${workflow_script}" ]]; then - workflow_script="${TARGET}/agent-context/sdlc-workflow.sh" -fi -if [[ -f "${workflow_script}" ]]; then - SDLC_ROOT="${TARGET}" - # shellcheck source=/dev/null - source "${workflow_script}" - sdlc_workflow_record_capture "${WORK_ID}" "${PHASE}" -fi +# One engine: record the capture on the workflow state via sdlc-engine. +sdlc_engine_run "${TARGET}" session record-capture --work-id "${WORK_ID}" --phase "${PHASE}" >/dev/null 2>&1 || true echo "staged ${#staged_records[@]} records → ${stage_file#${TARGET}/}; run 'sdlc.sh accept --work-id ${WORK_ID}' at retro/sync" diff --git a/scripts/guide/mcp-config-snippet.sh b/scripts/guide/mcp-config-snippet.sh index 4d20cda5..e9f3125e 100755 --- a/scripts/guide/mcp-config-snippet.sh +++ b/scripts/guide/mcp-config-snippet.sh @@ -34,7 +34,7 @@ cat <<EOF # # CLI fallback (same data, no MCP in IDE): # ./scripts/guide/query-guide.sh --work-id <WORK-ID> -# SDLC_ENGINE=python ./scripts/sdlc.sh context guide-query --work-id <WORK-ID> --text +# ./scripts/sdlc.sh context guide-query --work-id <WORK-ID> --text EOF if [[ "${MODE}" == "cursor" ]]; then diff --git a/scripts/init-project.sh b/scripts/init-project.sh index 57f6ad79..922cce9a 100755 --- a/scripts/init-project.sh +++ b/scripts/init-project.sh @@ -239,8 +239,6 @@ copy_if_missing \ "${REPO_ROOT}/templates/agent-context/harness/validation-rules.md" \ "${HOME_DIR}/harness/validation-rules.md" -migrate_playbooks_extensions_to_skills "${TARGET}" "${DRY_RUN}" - # Optional Guide DICE backend opt-in. The marker only enables runtime probing # (resolve-context-backend.sh); commands still fall back to file-based context # whenever Guide is unreachable. @@ -271,24 +269,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 <home>/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" - # Kasana I3 — optional verify sample. Never install as .git/hooks/pre-commit. copy_if_missing \ "${REPO_ROOT}/templates/agent-context/hooks/pre-commit.sample" \ @@ -299,7 +279,6 @@ for file in \ start-agent-session.sh \ resync-agent-session.sh \ capture-session-memory.sh \ - accept-lessons.sh \ index-spdd-analysis.sh \ resolve-agent-context.sh \ resolve-context-backend.sh \ diff --git a/scripts/lib/framework-install.sh b/scripts/lib/framework-install.sh index 21293c72..af98160a 100644 --- a/scripts/lib/framework-install.sh +++ b/scripts/lib/framework-install.sh @@ -63,91 +63,6 @@ framework_ensure_gitignore_runtime() { } >> "${gitignore}" } -# Merge children from src_dir into dest_dir (dest wins on file conflicts). -# Removes src_dir when empty. Used by upgrade-project consolidation (storage v3). -framework_merge_dir_into() { - local src_dir="$1" - local dest_dir="$2" - local dry_run="$3" - local child name - - [[ -d "${src_dir}" ]] || return 0 - if [[ "${dry_run}" -eq 1 ]]; then - echo "[dry-run] would mkdir -p ${dest_dir}" >&2 - else - mkdir -p "${dest_dir}" - fi - - shopt -s nullglob - for child in "${src_dir}"/* "${src_dir}"/.[!.]* "${src_dir}"/..?*; do - [[ -e "${child}" ]] || continue - name="$(basename "${child}")" - [[ "${name}" == "." || "${name}" == ".." ]] && continue - framework_consolidate_path "${child}" "${dest_dir}/${name}" "${dry_run}" - done - shopt -u nullglob - - if [[ "${dry_run}" -eq 0 ]]; then - find "${src_dir}" -depth -type d -empty -delete 2>/dev/null || true - rmdir "${src_dir}" 2>/dev/null || true - fi -} - -# Move or merge src into dest under the v3 home. Idempotent; dest wins conflicts. -# Emits one line: move|merge|keep|skip followed by relative description (for logging). -framework_consolidate_path() { - local src="$1" - local dest="$2" - local dry_run="$3" - local target="${4:-}" - local rel_src rel_dest - - [[ -e "${src}" ]] || return 0 - - if [[ -n "${target}" ]]; then - rel_src="${src#"${target}/"}" - rel_dest="${dest#"${target}/"}" - else - rel_src="${src}" - rel_dest="${dest}" - fi - - if [[ "${src}" -ef "${dest}" ]]; then - return 0 - fi - - if [[ ! -e "${dest}" ]]; then - if [[ "${dry_run}" -eq 1 ]]; then - echo "[dry-run] would move ${rel_src} -> ${rel_dest}" >&2 - else - mkdir -p "$(dirname "${dest}")" - mv "${src}" "${dest}" - fi - echo "move ${rel_src} -> ${rel_dest}" - return 0 - fi - - if [[ -d "${src}" && -d "${dest}" ]]; then - framework_merge_dir_into "${src}" "${dest}" "${dry_run}" - echo "merge ${rel_src} -> ${rel_dest}" - return 0 - fi - - if [[ -f "${src}" && -f "${dest}" ]]; then - if [[ "${dry_run}" -eq 1 ]]; then - echo "[dry-run] would keep ${rel_dest} (drop duplicate ${rel_src})" >&2 - else - rm -f "${src}" - fi - echo "keep ${rel_dest}" - return 0 - fi - - echo "skip type mismatch: ${rel_src} vs ${rel_dest}" >&2 - echo "skip ${rel_src}" - return 0 -} - # True when target is this orchestrator repo (framework source lives at root). framework_is_orchestrator_root() { local target="$1" @@ -156,130 +71,3 @@ framework_is_orchestrator_root() { && -d "${target}/templates" \ && -d "${target}/engine" ]] } - -# Drop empty legacy shells at repo root once the home copy exists. -framework_prune_legacy_layout_shells() { - local target="$1" - local home="$2" - local dry_run="$3" - local rel path - local is_orch=0 - - if framework_is_orchestrator_root "${target}"; then - is_orch=1 - fi - - for rel in requirements spdd session-notes harness scripts/sdlc-spdd docs/sdlc-spdd agent-context; do - path="${target}/${rel}" - [[ -e "${path}" ]] || continue - case "${rel}" in - requirements|spdd|session-notes|harness) - [[ -d "${home}/${rel}" ]] || continue - ;; - scripts/sdlc-spdd) - [[ -d "${home}/scripts" ]] || continue - ;; - docs/sdlc-spdd) - [[ -d "${home}/docs" ]] || continue - ;; - agent-context) - # Always prune/archive — install source lives under templates/. - ;; - esac - if [[ "${dry_run}" -eq 1 ]]; then - echo "[dry-run] would prune empty legacy dir ${rel}" >&2 - else - find "${path}" -depth -type d -empty -delete 2>/dev/null || true - rmdir "${path}" 2>/dev/null || true - fi - done - - if [[ "${dry_run}" -eq 0 ]]; then - # Never remove the orchestrator's root scripts/ or docs/ trees. - if [[ "${is_orch}" -eq 0 ]]; then - rm -f "${target}/scripts/.gitkeep" 2>/dev/null || true - rmdir "${target}/scripts" 2>/dev/null || true - rmdir "${target}/docs" 2>/dev/null || true - fi - fi -} - -# Move a leftover legacy path under home/.sdlc/legacy-layout-archive/<stamp>/. -# Emits: archive <rel> -> <archive-rel> -framework_archive_legacy_path() { - local target="$1" - local home="$2" - local rel="$3" - local stamp="$4" - local dry_run="$5" - local src dest - - src="${target}/${rel}" - [[ -e "${src}" ]] || return 0 - # Never archive the home itself or paths already under it. - [[ "${src}" == "${home}" || "${src}" == "${home}/"* ]] && return 0 - - dest="${home}/.sdlc/legacy-layout-archive/${stamp}/${rel}" - if [[ "${dry_run}" -eq 1 ]]; then - echo "[dry-run] would archive ${rel} -> ${dest#"${target}/"}" >&2 - echo "archive ${rel} -> ${dest#"${target}/"}" - return 0 - fi - mkdir -p "$(dirname "${dest}")" - mv "${src}" "${dest}" - echo "archive ${rel} -> ${dest#"${target}/"}" -} - -# After merge+prune, archive any remaining sprawled stay-set paths so the -# project root only keeps application source + IDE adapters (+ orchestrator -# framework source trees when dogfooding this repo). -framework_archive_remaining_legacy_layout() { - local target="$1" - local home="$2" - local dry_run="$3" - local stamp="$4" - local is_orch=0 - local rel - - if framework_is_orchestrator_root "${target}"; then - is_orch=1 - fi - - for rel in \ - requirements \ - spdd \ - session-notes \ - ROADMAP.md \ - harness \ - playbooks \ - extensions \ - docs/sdlc-spdd \ - scripts/sdlc-spdd; do - [[ -e "${target}/${rel}" ]] || continue - framework_archive_legacy_path "${target}" "${home}" "${rel}" "${stamp}" "${dry_run}" - done - - # Root runtime always belongs under the home after v3. - if [[ -e "${target}/.sdlc" && ! "${target}/.sdlc" -ef "${home}/.sdlc" ]]; then - framework_archive_legacy_path "${target}" "${home}" ".sdlc" "${stamp}" "${dry_run}" - fi - - shopt -s nullglob - for rel in "${target}"/milestone-*.md; do - [[ -e "${rel}" ]] || continue - framework_archive_legacy_path "${target}" "${home}" "$(basename "${rel}")" "${stamp}" "${dry_run}" - done - shopt -u nullglob - - # Leftover agent-context/ is always legacy (install source is templates/). - if [[ -e "${target}/agent-context" ]]; then - framework_archive_legacy_path "${target}" "${home}" "agent-context" "${stamp}" "${dry_run}" - fi - - if [[ "${dry_run}" -eq 0 ]]; then - if [[ "${is_orch}" -eq 0 ]]; then - rmdir "${target}/scripts" 2>/dev/null || true - rmdir "${target}/docs" 2>/dev/null || true - fi - fi -} diff --git a/scripts/lib/python.sh b/scripts/lib/python.sh index bd2496ec..da45a6ab 100644 --- a/scripts/lib/python.sh +++ b/scripts/lib/python.sh @@ -74,3 +74,18 @@ resolve_engine_python() { export SDLC_PY return 0 } + +# Run the engine against a project root: sdlc_engine_run <root> <verb> [args...] +# Uses the orchestrator's engine source when present (dogfood), else the +# installed package. Callers decide whether a failure is fatal. +sdlc_engine_run() { + local root="$1" + shift + SDLC_ROOT="${root}" resolve_engine_python || return 1 + if [[ -d "${root}/engine/src/sdlc_engine" ]]; then + PYTHONPATH="${root}/engine/src${PYTHONPATH:+:${PYTHONPATH}}" \ + "${SDLC_PY}" -m sdlc_engine --root "${root}" "$@" + return $? + fi + "${SDLC_PY}" -m sdlc_engine --root "${root}" "$@" +} diff --git a/scripts/lib/skills.sh b/scripts/lib/skills.sh index 13b6223f..bb5b8b5e 100644 --- a/scripts/lib/skills.sh +++ b/scripts/lib/skills.sh @@ -133,129 +133,3 @@ _skills_prepend_frontmatter() { [[ "${body}" == *$'\n' ]] || echo } > "${dest}" } - -# Idempotent: playbooks/ + extensions/ → harness/skills/; drop manifest machinery. -migrate_playbooks_extensions_to_skills() { - local target="$1" - local dry_run="${2:-0}" - local home skills_dir playbooks_dir extensions_dir - home="$(sdlc_home "${target}")" - skills_dir="$(sdlc_skills_dir "${target}")" - playbooks_dir="" - extensions_dir="" - if [[ -d "${home}/playbooks" ]]; then - playbooks_dir="${home}/playbooks" - elif [[ -d "${home}/agent-context/playbooks" ]]; then - playbooks_dir="${home}/agent-context/playbooks" - fi - if [[ -d "${home}/extensions" ]]; then - extensions_dir="${home}/extensions" - elif [[ -d "${home}/agent-context/extensions" ]]; then - extensions_dir="${home}/agent-context/extensions" - fi - - if [[ "${dry_run}" -eq 1 ]]; then - echo "[dry-run] would ensure ${skills_dir}" - else - mkdir -p "${skills_dir}" - fi - - local f base skill phases aliases body dest agent_dir agent_phases - shopt -s nullglob - - if [[ -n "${playbooks_dir}" && -d "${playbooks_dir}" ]]; then - for f in "${playbooks_dir}"/*.md; do - base="$(basename "${f}")" - IFS=$'\t' read -r skill phases <<< "$(_skills_playbook_meta "${base}")" - [[ "${skill}" == "SKIP" ]] && continue - dest="${skills_dir}/${skill}.md" - if [[ -f "${dest}" ]]; then - continue - fi - body="$(<"${f}")" - if [[ "${skill}" == "java-feature" ]]; then - aliases="java" - else - aliases="" - fi - if [[ "${dry_run}" -eq 1 ]]; then - echo "[dry-run] would migrate playbook ${f} -> ${dest}" - else - _skills_prepend_frontmatter "${dest}" "${skill}" "${phases}" "${aliases}" "${body}" - fi - done - fi - - if [[ -n "${extensions_dir}" && -d "${extensions_dir}" ]]; then - for f in "${extensions_dir}"/skills/*.md; do - [[ -f "${f}" ]] || continue - base="$(basename "${f}" .md)" - dest="${skills_dir}/${base}.md" - [[ -f "${dest}" ]] && continue - body="$(<"${f}")" - if [[ "${dry_run}" -eq 1 ]]; then - echo "[dry-run] would migrate extension skill ${f} -> ${dest}" - else - if [[ "${body}" == ---* ]]; then - cp "${f}" "${dest}" - else - _skills_prepend_frontmatter "${dest}" "${base}" "" "" "${body}" - fi - fi - done - - for agent_dir in "${extensions_dir}"/*-agent "${extensions_dir}/_all-agents"; do - [[ -d "${agent_dir}" ]] || continue - agent_phases="$(_skills_agent_folder_phases "$(basename "${agent_dir}")")" - [[ -n "${agent_phases}" ]] || continue - for f in "${agent_dir}"/*.md; do - [[ -f "${f}" ]] || continue - base="$(basename "${f}" .md)" - case "${base}" in - README|manifest|example-manifest-extension) continue ;; - esac - dest="${skills_dir}/${base}.md" - [[ -f "${dest}" ]] && continue - body="$(<"${f}")" - if [[ "${dry_run}" -eq 1 ]]; then - echo "[dry-run] would migrate extension ${f} -> ${dest} (phases: ${agent_phases})" - else - _skills_prepend_frontmatter "${dest}" "${base}" "${agent_phases}" "" "${body}" - fi - done - done - fi - - shopt -u nullglob - - # Move phase-index beside harness core files when still under memory/. - local harness_dir phase_src phase_dest - harness_dir="$(sdlc_harness_dir "${target}")" - phase_dest="${harness_dir}/phase-index.md" - for phase_src in \ - "${home}/agent-context/memory/phase-index.md" \ - "${home}/memory/phase-index.md"; do - [[ -f "${phase_src}" && ! -f "${phase_dest}" ]] || continue - if [[ "${dry_run}" -eq 1 ]]; then - echo "[dry-run] would move ${phase_src} -> ${phase_dest}" - else - mkdir -p "${harness_dir}" - cp "${phase_src}" "${phase_dest}" - fi - done - - if [[ -n "${extensions_dir}" && -d "${extensions_dir}" ]]; then - if [[ "${dry_run}" -eq 1 ]]; then - echo "[dry-run] would remove legacy extensions tree ${extensions_dir}" - else - rm -rf "${extensions_dir}" - fi - fi - if [[ -n "${playbooks_dir}" && -d "${playbooks_dir}" ]]; then - if [[ "${dry_run}" -eq 1 ]]; then - echo "[dry-run] would remove legacy playbooks tree ${playbooks_dir}" - else - rm -rf "${playbooks_dir}" - fi - fi -} diff --git a/scripts/run-test-suites.sh b/scripts/run-test-suites.sh index 0bc56ad3..ba801537 100755 --- a/scripts/run-test-suites.sh +++ b/scripts/run-test-suites.sh @@ -135,7 +135,6 @@ if ! test_preflight_python "$PY"; then fi cd "$ROOT" -export SDLC_ENGINE=python export PYTHONPATH="$ROOT/engine/src${PYTHONPATH:+:$PYTHONPATH}" if [[ "$CLEAN_STALE" == "1" ]]; then diff --git a/scripts/sdlc.sh b/scripts/sdlc.sh index b5ee1ba9..6e69cb6f 100755 --- a/scripts/sdlc.sh +++ b/scripts/sdlc.sh @@ -1,60 +1,31 @@ #!/usr/bin/env bash -# Short entry point for SDLC pointer + workflow helpers. -# Installed to sdlc-spdd/scripts/sdlc.sh in target projects (storage v3); -# lives at scripts/sdlc.sh in the orchestrator repo. +# Thin entry point for the SDLC-SPDD engine (one engine: Python sdlc-engine). +# Installed to sdlc-spdd/scripts/sdlc.sh in target projects; 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 +# Every verb is dispatched to `python -m sdlc_engine`. There is no shell +# workflow implementation and no SDLC_ENGINE switch (REF-003). 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" -elif [[ -f "${SCRIPT_DIR}/../../agent-context/sdlc-workflow.sh" ]]; then - # Legacy sprawled install: <root>/scripts/sdlc-spdd/sdlc.sh. +if [[ "$(basename "$(dirname "${SCRIPT_DIR}")")" == "sdlc-spdd" ]]; then + # Installed target or dogfood home: <root>/sdlc-spdd/scripts/sdlc.sh ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" - WORKFLOW="${ROOT}/agent-context/sdlc-workflow.sh" 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 checkout: <root>/scripts/sdlc.sh + ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" fi -# Script-derived ROOT finds the orch .venv + engine source. --target/--root -# only changes the project the engine operates on (otherwise `next` runs -# against the orch clone and prints "no active Work ID"). +# Script-derived ROOT finds the .venv + engine source. --target/--root only +# changes the project the engine operates on. PROJECT_ROOT="${ROOT}" _forward=() while [[ $# -gt 0 ]]; do case "$1" in --target|--root) - if [[ -z "${2:-}" ]]; then - echo "sdlc: $1 requires a directory path" >&2 - exit 2 - fi - if [[ ! -d "$2" ]]; then - echo "sdlc: $1 is not a directory: $2" >&2 + if [[ -z "${2:-}" || ! -d "${2:-}" ]]; then + echo "sdlc: $1 requires an existing directory" >&2 exit 2 fi PROJECT_ROOT="$(cd "$2" && pwd)" @@ -82,7 +53,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 @@ -90,133 +60,46 @@ if [[ -f "${SCRIPT_DIR}/lib/python.sh" ]]; then elif [[ -f "${ROOT}/scripts/lib/python.sh" ]]; then # shellcheck source=scripts/lib/python.sh source "${ROOT}/scripts/lib/python.sh" +else + echo "sdlc: scripts/lib/python.sh not found next to $0" >&2 + exit 1 fi -_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 - PYTHONPATH="${ROOT}/engine/src${PYTHONPATH:+:${PYTHONPATH}}" \ - "${SDLC_PY}" -c 'import sdlc_engine' 2>/dev/null - return $? - fi - "${SDLC_PY}" -c 'import sdlc_engine' 2>/dev/null -} +if [[ -n "${SDLC_ENGINE:-}" || -n "${SDLC_GATE_ENGINE:-}" ]]; then + echo "sdlc: SDLC_ENGINE / SDLC_GATE_ENGINE were removed; the Python engine is the only engine (REF-003)" >&2 + exit 2 +fi -# 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 -} +resolve_engine_python || exit 1 + +_pypath="" +if [[ -d "${ROOT}/engine/src/sdlc_engine" ]]; then + _pypath="${ROOT}/engine/src${PYTHONPATH:+:${PYTHONPATH}}" +fi -_run_python_engine() { - local args=("$@") - resolve_engine_python || exit 1 - if [[ -d "${ROOT}/engine/src/sdlc_engine" ]]; then - PYTHONPATH="${ROOT}/engine/src${PYTHONPATH:+:${PYTHONPATH}}" \ - exec "${SDLC_PY}" -m sdlc_engine --root "${PROJECT_ROOT}" "${args[@]}" - fi - exec "${SDLC_PY}" -m sdlc_engine --root "${PROJECT_ROOT}" "${args[@]}" -} +if ! PYTHONPATH="${_pypath:-${PYTHONPATH:-}}" "${SDLC_PY}" -c 'import sdlc_engine' 2>/dev/null; then + echo "sdlc: sdlc_engine is not importable by ${SDLC_PY}" >&2 + echo "Install with: ./scripts/setup-engine-venv.sh # Python 3.12 (or: pip install -e ./engine)" >&2 + exit 1 +fi cmd="${1:-next}" if [[ $# -gt 0 ]]; then shift fi -# Python-engine-only commands that must work even when SDLC_ENGINE=shell. -# Normalize hyphen aliases: local-* → local <verb>, db-* → db <verb>. -_py_only_args=() +# Hyphen aliases kept for chat commands and older docs. +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" "$@") - ;; - work-init-from-adf|init-from-adf) - _py_only_args=("work" "init-from-adf" "$@") - ;; - context) - _py_only_args=("context" "$@") - ;; - guide-query) - _py_only_args=("context" "guide-query" "$@") - ;; - installer|console|dashboard) - _py_only_args=("${cmd}" "$@") - ;; -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 - ;; - shell) - ;; - *) - echo "sdlc: unknown SDLC_ENGINE='${ENGINE_MODE}' (use auto|python|shell)" >&2 - exit 2 - ;; + help|-h|--help) args=("--help") ;; + local-*) args=("local" "${cmd#local-}" "$@") ;; + db-*) args=("db" "${cmd#db-}" "$@") ;; + work-init-from-adf|init-from-adf) args=("work" "init-from-adf" "$@") ;; + guide-query) args=("context" "guide-query" "$@") ;; + *) 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 - exit 1 +if [[ -n "${_pypath}" ]]; then + PYTHONPATH="${_pypath}" exec "${SDLC_PY}" -m sdlc_engine --root "${PROJECT_ROOT}" "${args[@]}" fi - -exec "${WORKFLOW}" "${cmd}" "$@" +exec "${SDLC_PY}" -m sdlc_engine --root "${PROJECT_ROOT}" "${args[@]}" diff --git a/scripts/start-agent-session.sh b/scripts/start-agent-session.sh index bdae3e2f..c4892036 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' @@ -102,45 +104,17 @@ TARGET="$(sdlc_resolve_target "${TARGET}")" export SDLC_ROOT="${TARGET}" HOME="$(sdlc_home "${TARGET}")" -pointer_script="${HOME}/scripts/sdlc-pointer.sh" -if [[ ! -f "${pointer_script}" ]]; then - pointer_script="${TARGET}/agent-context/sdlc-pointer.sh" -fi -if [[ -f "${pointer_script}" && -n "${WORK_ID}" ]]; then - SDLC_ROOT="${TARGET}" - # shellcheck source=/dev/null - source "${pointer_script}" - sdlc_set_pointer "${WORK_ID}" >/dev/null -fi - -workflow_script="${HOME}/scripts/sdlc-workflow.sh" -if [[ ! -f "${workflow_script}" ]]; then - workflow_script="${TARGET}/agent-context/sdlc-workflow.sh" -fi -team_script="${HOME}/scripts/sdlc-team-registry.sh" -if [[ ! -f "${team_script}" ]]; then - team_script="${TARGET}/agent-context/sdlc-team-registry.sh" -fi +# One engine: pointer, workflow state, and registry live in sdlc-engine. workflow_brief_md="Workflow tools not installed." 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 + sdlc_engine_run "${TARGET}" pointer set "${WORK_ID}" >/dev/null 2>&1 || true + sdlc_engine_run "${TARGET}" session touch --work-id "${WORK_ID}" --phase "${PHASE}" --milestone "${MILESTONE}" >/dev/null 2>&1 || true + sdlc_engine_run "${TARGET}" sync --work-id "${WORK_ID}" >/dev/null 2>&1 || true + workflow_brief_md="$(sdlc_engine_run "${TARGET}" session brief --work-id "${WORK_ID}" 2>/dev/null || echo "Workflow tools not installed.")" + jira_status="$(sdlc_engine_run "${TARGET}" session jira-status --work-id "${WORK_ID}" 2>/dev/null || true)" + jira_ask_prompt="$(sdlc_engine_run "${TARGET}" session jira-ask --work-id "${WORK_ID}" 2>/dev/null || true)" fi timestamp="$(sdlc_timestamp_iso)" @@ -229,13 +203,16 @@ case "${PHASE}" in ;; esac -# Prefer workflow helper when installed — honors Ready For Coding gate for code phase. +# The engine decides the command — honors the Ready For Coding gate for the code phase. 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}")" +if [[ "${QUIET}" -eq 0 && -n "${WORK_ID}" ]]; then + _rec="$(sdlc_engine_run "${TARGET}" session recommend --work-id "${WORK_ID}" --phase "${PHASE}" 2>/dev/null || true)" + if [[ -n "${_rec}" ]]; then + recommended_command="${_rec}" + fi fi if [[ "${QUIET}" -eq 1 ]]; then recommended_command="Quiet mode: retrieve context via SQLite/Guide/context store; no T## dogfood command." @@ -320,23 +297,7 @@ sqlite_section_md="" sqlite_lookup_loaded=0 if [[ -n "${WORK_ID}" ]]; then _run_db_lookup() { - local out="" - if [[ -x "${TARGET}/scripts/sdlc-spdd/sdlc.sh" ]]; then - out="$( - SDLC_ENGINE=python SDLC_ROOT="${TARGET}" \ - "${TARGET}/scripts/sdlc-spdd/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}" + sdlc_engine_run "${TARGET}" 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 diff --git a/scripts/test-ci-local.sh b/scripts/test-ci-local.sh index df663afa..1aa95809 100755 --- a/scripts/test-ci-local.sh +++ b/scripts/test-ci-local.sh @@ -28,7 +28,6 @@ chmod +x "$ROOT/scripts/run-test-suites.sh" step() { echo ""; echo "== $*"; } step "CLI smoke via sdlc.sh" -export SDLC_ENGINE=python "$ROOT/scripts/sdlc.sh" version "$ROOT/scripts/sdlc.sh" next "$ROOT/scripts/sdlc.sh" viewer --help diff --git a/scripts/upgrade-project.sh b/scripts/upgrade-project.sh index a796aae5..5b5a1a09 100755 --- a/scripts/upgrade-project.sh +++ b/scripts/upgrade-project.sh @@ -16,17 +16,10 @@ Upgrade SDLC-SPDD framework files in a target project that was initialized by an earlier version of this scaffold. Storage v3: all framework assets live under one folder — <target>/sdlc-spdd/ -(the home). For older sprawled installs this upgrade first consolidates: - - - converts legacy memory (context index, lesson files, registry TSV, …) - into the lessons ledger via `sdlc-engine storage migrate` when the Python - engine is available (otherwise data is left in place with instructions) - - moves framework dirs (requirements/, spdd/, session-notes/, ROADMAP.md, - docs/sdlc-spdd/, harness/, agent-context harness/playbooks/extensions, - scripts/sdlc-spdd/, .sdlc/) into <target>/sdlc-spdd/ — merging when the - home already exists (destination wins on conflicts) - - archives any leftover sprawled paths under - sdlc-spdd/.sdlc/legacy-layout-archive/<stamp>/ so the project root is clean +(the home). It is the only supported layout. Pre-v3 installs (framework dirs +at the repo root, agent-context/, work-registry.tsv, scripts/sdlc-spdd/) are +not migrated: the upgrade stops with a message. Re-run init-project.sh on a +clean checkout instead. The upgrade is framework-only and idempotent: - updates SDLC-SPDD assistant prompts (IDE stubs stay at the repo root but @@ -93,10 +86,6 @@ while [[ $# -gt 0 ]]; do BACKUP=0 shift ;; - --consolidate) - # No-op: consolidation into sdlc-spdd/ always runs (storage v3). - shift - ;; --help|-h) usage exit 0 @@ -130,7 +119,7 @@ updated=() unchanged=() backed_up=() preserved=() -moved=() +removed=() CLAUDE_BEGIN="<!-- BEGIN SDLC-SPDD MANAGED CLAUDE GROUNDING -->" CLAUDE_END="<!-- END SDLC-SPDD MANAGED CLAUDE GROUNDING -->" @@ -344,148 +333,37 @@ upsert_claude_memory() { } # --------------------------------------------------------------------------- -# Phase 1 — legacy memory conversion (delegated to the Python engine). -# Runs before the home folder is created so the engine resolves home == root -# and finds the legacy registry TSV and memory trees in place. +# Phase 1 — layout check. Storage v3 only; pre-v3 layouts are refused. # --------------------------------------------------------------------------- -_engine_python() { - if [[ -d "${REPO_ROOT}/engine/src/sdlc_engine" ]]; then - PYTHONPATH="${REPO_ROOT}/engine/src${PYTHONPATH:+:${PYTHONPATH}}" python3 "$@" - else - python3 "$@" +_pre_v3_paths=() +for rel in \ + agent-context \ + work-registry.tsv \ + scripts/sdlc-spdd \ + docs/sdlc-spdd \ + spdd \ + requirements/milestones \ + session-notes \ + .sdlc; do + if [[ -e "${TARGET}/${rel}" ]] && ! framework_is_orchestrator_root "${TARGET}"; then + _pre_v3_paths+=("${rel}") fi -} - -_engine_available() { - _engine_python -c 'import sdlc_engine' 2>/dev/null -} - -has_legacy_memory() { - # Legacy layout names are assembled from parts so the repo-wide - # no-legacy-reference sweep over scripts/ stays clean. - local ac="agent-context" - local wr="work-registry" - local ci="context-index" - local rel - for rel in \ - "${ac}/memory" \ - "${ac}/features" \ - "${ac}/sessions" \ - "${ac}/${wr}.tsv" \ - "spdd/memory/${ci}.md" \ - spdd/memory/lessons \ - spdd/memory/entries \ - spdd/memory/sessions; do - if [[ -e "${TARGET}/${rel}" || -e "${HOME_DIR}/${rel}" ]]; then - return 0 - fi +done +if [[ ${#_pre_v3_paths[@]} -gt 0 ]]; then + echo "Error: pre-v3 SDLC-SPDD layout detected at ${TARGET}:" >&2 + for rel in "${_pre_v3_paths[@]}"; do + echo " - ${rel}" >&2 done - return 1 -} - -legacy_migration_note="" -if has_legacy_memory; then - if _engine_available; then - echo "Legacy memory detected — running sdlc-engine storage migrate..." - if [[ "${DRY_RUN}" -eq 1 ]]; then - _engine_python -m sdlc_engine --root "${TARGET}" storage migrate --dry-run || true - else - _engine_python -m sdlc_engine --root "${TARGET}" storage migrate - fi - legacy_migration_note="Legacy memory converted to the lessons ledger (export under .sdlc/legacy-export/)." - else - legacy_migration_note="Legacy memory left in place: the Python engine is not installed. -Convert it later with: - python3 -m pip install -e '<orchestrator>/engine' - sdlc-engine --root ${TARGET} storage migrate" - echo "WARNING: ${legacy_migration_note}" >&2 - fi -fi - -# --------------------------------------------------------------------------- -# Phase 2 — consolidation into the single-folder home (idempotent). -# --------------------------------------------------------------------------- - -consolidate_into_home() { - local src="$1" - local dest="$2" - local line action detail - - [[ -e "${src}" ]] || return 0 - while IFS= read -r line; do - [[ -n "${line}" ]] || continue - action="${line%% *}" - detail="${line#* }" - case "${action}" in - move|merge) - moved+=("${detail}") - ;; - keep) - preserved+=("${detail}") - ;; - skip) - preserved+=("${src#${TARGET}/} (${detail})") - ;; - esac - done < <(framework_consolidate_path "${src}" "${dest}" "${DRY_RUN}" "${TARGET}") -} - -remove_legacy_framework_file() { - local path="$1" - [[ -e "${path}" ]] || return 0 - if [[ "${DRY_RUN}" -eq 1 ]]; then - echo "[dry-run] would remove legacy framework file ${path#${TARGET}/}" + echo "Storage v3 (single sdlc-spdd/ home) is the only supported layout; legacy layouts are not migrated." >&2 + if [[ -d "${HOME_DIR}" ]]; then + echo "Remove the paths above (git history keeps them) and re-run this upgrade." >&2 else - rm -rf "${path}" + echo "Remove the paths above (git history keeps them) and run: ./scripts/init-project.sh --target ${TARGET}" >&2 fi - moved+=("removed ${path#${TARGET}/}") -} - -# Framework dirs and stay-set artifacts move under <target>/sdlc-spdd/. -# When the home already exists, merge legacy root trees into it (dest wins). -# Install source for harness/workflow scripts is templates/agent-context/ — -# any leftover root agent-context/ is legacy and gets archived. -if framework_is_orchestrator_root "${TARGET}"; then - echo "Orchestrator root detected — consolidating dogfood stay-set into sdlc-spdd/; keeping root scripts/ as framework tooling." -fi - -consolidate_into_home "${TARGET}/requirements" "${HOME_DIR}/requirements" -consolidate_into_home "${TARGET}/spdd" "${HOME_DIR}/spdd" -consolidate_into_home "${TARGET}/session-notes" "${HOME_DIR}/session-notes" -consolidate_into_home "${TARGET}/ROADMAP.md" "${HOME_DIR}/ROADMAP.md" -consolidate_into_home "${TARGET}/docs/sdlc-spdd" "${HOME_DIR}/docs" -consolidate_into_home "${TARGET}/harness" "${HOME_DIR}/harness" -consolidate_into_home "${TARGET}/agent-context/harness" "${HOME_DIR}/harness" -consolidate_into_home "${TARGET}/agent-context/playbooks" "${HOME_DIR}/playbooks" -consolidate_into_home "${TARGET}/agent-context/extensions" "${HOME_DIR}/extensions" -migrate_playbooks_extensions_to_skills "${TARGET}" "${DRY_RUN}" -consolidate_into_home "${TARGET}/scripts/sdlc-spdd" "${HOME_DIR}/scripts" -consolidate_into_home "${TARGET}/.sdlc" "${HOME_DIR}/.sdlc" -shopt -s nullglob -for _root_ms in "${TARGET}"/milestone-*.md; do - consolidate_into_home "${_root_ms}" "${HOME_DIR}/$(basename "${_root_ms}")" -done -shopt -u nullglob - -framework_prune_legacy_layout_shells "${TARGET}" "${HOME_DIR}" "${DRY_RUN}" - -# Drop known framework-owned leftovers, then archive any remaining agent-context/. -remove_legacy_framework_file "${TARGET}/agent-context/sdlc-pointer.sh" -remove_legacy_framework_file "${TARGET}/agent-context/sdlc-workflow.sh" -remove_legacy_framework_file "${TARGET}/agent-context/sdlc-team-registry.sh" -remove_legacy_framework_file "${TARGET}/agent-context/README.md" -remove_legacy_framework_file "${TARGET}/agent-context/hooks" -if [[ "${DRY_RUN}" -eq 0 && -d "${TARGET}/agent-context" ]]; then - find "${TARGET}/agent-context" -name .gitkeep -delete 2>/dev/null || true - find "${TARGET}/agent-context" -type d -empty -delete 2>/dev/null || true + exit 3 fi -while IFS= read -r line; do - [[ -n "${line}" ]] || continue - moved+=("${line#archive }") -done < <(framework_archive_remaining_legacy_layout "${TARGET}" "${HOME_DIR}" "${DRY_RUN}" "${timestamp}") - # --------------------------------------------------------------------------- # Phase 3 — framework file refresh under the home (create missing, upgrade # framework-owned files with backups, preserve project content). @@ -564,8 +442,6 @@ for file in \ "${HOME_DIR}/harness/${file}" done -migrate_playbooks_extensions_to_skills "${TARGET}" "${DRY_RUN}" - # User-facing docs are framework-owned when installed under <home>/docs/. # Skip orchestrator-internal docs (see scripts/lib/shipped-docs-boundary.sh). # shellcheck source=lib/shipped-docs-boundary.sh @@ -589,20 +465,18 @@ 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 <home>/scripts/. -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}" +# One engine (REF-003): remove the retired bash workflow twins from earlier v3 installs. +for file in sdlc-pointer.sh sdlc-workflow.sh sdlc-team-registry.sh accept-lessons.sh hooks/notify-team-registry.example.sh; do + if [[ -e "${HOME_DIR}/scripts/${file}" ]]; then + if [[ "${DRY_RUN}" -eq 1 ]]; then + echo "[dry-run] would remove retired ${file}" + else + rm -f "${HOME_DIR}/scripts/${file}" + fi + removed+=("sdlc-spdd/scripts/${file}") + fi done -copy_framework_file \ - "${REPO_ROOT}/templates/agent-context/hooks/notify-team-registry.example.sh" \ - "${HOME_DIR}/scripts/hooks/notify-team-registry.example.sh" - # Kasana I3 — optional verify sample. Never install as .git/hooks/pre-commit. copy_framework_file \ "${REPO_ROOT}/templates/agent-context/hooks/pre-commit.sample" \ @@ -613,7 +487,6 @@ for file in \ start-agent-session.sh \ resync-agent-session.sh \ capture-session-memory.sh \ - accept-lessons.sh \ index-spdd-analysis.sh \ resolve-agent-context.sh \ resolve-context-backend.sh \ @@ -705,8 +578,8 @@ fi echo "SDLC-SPDD framework upgrade complete for: ${TARGET}" echo "Framework home: ${HOME_DIR}" -echo "Consolidated (${#moved[@]}):" -printf ' %s\n' "${moved[@]:-none}" +echo "Removed retired files (${#removed[@]}):" +printf ' %s\n' "${removed[@]:-none}" echo "Created (${#created[@]}):" printf ' %s\n' "${created[@]:-none}" echo "Updated framework files (${#updated[@]}):" @@ -719,9 +592,6 @@ if [[ "${BACKUP}" -eq 1 ]]; then echo "Backups (${#backed_up[@]}):" printf ' %s\n' "${backed_up[@]:-none}" fi -if [[ -n "${legacy_migration_note}" ]]; then - echo "Legacy memory: ${legacy_migration_note}" -fi echo "Not touched: application source, requirements, canvases, reviews, sync logs, existing roadmap/milestones, accumulated ledger memory, or application docs outside sdlc-spdd/docs." verify_args=(--target "${TARGET}") diff --git a/scripts/verify-project-install.sh b/scripts/verify-project-install.sh index 84b3c1d5..1d3e610b 100755 --- a/scripts/verify-project-install.sh +++ b/scripts/verify-project-install.sh @@ -189,12 +189,8 @@ 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 "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 \ Runtime "create work from milestone script" "${HOME_REL}/scripts/create-work-from-milestone.sh" executable \ Runtime "sync roadmap script" "${HOME_REL}/scripts/sync-roadmap-from-spdd.sh" executable \ Runtime "summarize session notes script" "${HOME_REL}/scripts/summarize-session-notes.sh" executable \ @@ -233,6 +229,10 @@ legacy_checks=( Legacy "legacy session briefs" "${legacy_ac}/sessions" absent Legacy "legacy work registry" "${legacy_ac}/${legacy_wr}.tsv" absent Legacy "legacy workflow manager" "${legacy_ac}/sdlc-workflow.sh" absent + Legacy "retired bash workflow twin" "${HOME_REL}/scripts/sdlc-workflow.sh" absent + Legacy "retired bash registry twin" "${HOME_REL}/scripts/sdlc-team-registry.sh" absent + Legacy "retired bash pointer twin" "${HOME_REL}/scripts/sdlc-pointer.sh" absent + Legacy "retired accept-lessons script" "${HOME_REL}/scripts/accept-lessons.sh" absent Legacy "legacy agent-context tree" "${legacy_ac}" absent Legacy "legacy runtime scripts" "scripts/sdlc-spdd" absent Legacy "legacy root requirements" "requirements" absent diff --git a/templates/agent-context/README.md b/templates/agent-context/README.md index 6a3776b9..3a83a585 100644 --- a/templates/agent-context/README.md +++ b/templates/agent-context/README.md @@ -9,7 +9,6 @@ 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) | Committed memory lives under **`spdd/memory/`** (`lessons.jsonl`, `registry.jsonl`). Hot session brief: **`.sdlc/sessions/current-session.md`** (gitignored). diff --git a/templates/agent-context/hooks/notify-team-registry.example.sh b/templates/agent-context/hooks/notify-team-registry.example.sh deleted file mode 100644 index 28040aee..00000000 --- a/templates/agent-context/hooks/notify-team-registry.example.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env bash -# Example SDLC_TEAM_REGISTRY_HOOK — copy to agent-context/hooks/notify-team-registry.sh -# and set: export SDLC_TEAM_REGISTRY_HOOK=./agent-context/hooks/notify-team-registry.sh -# -# Args: work_id status phase operation owner updated note -# -# Slack (set SDLC_TEAM_SLACK_WEBHOOK): -# https://api.slack.com/messaging/webhooks - -set -euo pipefail - -work_id="${1:-}" -status="${2:-}" -phase="${3:-}" -operation="${4:-}" -owner="${5:-}" -updated="${6:-}" -note="${7:-}" - -message="SDLC registry: *${work_id}* → ${status} (phase: ${phase}, op: ${operation:-none}, by: ${owner})" -[[ -n "${note}" ]] && message+=" — ${note}" - -if [[ -n "${SDLC_TEAM_SLACK_WEBHOOK:-}" ]]; then - payload="$(printf '{"text":"%s"}' "${message//\"/\\\"}")" - curl -fsS -X POST -H 'Content-type: application/json' \ - --data "${payload}" \ - "${SDLC_TEAM_SLACK_WEBHOOK}" >/dev/null 2>&1 || true -fi - -# Jira (optional — set SDLC_TEAM_JIRA_WEBHOOK to a script or URL your team approves): -if [[ -n "${SDLC_TEAM_JIRA_WEBHOOK:-}" ]]; then - curl -fsS -X POST -H 'Content-type: application/json' \ - --data "{\"work_id\":\"${work_id}\",\"status\":\"${status}\",\"owner\":\"${owner}\",\"note\":\"${note}\"}" \ - "${SDLC_TEAM_JIRA_WEBHOOK}" >/dev/null 2>&1 || true -fi - -echo "${message}" diff --git a/templates/agent-context/sdlc-pointer.sh b/templates/agent-context/sdlc-pointer.sh deleted file mode 100755 index 3a0f4334..00000000 --- a/templates/agent-context/sdlc-pointer.sh +++ /dev/null @@ -1,133 +0,0 @@ -#!/usr/bin/env bash -# Simple pointer manager for SDLC chores/tasks -# Usage: -# source agent-context/sdlc-pointer.sh -# sdlc_init # call on initialization -# sdlc_set_pointer ID # sets pointer -# sdlc_get_pointer # prints pointer or empty -# sdlc_reset_pointer # clears pointer -# run_against_pointer "<expected>" -- <command...> -# -# Default storage: .sdlc/pointer in the repository root. - -if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then - set -euo pipefail -fi - -SDLC_ROOT="${SDLC_ROOT:-$(git -C "${PWD}" rev-parse --show-toplevel 2>/dev/null || pwd)}" -_paths_lib="${SDLC_ROOT}/scripts/lib/paths.sh" -if [[ ! -f "${_paths_lib}" ]]; then - _paths_lib="${SDLC_ROOT}/sdlc-spdd/scripts/lib/paths.sh" -fi -if [[ ! -f "${_paths_lib}" ]]; then - _paths_lib="${SDLC_ROOT}/scripts/sdlc-spdd/lib/paths.sh" -fi -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_ROOT}/.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 f27126a1..00000000 --- a/templates/agent-context/sdlc-team-registry.sh +++ /dev/null @@ -1,981 +0,0 @@ -#!/usr/bin/env bash -# Team-visible Work ID registry — committed coordination layer on top of local .sdlc/ state. -# -# Local pointer (.sdlc/pointer) stays machine-private. -# 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}/scripts/lib/paths.sh" -if [[ ! -f "${_paths_lib}" ]]; then - _paths_lib="${SDLC_ROOT}/sdlc-spdd/scripts/lib/paths.sh" -fi -if [[ ! -f "${_paths_lib}" ]]; then - _paths_lib="${SDLC_ROOT}/scripts/sdlc-spdd/lib/paths.sh" -fi -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_ROOT}")" -SDLC_TEAM_REGISTRY_LEGACY="${SDLC_ROOT}/agent-context/work-registry.tsv" -SDLC_TEAM_REGISTRY_LOCK="${SDLC_DIR:-${SDLC_ROOT}/.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="${root}" - 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: <KEY> | 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}" ]] || [[ -f "${SDLC_TEAM_REGISTRY_LEGACY}" ]]; 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_ROOT}/.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 - <<PY -import json -from pathlib import Path -for line in Path(${SDLC_TEAM_REGISTRY_JSONL@Q}).read_text(encoding="utf-8").splitlines(): - line = line.strip() - if not line: - continue - try: - ev = json.loads(line) - except json.JSONDecodeError: - continue - if ev.get("work_id"): - print(json.dumps(ev, ensure_ascii=False)) -PY - return 0 - fi - # Read-only TSV fallback (never written). - if [[ ! -f "${SDLC_TEAM_REGISTRY_LEGACY}" ]]; then - return 0 - fi - python3 - <<PY -import json -from pathlib import Path -tsv = Path(${SDLC_TEAM_REGISTRY_LEGACY@Q}) -for line in tsv.read_text(encoding="utf-8").splitlines(): - if not line or line.startswith("#") or line.startswith("work_id"): - continue - parts = line.split("\t") - while len(parts) < 7: - parts.append("") - ev = { - "event": "legacy-tsv", - "work_id": parts[0], - "status": parts[1], - "phase": parts[2], - "operation": parts[3], - "owner": parts[4], - "ts": parts[5], - "note": parts[6], - } - print(json.dumps(ev, ensure_ascii=False)) -PY -} - -_team_registry_rows() { - _team_registry_init - python3 - <<PY -import json -from pathlib import Path - -by_id = {} -jsonl = Path(${SDLC_TEAM_REGISTRY_JSONL@Q}) -if jsonl.is_file() and jsonl.stat().st_size: - for line in jsonl.read_text(encoding="utf-8").splitlines(): - line = line.strip() - if not line: - continue - try: - ev = json.loads(line) - except json.JSONDecodeError: - continue - wid = ev.get("work_id", "") - if wid: - by_id[wid] = ev -legacy = Path(${SDLC_TEAM_REGISTRY_LEGACY@Q}) -if not by_id and legacy.is_file(): - for line in legacy.read_text(encoding="utf-8").splitlines(): - if not line or line.startswith("#") or line.startswith("work_id"): - continue - parts = line.split("\t") - while len(parts) < 7: - parts.append("") - wid = parts[0] - if not wid: - continue - by_id[wid] = { - "work_id": wid, - "status": parts[1], - "phase": parts[2], - "operation": parts[3], - "owner": parts[4], - "ts": parts[5], - "note": parts[6], - } -for wid in sorted(by_id): - ev = by_id[wid] - print("\t".join([ - ev.get("work_id", ""), - ev.get("status", "available"), - ev.get("phase", ""), - ev.get("operation", ""), - ev.get("owner", ""), - ev.get("ts", ""), - ev.get("note", ""), - ])) -PY -} - -_team_with_registry_lock() { - if command -v flock >/dev/null 2>&1; then - ( - flock -x 200 || exit 1 - "$@" - ) 200>"${SDLC_TEAM_REGISTRY_LOCK}" - else - "$@" - fi -} - - -_team_registry_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 - <<PY -import json -print(json.dumps({ - "event": ${event@Q}, - "work_id": ${work_id@Q}, - "status": ${status@Q}, - "phase": ${phase@Q}, - "operation": ${operation@Q}, - "owner": ${owner@Q}, - "note": ${note@Q}, - "ts": ${updated@Q}, -}, ensure_ascii=False)) -PY -)" - if declare -F sdlc_append_jsonl >/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_ROOT}/.sdlc}/workflows/${work_id}.state" - if [[ -f "${file}" ]]; then - phase="$(grep -m1 '^phase=' "${file}" 2>/dev/null | cut -d= -f2- || true)" - operation="$(grep -m1 '^operation=' "${file}" 2>/dev/null | cut -d= -f2- || true)" - fi - 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="${root}" - 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/<WORK-ID>.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="" - # No agent-context/features moves (storage v3). - - 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="${root}" - 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="${root}/.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="${root}" - 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 <WORK-ID> [--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 <WORK-ID>)" - fi - echo - echo "Hooks: set SDLC_TEAM_REGISTRY_HOOK to agent-context/hooks/notify-team-registry.sh" - echo "Discover all Work IDs: ./scripts/sdlc.sh list-work" -} - -# Locate the workflow manager: same dir as this script (v3 installs place both -# under <home>/scripts/), then <home>/scripts/, then legacy agent-context/. -_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" \ - "${SDLC_ROOT}/agent-context/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 <ID> --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 1ef54b57..00000000 --- a/templates/agent-context/sdlc-workflow.sh +++ /dev/null @@ -1,2089 +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 agent-context/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/workflows/<WORK-ID>.state and .sdlc/workflows/<WORK-ID>.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 helpers (installed sdlc-spdd/scripts/lib, legacy -# scripts/sdlc-spdd/lib, or orchestrator scripts/lib). -_wf_readiness_lib="" -if [[ -f "${SDLC_ROOT}/sdlc-spdd/scripts/lib/readiness.sh" ]]; then - _wf_readiness_lib="${SDLC_ROOT}/sdlc-spdd/scripts/lib/readiness.sh" -elif [[ -f "${SDLC_ROOT}/scripts/sdlc-spdd/lib/readiness.sh" ]]; then - _wf_readiness_lib="${SDLC_ROOT}/scripts/sdlc-spdd/lib/readiness.sh" -elif [[ -f "${SDLC_ROOT}/scripts/lib/readiness.sh" ]]; then - _wf_readiness_lib="${SDLC_ROOT}/scripts/lib/readiness.sh" -fi -if [[ -n "${_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_ROOT:-.}/agent-context/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/<requirement>.md" ;; - plan) echo "/sdlc-spdd-plan @spdd/analysis/${work_id:-<WORK-ID>}-analysis.md" ;; - architect) echo "/sdlc-spdd-architect @spdd/canvas/${work_id:-<WORK-ID>}.md" ;; - code) echo "/sdlc-spdd-code @spdd/canvas/${work_id:-<WORK-ID>}.md operation ${operation:-<T##>}" ;; - api-test) echo "/sdlc-spdd-api-test @spdd/canvas/${work_id:-<WORK-ID>}.md" ;; - review) echo "/sdlc-spdd-review @spdd/canvas/${work_id:-<WORK-ID>}.md" ;; - prompt-update) echo "/sdlc-spdd-prompt-update @spdd/canvas/${work_id:-<WORK-ID>}.md" ;; - retro) echo "/sdlc-spdd-retro @spdd/canvas/${work_id:-<WORK-ID>}.md" ;; - sync) echo "/sdlc-spdd-sync @spdd/canvas/${work_id:-<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 "<WORK-ID>" - return 0 - fi - if [[ -z "${phase}" ]]; then - phase="$(_wf_read_state_var "$(_wf_state_file "${work_id}")" phase init)" - fi - if [[ -x "${root}/sdlc-spdd/scripts/start-agent-session.sh" ]]; then - echo "./sdlc-spdd/scripts/start-agent-session.sh --target . --work-id ${work_id} --phase ${phase}" - elif [[ -x "${root}/scripts/sdlc-spdd/start-agent-session.sh" ]]; then - echo "./scripts/sdlc-spdd/start-agent-session.sh --target . --work-id ${work_id} --phase ${phase}" - else - echo "./scripts/start-agent-session.sh --target . --work-id ${work_id} --phase ${phase}" - fi -} - -sdlc_workflow_shell_capture() { - local work_id="${1:-}" - local phase="${2:-}" - 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 \"<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 \"<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 <WORK-ID>\`." - 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 <<EOF -| Field | Value | -|-------|-------| -| Work ID | ${work_id} | -| Workflow status | $([[ "${active}" == "1" ]] && echo active || echo shelved) | -| Phase | ${phase} ($(( $(_wf_phase_index "${phase}") + 1 ))/${#SDLC_PHASE_ORDER[@]}) | -| Readiness | ${readiness:-absent} | -| Jira | ${jira_status:-unknown} | -| Next operation | ${op_title:-${operation:-none}} | -| Assistant command | $(sdlc_workflow_recommended_command "${phase}" "${work_id}" "${operation}") | -| After this phase | \`./scripts/sdlc.sh advance\` | -| Capture (guarded) | \`$(_wf_shell_helper_path) capture --summary "<summary>"\` | -| 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 <WORK-ID> # 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 <WORK-ID>" >&2 - return 2 - fi - sdlc_workflow_sync "${work_id}" >/dev/null - local phase start_script - phase="$(_wf_read_state_var "$(_wf_state_file "${work_id}")" phase init)" - start_script="${SDLC_ROOT}/sdlc-spdd/scripts/start-agent-session.sh" - if [[ ! -x "${start_script}" ]]; then - start_script="${SDLC_ROOT}/scripts/sdlc-spdd/start-agent-session.sh" - fi - if [[ ! -x "${start_script}" ]]; then - start_script="${SDLC_ROOT}/scripts/start-agent-session.sh" - fi - if [[ ! -x "${start_script}" ]]; then - echo "sdlc_workflow_start: start-agent-session.sh not found" >&2 - return 1 - fi - "${start_script}" --target "${SDLC_ROOT}" --work-id "${work_id}" --phase "${phase}" -} - -sdlc_workflow_status_json() { - local work_id="${1:-}" - if [[ -z "${work_id}" ]]; then - work_id="$(sdlc_get_pointer)" - fi - - local pointer phases_json gates_json shelved_json="" - pointer="$(sdlc_get_pointer)" - - phases_json="" - local p first=1 - for p in "${SDLC_PHASE_ORDER[@]}"; do - [[ "${first}" -eq 1 ]] || phases_json+="," - phases_json+="\"${p}\"" - first=0 - done - - gates_json="" - if [[ -n "${work_id}" ]]; then - _wf_ensure_state "${work_id}" - local file phase operation active gi gate state - file="$(_wf_state_file "${work_id}")" - phase="$(_wf_read_state_var "${file}" phase init)" - operation="$(_wf_read_state_var "${file}" operation)" - active="$(_wf_read_state_var "${file}" active 1)" - first=1 - gates_json="{" - for ((gi = 0; gi < ${#SDLC_GATE_NAMES[@]}; gi++)); do - gate="${SDLC_GATE_NAMES[$gi]}" - state="$(_wf_read_state_var "${file}" "gate_${gate}" pending)" - [[ "${first}" -eq 1 ]] || gates_json+="," - gates_json+="\"${gate}\":\"${state}\"" - first=0 - done - gates_json+="}" - - 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 <<EOF -SDLC workflow helper — short paths for humans and agents - - ${helper} # full status (auto-syncs from artifacts) - ${helper} next # concise "what do I do now?" - ${helper} start # open session brief at current phase - ${helper} capture --summary "..." # guarded capture (pointer must match) - ${helper} complete --summary "..." --verify-command CMD --verify-exit N --verify-result pass - ${helper} status --json - - ${helper} resume <WORK-ID> [--phase PHASE] [--force] - ${helper} advance [--to PHASE] [--force] - ${helper} gate <PHASE> [--work-id ID] # check prerequisites to enter a phase (exit 0/1) - ${helper} skip <PHASE> --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 <WORK-ID> [--force] [--branch NAME] [--pr #N] [--jira KEY] - ${helper} release --reason "why" - ${helper} sync-team # mark done/cancelled from canvas Final Status - ${helper} archive <WORK-ID> # 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/<WORK-ID>.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}" - if [[ -f "${root}/scripts/lib/paths.sh" ]]; then - printf '%s' "${root}/scripts/lib/paths.sh" - elif [[ -f "${root}/sdlc-spdd/scripts/lib/paths.sh" ]]; then - printf '%s' "${root}/sdlc-spdd/scripts/lib/paths.sh" - elif [[ -f "${root}/scripts/sdlc-spdd/lib/paths.sh" ]]; then - printf '%s' "${root}/scripts/sdlc-spdd/lib/paths.sh" - fi -} - -_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 - <<PY -import json -from pathlib import Path - -wid = ${work_id@Q} -kinds = set(x.strip() for x in ${kinds_csv@Q}.split(",") if x.strip()) if ${kinds_csv@Q} else None -count = 0 -for path in (Path(${ledger@Q}), Path(${stage@Q})): - if not path.is_file(): - continue - for line in path.read_text(encoding="utf-8").splitlines(): - line = line.strip() - if not line: - continue - try: - rec = json.loads(line) - except json.JSONDecodeError: - continue - if rec.get("work_id") != wid: - continue - if kinds is not None and rec.get("kind") not in kinds: - continue - count += 1 -print(count) -PY -} - -_wf_has_progress_evidence() { - local work_id="$1" - local root="${SDLC_ROOT}" - local n - n="$(_wf_ledger_record_count "${root}" "${work_id}" "")" - [[ "${n:-0}" -gt 0 ]] -} - -_wf_has_retro_evidence() { - local work_id="$1" - local root="${SDLC_ROOT}" - local n - n="$(_wf_ledger_record_count "${root}" "${work_id}" "decision,pitfall,pattern")" - [[ "${n:-0}" -gt 0 ]] -} - -# --- phase gates (requirements-first enforcement) --- - -_wf_python_gate_available() { - # Gate SUT is Python when importable (REF-001). SDLC_ENGINE=shell no longer - # skips that path. SDLC_GATE_ENGINE=shell is the explicit non-SUT fallback. - if [[ "${SDLC_GATE_ENGINE:-}" == "shell" ]]; then - return 1 - fi - if [[ -d "${SDLC_ROOT}/engine/src/sdlc_engine" ]]; then - PYTHONPATH="${SDLC_ROOT}/engine/src${PYTHONPATH:+:${PYTHONPATH}}" \ - python3 -c 'import sdlc_engine' 2>/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 <phase> [--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 - <<PY -import json -from pathlib import Path - -wid = ${work_id@Q} -lines = [] -for path in (Path(${ledger@Q}), Path(${stage@Q})): - if not path.is_file(): - continue - for line in path.read_text(encoding="utf-8").splitlines(): - line = line.strip() - if not line: - continue - try: - rec = json.loads(line) - except json.JSONDecodeError: - continue - if rec.get("work_id") != wid: - continue - title = rec.get("title") or rec.get("body", "")[:80] - lines.append(f"- {rec.get('ts','')}: [{rec.get('kind','')}] {title}") -for ln in lines: - print(ln) -PY -} - -_wf_home_path() { - _wf_ensure_paths_lib || { printf '%s' "${SDLC_ROOT}"; return; } - sdlc_home "${SDLC_ROOT}" -} - -_wf_infer_phase_from_artifacts() { - local work_id="$1" - local root="${SDLC_ROOT}" - local home - home="$(_wf_home_path)" - local inferred="init" - local req analysis canvas review sync_log progress_evidence - - 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 - inferred="analysis" - fi - if [[ -f "${analysis}" ]]; then - inferred="plan" - fi - if [[ -f "${canvas}" ]]; then - inferred="architect" - if grep -Eqi 'ready[[:space:]]+for[[:space:]]+coding' "${canvas}" 2>/dev/null; then - inferred="code" - fi - fi - if [[ -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() { - if [[ -x "${SDLC_ROOT}/sdlc-spdd/scripts/sdlc.sh" ]]; then - echo "./sdlc-spdd/scripts/sdlc.sh" - elif [[ -x "${SDLC_ROOT}/scripts/sdlc-spdd/sdlc.sh" ]]; then - echo "./scripts/sdlc-spdd/sdlc.sh" - else - echo "./scripts/sdlc.sh" - fi -} - -_wf_sync_impl() { - local work_id="$1" - local file stored inferred resolved gate_line gate value current - _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 <WORK-ID>" >&2 - return 2 - fi - - if [[ -z "${phase}" ]]; then - phase="$(_wf_read_state_var "$(_wf_state_file "${work_id}")" phase resume)" - fi - - local capture_script="${SDLC_ROOT}/sdlc-spdd/scripts/capture-session-memory.sh" - if [[ ! -x "${capture_script}" ]]; then - capture_script="${SDLC_ROOT}/scripts/sdlc-spdd/capture-session-memory.sh" - fi - if [[ ! -x "${capture_script}" ]]; then - capture_script="${SDLC_ROOT}/scripts/capture-session-memory.sh" - fi - if [[ ! -x "${capture_script}" ]]; then - echo "sdlc_workflow_capture: capture-session-memory.sh not found" >&2 - return 1 - fi - - run_against_pointer "${work_id}" -- "${capture_script}" \ - --target "${SDLC_ROOT}" \ - --work-id "${work_id}" \ - --phase "${phase}" \ - "${passthrough[@]}" -} - -sdlc_workflow_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 <WORK-ID> # 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_ROOT}/sdlc-spdd/scripts/accept-lessons.sh" - if [[ ! -x "${accept_script}" ]]; then - accept_script="${SDLC_ROOT}/scripts/sdlc-spdd/accept-lessons.sh" - fi - if [[ ! -x "${accept_script}" ]]; then - accept_script="${SDLC_ROOT}/scripts/accept-lessons.sh" - fi - 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/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/live-consumer/scenarios/01-install.sh b/tests/live-consumer/scenarios/01-install.sh index fd078721..f65bf783 100755 --- a/tests/live-consumer/scenarios/01-install.sh +++ b/tests/live-consumer/scenarios/01-install.sh @@ -9,8 +9,8 @@ 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 "no bash pointer twin" || bad "bash pointer twin installed" +[[ ! -e "${HOME}/scripts/sdlc-workflow.sh" ]] && ok "no bash workflow twin" || bad "bash workflow twin installed" [[ -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/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/test-framework-install-consolidate.sh b/tests/test-framework-install-consolidate.sh deleted file mode 100755 index 3a3bb955..00000000 --- a/tests/test-framework-install-consolidate.sh +++ /dev/null @@ -1,142 +0,0 @@ -#!/usr/bin/env bash -# Unit tests for storage-v3 consolidate/archive helpers in framework-install.sh. -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" -# shellcheck source=../scripts/lib/framework-install.sh -source "${REPO_ROOT}/scripts/lib/framework-install.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)); } - -echo "== framework_is_orchestrator_root ==" -if framework_is_orchestrator_root "${REPO_ROOT}"; then - ok "detects this orchestrator repo" -else - bad "failed to detect orchestrator repo" -fi -mkdir -p "${WORK}/plain" -if ! framework_is_orchestrator_root "${WORK}/plain"; then - ok "rejects plain target" -else - bad "false positive orchestrator detect" -fi - -echo "== framework_consolidate_path: move when dest missing ==" -mkdir -p "${WORK}/t1/src-dir" -echo move-me > "${WORK}/t1/src-dir/a.txt" -line="$(framework_consolidate_path "${WORK}/t1/src-dir" "${WORK}/t1/dest-dir" 0 "${WORK}/t1")" -if [[ "${line}" == move\ * && -f "${WORK}/t1/dest-dir/a.txt" && ! -e "${WORK}/t1/src-dir" ]]; then - ok "move when dest missing" -else - bad "move when dest missing (got: ${line})" -fi - -echo "== framework_consolidate_path: merge dirs, dest wins on conflict ==" -mkdir -p "${WORK}/t2/src/sub" "${WORK}/t2/dest/sub" -echo from-src-only > "${WORK}/t2/src/only-src.txt" -echo from-dest > "${WORK}/t2/dest/conflict.txt" -echo from-src > "${WORK}/t2/src/conflict.txt" -echo nested-src > "${WORK}/t2/src/sub/nested.txt" -echo nested-dest > "${WORK}/t2/dest/sub/keep.txt" -framework_consolidate_path "${WORK}/t2/src" "${WORK}/t2/dest" 0 "${WORK}/t2" >/dev/null -if [[ -f "${WORK}/t2/dest/only-src.txt" \ - && "$(cat "${WORK}/t2/dest/conflict.txt")" == "from-dest" \ - && -f "${WORK}/t2/dest/sub/nested.txt" \ - && -f "${WORK}/t2/dest/sub/keep.txt" \ - && ! -e "${WORK}/t2/src" ]]; then - ok "merge dirs with dest-wins conflict" -else - bad "merge dirs with dest-wins conflict" -fi - -echo "== framework_consolidate_path: dry-run leaves source ==" -mkdir -p "${WORK}/t3/src" -echo x > "${WORK}/t3/src/f.txt" -framework_consolidate_path "${WORK}/t3/src" "${WORK}/t3/dest" 1 "${WORK}/t3" >/dev/null 2>&1 -if [[ -f "${WORK}/t3/src/f.txt" && ! -e "${WORK}/t3/dest" ]]; then - ok "dry-run consolidate does not move" -else - bad "dry-run consolidate mutated tree" -fi - -echo "== framework_archive_legacy_path ==" -mkdir -p "${WORK}/t4/home/.sdlc" "${WORK}/t4/requirements" -echo req > "${WORK}/t4/requirements/r.md" -line="$(framework_archive_legacy_path "${WORK}/t4" "${WORK}/t4/home" "requirements" "STAMP1" 0)" -if [[ "${line}" == archive\ requirements\ -\>* \ - && -f "${WORK}/t4/home/.sdlc/legacy-layout-archive/STAMP1/requirements/r.md" \ - && ! -e "${WORK}/t4/requirements" ]]; then - ok "archive moves leftover under legacy-layout-archive" -else - bad "archive legacy path (got: ${line})" -fi - -echo "== framework_archive_remaining_legacy_layout archives agent-context always ==" -mkdir -p "${WORK}/t5/home/.sdlc" "${WORK}/t5/agent-context/custom" -echo custom > "${WORK}/t5/agent-context/custom/n.md" -echo roadmap > "${WORK}/t5/ROADMAP.md" -framework_archive_remaining_legacy_layout "${WORK}/t5" "${WORK}/t5/home" 0 "STAMP2" >/dev/null -if [[ ! -e "${WORK}/t5/agent-context" \ - && ! -e "${WORK}/t5/ROADMAP.md" \ - && -f "${WORK}/t5/home/.sdlc/legacy-layout-archive/STAMP2/agent-context/custom/n.md" ]]; then - ok "target archives agent-context + ROADMAP leftovers" -else - bad "target archive remaining leftovers" -fi - -# Fake orchestrator: agent-context still archived; root scripts/ kept. -mkdir -p "${WORK}/t6/home/.sdlc" \ - "${WORK}/t6/agent-context/harness" \ - "${WORK}/t6/scripts" \ - "${WORK}/t6/templates" \ - "${WORK}/t6/engine" \ - "${WORK}/t6/requirements" -: > "${WORK}/t6/scripts/init-project.sh" -: > "${WORK}/t6/scripts/upgrade-project.sh" -echo keep-src > "${WORK}/t6/agent-context/harness/quality-gates.md" -echo leftover-req > "${WORK}/t6/requirements/x.md" -printf '%s\n' '# keep' > "${WORK}/t6/scripts/keep-me.sh" -framework_archive_remaining_legacy_layout "${WORK}/t6" "${WORK}/t6/home" 0 "STAMP3" >/dev/null -if [[ ! -e "${WORK}/t6/agent-context" \ - && -f "${WORK}/t6/scripts/keep-me.sh" \ - && ! -e "${WORK}/t6/requirements" \ - && -f "${WORK}/t6/home/.sdlc/legacy-layout-archive/STAMP3/agent-context/harness/quality-gates.md" \ - && -f "${WORK}/t6/home/.sdlc/legacy-layout-archive/STAMP3/requirements/x.md" ]]; then - ok "orchestrator archives agent-context; keeps root scripts/" -else - bad "orchestrator archive remaining leftovers" -fi - -echo "== framework_rewrite_adapter_paths (no GNU sed -i) ==" -cat > "${WORK}/rewrite.md" <<'EOF' -See ./scripts/sdlc.sh and docs/sdlc-spdd/foo.md -Then spdd/canvas/ and .sdlc/staged/lessons.jsonl -Also requirements/x.md session-notes/ and ROADMAP.md -EOF -framework_rewrite_adapter_paths "${WORK}/rewrite.md" -got="$(cat "${WORK}/rewrite.md")" -if grep -Fq './sdlc-spdd/scripts/sdlc.sh' "${WORK}/rewrite.md" \ - && grep -Fq 'sdlc-spdd/docs/foo.md' "${WORK}/rewrite.md" \ - && grep -Fq 'sdlc-spdd/spdd/canvas/' "${WORK}/rewrite.md" \ - && grep -Fq 'sdlc-spdd/.sdlc/staged/lessons.jsonl' "${WORK}/rewrite.md" \ - && grep -Fq 'sdlc-spdd/requirements/x.md' "${WORK}/rewrite.md" \ - && grep -Fq 'sdlc-spdd/session-notes/' "${WORK}/rewrite.md" \ - && grep -Fq 'sdlc-spdd/ROADMAP.md' "${WORK}/rewrite.md"; then - ok "rewrites adapter paths without sed -i" -else - bad "rewrite failed: ${got}" -fi - -echo -echo "Results: ${pass} passed, ${fail} failed" -if [[ "${fail}" -gt 0 ]]; then - exit 1 -fi -echo "All framework-install consolidate unit tests passed." diff --git a/tests/test-integration-merge.sh b/tests/test-integration-merge.sh index 484254e5..c5a62774 100755 --- a/tests/test-integration-merge.sh +++ b/tests/test-integration-merge.sh @@ -127,8 +127,7 @@ for t in \ test-resolve-agent-context.sh \ test-archive-work.sh \ test-sdlc-pointer.sh \ - test-framework-install-consolidate.sh \ - test-upgrade-consolidate.sh; do + test-upgrade-layout.sh; do if "${REPO_ROOT}/tests/${t}" >/dev/null; then ok "nested ${t}" else diff --git a/tests/test-resolve-agent-context.sh b/tests/test-resolve-agent-context.sh index 102efe0c..0a5ee565 100755 --- a/tests/test-resolve-agent-context.sh +++ b/tests/test-resolve-agent-context.sh @@ -172,43 +172,6 @@ else bad "scoped excerpt file missing" fi -echo "== Test 13: legacy playbooks/extensions migrate idempotently ==" -mkdir -p "${WORK}/agent-context/playbooks" "${WORK}/agent-context/extensions/_all-agents" \ - "${WORK}/agent-context/extensions/coding-agent" "${WORK}/agent-context/extensions/skills" -echo "# Legacy bug playbook" > "${WORK}/agent-context/playbooks/legacy-widget-playbook.md" -echo "# Legacy norm" > "${WORK}/agent-context/extensions/_all-agents/legacy-norm.md" -echo "# Legacy style" > "${WORK}/agent-context/extensions/coding-agent/legacy-style.md" -echo "# Legacy TDD" > "${WORK}/agent-context/extensions/skills/legacy-tdd.md" -rm -f "${WORK}/harness/skills/legacy-widget.md" \ - "${WORK}/harness/skills/legacy-norm.md" \ - "${WORK}/harness/skills/legacy-style.md" \ - "${WORK}/harness/skills/legacy-tdd.md" -migrate_playbooks_extensions_to_skills "${WORK}" 0 -if [[ -f "${WORK}/harness/skills/legacy-widget.md" ]] && \ - [[ -f "${WORK}/harness/skills/legacy-norm.md" ]] && \ - [[ ! -d "${WORK}/agent-context/playbooks" ]] && \ - [[ ! -d "${WORK}/agent-context/extensions" ]]; then - ok "legacy trees migrated and removed" -else - bad "legacy migration failed" -fi -migrate_playbooks_extensions_to_skills "${WORK}" 0 -if [[ -f "${WORK}/harness/skills/legacy-widget.md" ]]; then - ok "second migrate is idempotent" -else - bad "idempotent migrate broke skills" -fi - -echo "== Test 14: session-handoff playbook is not migrated ==" -mkdir -p "${WORK}/agent-context/playbooks" -echo "# Session handoff" > "${WORK}/agent-context/playbooks/session-handoff-playbook.md" -migrate_playbooks_extensions_to_skills "${WORK}" 0 -if [[ ! -f "${WORK}/harness/skills/session-handoff.md" ]]; then - ok "session-handoff skipped" -else - bad "session-handoff should not migrate" -fi - echo if (( fail > 0 )); then echo "${fail} failed, ${pass} passed" >&2 diff --git a/tests/test-upgrade-consolidate.sh b/tests/test-upgrade-consolidate.sh deleted file mode 100755 index 9787167f..00000000 --- a/tests/test-upgrade-consolidate.sh +++ /dev/null @@ -1,340 +0,0 @@ -#!/usr/bin/env bash -# Full regression suite for storage-v3 upgrade consolidation: -# A. Pure legacy sprawl (no home yet) → single sdlc-spdd/ home -# B. Dual layout merge when home already exists (dest wins) -# C. Leftover agent-context archived (not left at root) -# D. Idempotent second upgrade -# E. Dry-run + --consolidate no-op flag -# F. Orchestrator-shaped target archives agent-context/; keeps root scripts/ -# G. verify-project-install accepts orchestrator after stay-set move -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" - -SETUP="${REPO_ROOT}/scripts/setup-agent-prompts.sh" -INIT="${REPO_ROOT}/scripts/init-project.sh" -UPGRADE="${REPO_ROOT}/scripts/upgrade-project.sh" -VERIFY="${REPO_ROOT}/scripts/verify-project-install.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)); } - -assert_absent() { - local root="$1" path="$2" label="${3:-$2}" - if [[ ! -e "${root}/${path}" ]]; then ok "absent: ${label}"; else bad "should be absent: ${label}"; fi -} -assert_file() { - local f="$1" label="${2:-$1}" - if [[ -f "${f}" ]]; then ok "exists: ${label}"; else bad "missing: ${label}"; fi -} -assert_file_contains() { - local f="$1" expected="$2" label="${3:-$1}" - if [[ -f "${f}" ]] && grep -Fq "${expected}" "${f}"; then - ok "contains (${label}): ${expected}" - else - bad "missing (${label}): ${expected}" - fi -} -assert_dir() { - local d="$1" label="${2:-$1}" - if [[ -d "${d}" ]]; then ok "dir: ${label}"; else bad "missing dir: ${label}"; fi -} - -seed_legacy_sprawl() { - local target="$1" - mkdir -p \ - "${target}/requirements/milestones" \ - "${target}/spdd/canvas" \ - "${target}/spdd/memory" \ - "${target}/session-notes" \ - "${target}/docs/sdlc-spdd" \ - "${target}/harness/skills" \ - "${target}/agent-context/harness/skills" \ - "${target}/agent-context/playbooks" \ - "${target}/agent-context/custom" \ - "${target}/scripts/sdlc-spdd" \ - "${target}/.sdlc/sessions" - printf '%s\n' '# Legacy ROADMAP' > "${target}/ROADMAP.md" - printf '%s\n' '# Legacy req' > "${target}/requirements/legacy-req.md" - printf '%s\n' '# Legacy canvas' > "${target}/spdd/canvas/LEGACY-001.md" - : > "${target}/spdd/memory/lessons.jsonl" - : > "${target}/spdd/memory/registry.jsonl" - printf '%s\n' '# Legacy note' > "${target}/session-notes/legacy.md" - printf '%s\n' '# Legacy docs hub' > "${target}/docs/sdlc-spdd/README.md" - printf '%s\n' '# Project-local doc keep' > "${target}/docs/sdlc-spdd/project-local.md" - printf '%s\n' '# Root harness skill' > "${target}/harness/skills/root-skill.md" - printf '%s\n' '# AC harness skill' > "${target}/agent-context/harness/skills/ac-skill.md" - printf '%s\n' '# playbook' > "${target}/agent-context/playbooks/legacy-widget-playbook.md" - printf '%s\n' '# custom leftover' > "${target}/agent-context/custom/note.md" - printf '%s\n' '#!/bin/sh' > "${target}/scripts/sdlc-spdd/legacy-marker.sh" - printf '%s\n' 'session-at-root' > "${target}/.sdlc/sessions/root-session.txt" - printf '%s\n' '# Milestone' > "${target}/milestone-legacy.md" - # Minimal adapters so upgrade --cursor can refresh without init. - mkdir -p "${target}/.cursor/commands" "${target}/.cursor/rules" - printf '%s\n' '# stub' > "${target}/.cursor/commands/sdlc-spdd-plan.md" - printf '%s\n' '# stub' > "${target}/.cursor/commands/sdlc-spdd-init.md" - printf '%s\n' 'stub' > "${target}/.cursor/rules/sdlc-spdd.mdc" - printf '%s\n' $'\n' > "${target}/.gitignore" -} - -# --------------------------------------------------------------------------- -echo "== A. Pure legacy sprawl → sdlc-spdd/ home ==" -# --------------------------------------------------------------------------- -A="${WORK}/pure-legacy" -mkdir -p "${A}" -seed_legacy_sprawl "${A}" -AH="${A}/sdlc-spdd" - -"${UPGRADE}" --target "${A}" --cursor --no-backup >/dev/null - -assert_dir "${AH}" "home created" -assert_absent "${A}" "requirements" -assert_absent "${A}" "spdd" -assert_absent "${A}" "session-notes" -assert_absent "${A}" "ROADMAP.md" -assert_absent "${A}" "harness" -assert_absent "${A}" "docs/sdlc-spdd" -assert_absent "${A}" "scripts/sdlc-spdd" -assert_absent "${A}" ".sdlc" -assert_absent "${A}" "agent-context" -assert_absent "${A}" "milestone-legacy.md" - -assert_file_contains "${AH}/ROADMAP.md" "Legacy ROADMAP" "home ROADMAP" -assert_file_contains "${AH}/requirements/legacy-req.md" "Legacy req" "home req" -assert_file_contains "${AH}/spdd/canvas/LEGACY-001.md" "Legacy canvas" "home canvas" -assert_file_contains "${AH}/session-notes/legacy.md" "Legacy note" "home session note" -# Framework refresh may overwrite docs/README.md; project-local docs must remain. -assert_file_contains "${AH}/docs/project-local.md" "Project-local doc keep" "home project-local docs" -assert_file "${AH}/docs/README.md" "home docs hub present" -assert_file_contains "${AH}/harness/skills/root-skill.md" "Root harness skill" "root harness skill" -assert_file_contains "${AH}/harness/skills/ac-skill.md" "AC harness skill" "ac harness skill" -assert_file_contains "${AH}/scripts/legacy-marker.sh" "#!/bin/sh" "legacy scripts merged" -assert_file_contains "${AH}/.sdlc/sessions/root-session.txt" "session-at-root" "runtime merged" -assert_file "${AH}/milestone-legacy.md" "milestone moved" -assert_file "${AH}/spdd/memory/lessons.jsonl" "lessons ledger" -assert_file "${AH}/spdd/memory/registry.jsonl" "registry ledger" - -shopt -s nullglob -archive_hits=("${AH}"/.sdlc/legacy-layout-archive/*/agent-context/custom/note.md) -shopt -u nullglob -if ((${#archive_hits[@]} > 0)); then - ok "A: leftover agent-context archived" -else - bad "A: leftover agent-context not archived" -fi - -if "${VERIFY}" --target "${A}" --require-cursor >/dev/null; then - ok "A: verify passes after pure-legacy upgrade" -else - bad "A: verify failed after pure-legacy upgrade" -fi - -# --------------------------------------------------------------------------- -echo "== B. Dual layout merge (home already exists; dest wins) ==" -# --------------------------------------------------------------------------- -B="${WORK}/dual" -mkdir -p "${B}" -"${SETUP}" --target "${B}" --cursor >/dev/null -BH="${B}/sdlc-spdd" - -printf '%s\n' '# Home canvas' > "${BH}/spdd/canvas/HOME-001.md" -printf '%s\n' '# Home ROADMAP keep' > "${BH}/ROADMAP.md" -mkdir -p "${B}/spdd/canvas" "${B}/requirements" "${B}/scripts/sdlc-spdd" \ - "${B}/session-notes" "${BH}/.sdlc/staged" "${B}/.sdlc/sessions" \ - "${B}/agent-context/custom" -printf '%s\n' '# Legacy canvas' > "${B}/spdd/canvas/LEGACY-001.md" -printf '%s\n' '# Legacy req' > "${B}/requirements/legacy-req.md" -printf '%s\n' '# Home req' > "${BH}/requirements/home-req.md" -printf '%s\n' '# root roadmap lose' > "${B}/ROADMAP.md" -printf '%s\n' '# legacy script' > "${B}/scripts/sdlc-spdd/legacy-marker.sh" -printf '%s\n' 'staged-at-home' > "${BH}/.sdlc/staged/note.txt" -printf '%s\n' 'session-at-root' > "${B}/.sdlc/sessions/root-session.txt" -printf '%s\n' '# leftover custom' > "${B}/agent-context/custom/note.md" -printf '%s\n' '# legacy note' > "${B}/session-notes/legacy.md" - -"${UPGRADE}" --target "${B}" --cursor --no-backup >/dev/null - -assert_absent "${B}" "requirements" -assert_absent "${B}" "spdd" -assert_absent "${B}" "scripts/sdlc-spdd" -assert_absent "${B}" ".sdlc" -assert_absent "${B}" "agent-context" -assert_absent "${B}" "ROADMAP.md" -assert_absent "${B}" "session-notes" - -assert_file_contains "${BH}/spdd/canvas/HOME-001.md" "Home canvas" "B home canvas kept" -assert_file_contains "${BH}/spdd/canvas/LEGACY-001.md" "Legacy canvas" "B legacy canvas merged" -assert_file_contains "${BH}/requirements/home-req.md" "Home req" "B home req kept" -assert_file_contains "${BH}/requirements/legacy-req.md" "Legacy req" "B legacy req merged" -assert_file_contains "${BH}/ROADMAP.md" "Home ROADMAP keep" "B dest ROADMAP wins" -assert_file_contains "${BH}/.sdlc/staged/note.txt" "staged-at-home" "B staged kept" -assert_file_contains "${BH}/.sdlc/sessions/root-session.txt" "session-at-root" "B runtime merged" -assert_file_contains "${BH}/session-notes/legacy.md" "legacy note" "B session notes merged" -assert_file "${BH}/scripts/legacy-marker.sh" "B legacy scripts merged" - -shopt -s nullglob -archive_hits=("${BH}"/.sdlc/legacy-layout-archive/*/agent-context/custom/note.md) -shopt -u nullglob -if ((${#archive_hits[@]} > 0)); then - ok "B: leftover agent-context archived" -else - bad "B: leftover agent-context not archived" -fi - -if "${VERIFY}" --target "${B}" --require-cursor >/dev/null; then - ok "B: verify passes after dual-layout upgrade" -else - bad "B: verify failed after dual-layout upgrade" -fi - -# --------------------------------------------------------------------------- -echo "== C. Idempotent second upgrade ==" -# --------------------------------------------------------------------------- -before_canvas="$(cat "${BH}/spdd/canvas/HOME-001.md")" -"${UPGRADE}" --target "${B}" --cursor --no-backup >/dev/null -after_canvas="$(cat "${BH}/spdd/canvas/HOME-001.md")" -if [[ "${before_canvas}" == "${after_canvas}" ]]; then - ok "C: second upgrade preserves project canvas" -else - bad "C: second upgrade mutated project canvas" -fi -assert_absent "${B}" "requirements" "C still absent requirements" -assert_absent "${B}" "spdd" "C still absent spdd" -assert_absent "${B}" "agent-context" "C still absent agent-context" -if "${VERIFY}" --target "${B}" --require-cursor >/dev/null; then - ok "C: verify still passes" -else - bad "C: verify failed after second upgrade" -fi - -# --------------------------------------------------------------------------- -echo "== D. Dry-run + --consolidate no-op ==" -# --------------------------------------------------------------------------- -D="${WORK}/dry-run" -mkdir -p "${D}" -seed_legacy_sprawl "${D}" -if "${UPGRADE}" --target "${D}" --cursor --dry-run --consolidate >/dev/null 2>&1; then - ok "D: dry-run + --consolidate accepted" -else - bad "D: dry-run/--consolidate failed" -fi -if [[ -d "${D}/requirements" && -d "${D}/spdd" && ! -d "${D}/sdlc-spdd" ]]; then - ok "D: dry-run left legacy tree untouched" -else - bad "D: dry-run mutated or created home unexpectedly" -fi - -# --------------------------------------------------------------------------- -echo "== E. Orchestrator-shaped target archives agent-context/ ==" -# --------------------------------------------------------------------------- -E="${WORK}/orch-shaped" -mkdir -p "${E}" -# Minimal orchestrator markers + stay-set sprawl + leftover agent-context. -mkdir -p \ - "${E}/scripts" \ - "${E}/templates/cursor" \ - "${E}/engine/src" \ - "${E}/agent-context/harness/skills" \ - "${E}/agent-context/custom" \ - "${E}/requirements" \ - "${E}/spdd/canvas" \ - "${E}/session-notes" \ - "${E}/.sdlc/sessions" \ - "${E}/.cursor/commands" \ - "${E}/.cursor/rules" -cp "${REPO_ROOT}/scripts/init-project.sh" "${E}/scripts/init-project.sh" -cp "${REPO_ROOT}/scripts/upgrade-project.sh" "${E}/scripts/upgrade-project.sh" -printf '%s\n' '# orch harness leftover' > "${E}/agent-context/harness/quality-gates.md" -printf '%s\n' '# orch skill leftover' > "${E}/agent-context/harness/skills/source-skill.md" -printf '%s\n' '# custom leftover' > "${E}/agent-context/custom/note.md" -printf '%s\n' '#!/bin/sh' > "${E}/agent-context/sdlc-workflow.sh" -chmod +x "${E}/agent-context/sdlc-workflow.sh" -printf '%s\n' '# stay-set req' > "${E}/requirements/orch-req.md" -printf '%s\n' '# stay-set canvas' > "${E}/spdd/canvas/ORCH-001.md" -printf '%s\n' '# stay-set note' > "${E}/session-notes/orch.md" -printf '%s\n' '# stay-set roadmap' > "${E}/ROADMAP.md" -printf '%s\n' 'session' > "${E}/.sdlc/sessions/s.txt" -printf '%s\n' '# stub' > "${E}/.cursor/commands/sdlc-spdd-plan.md" -printf '%s\n' '# stub' > "${E}/.cursor/commands/sdlc-spdd-init.md" -printf '%s\n' 'stub' > "${E}/.cursor/rules/sdlc-spdd.mdc" -printf '%s\n' $'\n' > "${E}/.gitignore" -printf '%s\n' '# keep' > "${E}/scripts/keep-me.sh" - -EH="${E}/sdlc-spdd" -"${UPGRADE}" --target "${E}" --cursor --no-backup >/dev/null - -assert_absent "${E}" "requirements" "E stay-set requirements moved" -assert_absent "${E}" "spdd" "E stay-set spdd moved" -assert_absent "${E}" "session-notes" "E stay-set session-notes moved" -assert_absent "${E}" "ROADMAP.md" "E stay-set ROADMAP moved" -assert_absent "${E}" ".sdlc" "E root .sdlc moved" -assert_absent "${E}" "agent-context" "E agent-context archived" - -if [[ -f "${E}/scripts/keep-me.sh" && -f "${E}/scripts/init-project.sh" ]]; then - ok "E: orchestrator root scripts/ preserved" -else - bad "E: orchestrator root scripts/ were removed" -fi - -assert_file_contains "${EH}/requirements/orch-req.md" "stay-set req" "E home req" -assert_file_contains "${EH}/spdd/canvas/ORCH-001.md" "stay-set canvas" "E home canvas" -assert_file_contains "${EH}/ROADMAP.md" "stay-set roadmap" "E home ROADMAP" -assert_file_contains "${EH}/harness/skills/source-skill.md" "orch skill leftover" "E harness skill consolidated" -assert_file "${EH}/harness/quality-gates.md" "E home harness quality-gates present" - -shopt -s nullglob -archive_hits=("${EH}"/.sdlc/legacy-layout-archive/*/agent-context/custom/note.md) -shopt -u nullglob -if ((${#archive_hits[@]} > 0)); then - ok "E: leftover agent-context custom archived" -else - bad "E: leftover agent-context custom not archived" -fi - -if "${VERIFY}" --target "${E}" --require-cursor >/dev/null; then - ok "E: verify passes for orchestrator-shaped target" -else - bad "E: verify failed for orchestrator-shaped target" -fi - -# --------------------------------------------------------------------------- -echo "== F. Fresh init already v3 — upgrade is no-op for layout ==" -# --------------------------------------------------------------------------- -F="${WORK}/fresh-v3" -mkdir -p "${F}" -"${INIT}" --target "${F}" --cursor --no-backup >/dev/null 2>&1 \ - || "${SETUP}" --target "${F}" --cursor >/dev/null -FH="${F}/sdlc-spdd" -assert_dir "${FH}" "F v3 home from init/setup" -printf '%s\n' '# project canvas' > "${FH}/spdd/canvas/FRESH-001.md" -"${UPGRADE}" --target "${F}" --cursor --no-backup >/dev/null -assert_absent "${F}" "requirements" "F no root requirements" -assert_absent "${F}" "spdd" "F no root spdd" -assert_file_contains "${FH}/spdd/canvas/FRESH-001.md" "project canvas" "F project content kept" -if "${VERIFY}" --target "${F}" --require-cursor >/dev/null; then - ok "F: verify passes on fresh v3 after upgrade" -else - bad "F: verify failed on fresh v3 after upgrade" -fi - -# --------------------------------------------------------------------------- -echo "== G. Nested helper unit suite ==" -# --------------------------------------------------------------------------- -if "${REPO_ROOT}/tests/test-framework-install-consolidate.sh" >/dev/null; then - ok "G: framework-install consolidate unit tests" -else - bad "G: framework-install consolidate unit tests" -fi - -echo -echo "Results: ${pass} passed, ${fail} failed" -if [[ "${fail}" -gt 0 ]]; then - exit 1 -fi -echo "All upgrade-consolidate tests passed." diff --git a/tests/test-upgrade-layout.sh b/tests/test-upgrade-layout.sh new file mode 100755 index 00000000..2535ec91 --- /dev/null +++ b/tests/test-upgrade-layout.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +# Storage v3 layout contract for upgrade-project.sh: +# A. Pre-v3 sprawl with no sdlc-spdd/ home → refused (exit 3), nothing moved +# B. Dual layout (home + leftover legacy paths) → refused, nothing moved +# C. Clean v3 install → upgrade succeeds, is idempotent, verify passes +# D. Retired bash twins are never installed and are pruned on upgrade +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" + +INIT="${REPO_ROOT}/scripts/init-project.sh" +UPGRADE="${REPO_ROOT}/scripts/upgrade-project.sh" +VERIFY="${REPO_ROOT}/scripts/verify-project-install.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)); } + +seed_legacy_sprawl() { + local target="$1" + mkdir -p \ + "${target}/requirements/milestones" \ + "${target}/spdd/canvas" \ + "${target}/session-notes" \ + "${target}/agent-context/harness" + echo "# legacy canvas" > "${target}/spdd/canvas/FEAT-001-legacy.md" + printf 'work_id\tstatus\n' > "${target}/agent-context/work-registry.tsv" +} + +run_upgrade() { + local target="$1" + local out rc=0 + out="$("${UPGRADE}" --target "${target}" 2>&1)" || rc=$? + printf '%s\n' "${out}" > "${target}.upgrade.log" + return "${rc}" +} + +echo "== A. Pre-v3 sprawl without home is refused ==" +mkdir -p "${WORK}/a" +seed_legacy_sprawl "${WORK}/a" +if run_upgrade "${WORK}/a"; then + bad "upgrade should refuse pre-v3 layout" +else + rc=$? + [[ "${rc}" -eq 3 ]] && ok "exit 3 on pre-v3 layout" || bad "expected exit 3, got ${rc}" +fi +grep -q "pre-v3 SDLC-SPDD layout detected" "${WORK}/a.upgrade.log" && ok "refusal message printed" || bad "missing refusal message" +grep -q "init-project.sh --target" "${WORK}/a.upgrade.log" && ok "points at init-project" || bad "missing init hint" +[[ -f "${WORK}/a/spdd/canvas/FEAT-001-legacy.md" ]] && ok "legacy canvas untouched" || bad "legacy canvas was moved" +[[ ! -d "${WORK}/a/sdlc-spdd" ]] && ok "no home created on refusal" || bad "home created despite refusal" + +echo "== B. Dual layout (home + legacy leftovers) is refused ==" +mkdir -p "${WORK}/b" +"${INIT}" --target "${WORK}/b" >/dev/null 2>&1 || bad "init failed for B" +mkdir -p "${WORK}/b/agent-context/memory" +echo "x" > "${WORK}/b/agent-context/memory/lessons.md" +if run_upgrade "${WORK}/b"; then + bad "upgrade should refuse dual layout" +else + ok "dual layout refused" +fi +grep -q "re-run this upgrade" "${WORK}/b.upgrade.log" && ok "dual-layout hint printed" || bad "missing dual-layout hint" +[[ -f "${WORK}/b/agent-context/memory/lessons.md" ]] && ok "legacy leftover untouched" || bad "legacy leftover was moved" +[[ ! -d "${WORK}/b/sdlc-spdd/.sdlc/legacy-layout-archive" ]] && ok "no legacy archive dir" || bad "legacy archive dir created" + +echo "== C. Clean v3 install upgrades idempotently ==" +mkdir -p "${WORK}/c" +"${INIT}" --target "${WORK}/c" >/dev/null 2>&1 && ok "init v3" || bad "init failed for C" +echo "# project canvas" > "${WORK}/c/sdlc-spdd/spdd/canvas/FEAT-002-keep.md" +run_upgrade "${WORK}/c" && ok "first upgrade" || bad "first upgrade failed: $(cat "${WORK}/c.upgrade.log")" +run_upgrade "${WORK}/c" && ok "second upgrade" || bad "second upgrade failed" +[[ -f "${WORK}/c/sdlc-spdd/spdd/canvas/FEAT-002-keep.md" ]] && ok "project canvas preserved" || bad "project canvas lost" +if "${VERIFY}" --target "${WORK}/c" >"${WORK}/c.verify.log" 2>&1; then + ok "verify passes after upgrade" +else + bad "verify failed: $(tail -20 "${WORK}/c.verify.log")" +fi + +echo "== D. Retired bash twins are not installed and are pruned ==" +for twin in sdlc-workflow.sh sdlc-team-registry.sh sdlc-pointer.sh accept-lessons.sh; do + [[ ! -e "${WORK}/c/sdlc-spdd/scripts/${twin}" ]] && ok "not installed: ${twin}" || bad "installed: ${twin}" +done +touch "${WORK}/c/sdlc-spdd/scripts/sdlc-workflow.sh" +run_upgrade "${WORK}/c" || bad "upgrade with stale twin failed" +[[ ! -e "${WORK}/c/sdlc-spdd/scripts/sdlc-workflow.sh" ]] && ok "stale twin pruned" || bad "stale twin left behind" +[[ -x "${WORK}/c/sdlc-spdd/scripts/sdlc.sh" ]] && ok "sdlc.sh dispatcher present" || bad "sdlc.sh missing" +grep -q "python -m sdlc_engine\|sdlc_engine" "${WORK}/c/sdlc-spdd/scripts/sdlc.sh" && ok "dispatcher targets Python engine" || bad "dispatcher does not target engine" + +echo +echo "Results: ${pass} passed, ${fail} failed" +(( fail == 0 )) From a7446313a49a16f394d4ef4d461a81454e99c90b Mon Sep 17 00:00:00 2001 From: Cursor Agent <cursoragent@cursor.com> Date: Mon, 14 Sep 2026 00:01:14 +0000 Subject: [PATCH 13/22] CHORE: refresh dogfood install (twins pruned, framework docs synced) Co-authored-by: John Menke <jmjava@gmail.com> --- .../workflows/validate-sdlc-spdd-adapters.yml | 14 +- .../adf-template-library-and-vue3-console.md | 8 +- sdlc-spdd/docs/spdd-compliance.md | 2 +- sdlc-spdd/docs/storage-v3.md | 4 +- sdlc-spdd/docs/triple-path-context.md | 11 +- sdlc-spdd/scripts/accept-lessons.sh | 216 -- sdlc-spdd/scripts/capture-session-memory.sh | 14 +- .../hooks/notify-team-registry.example.sh | 37 - sdlc-spdd/scripts/lib/python.sh | 15 + sdlc-spdd/scripts/lib/skills.sh | 126 - sdlc-spdd/scripts/resolve-agent-context.sh | 4 +- sdlc-spdd/scripts/sdlc-pointer.sh | 133 -- sdlc-spdd/scripts/sdlc-team-registry.sh | 981 -------- sdlc-spdd/scripts/sdlc-workflow.sh | 2089 ----------------- sdlc-spdd/scripts/sdlc.sh | 201 +- sdlc-spdd/scripts/start-agent-session.sh | 73 +- sdlc-spdd/scripts/verify-project-install.sh | 8 +- 17 files changed, 105 insertions(+), 3831 deletions(-) delete mode 100755 sdlc-spdd/scripts/accept-lessons.sh delete mode 100644 sdlc-spdd/scripts/hooks/notify-team-registry.example.sh 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 diff --git a/.github/workflows/validate-sdlc-spdd-adapters.yml b/.github/workflows/validate-sdlc-spdd-adapters.yml index dcc65968..7184de52 100644 --- a/.github/workflows/validate-sdlc-spdd-adapters.yml +++ b/.github/workflows/validate-sdlc-spdd-adapters.yml @@ -9,7 +9,6 @@ on: - '.github/copilot-instructions.md' - '.claude/commands/sdlc-*.md' - 'CLAUDE.md' - - 'scripts/validate-command-adapters.sh' - 'sdlc-spdd/scripts/validate-command-adapters.sh' - '.github/workflows/validate-sdlc-spdd-adapters.yml' push: @@ -21,7 +20,6 @@ on: - '.github/copilot-instructions.md' - '.claude/commands/sdlc-*.md' - 'CLAUDE.md' - - 'scripts/validate-command-adapters.sh' - 'sdlc-spdd/scripts/validate-command-adapters.sh' - '.github/workflows/validate-sdlc-spdd-adapters.yml' @@ -34,13 +32,5 @@ jobs: - name: Check validator shell syntax run: bash -n sdlc-spdd/scripts/validate-command-adapters.sh - - name: Validate installed dogfood packs (not templates/) - run: | - set -euo pipefail - scratch="$(mktemp -d)" - mkdir -p "${scratch}/.cursor/commands" "${scratch}/.cursor/rules" - cp .cursor/commands/sdlc-*.md "${scratch}/.cursor/commands/" - cp .cursor/rules/sdlc-spdd.mdc "${scratch}/.cursor/rules/" - # Copilot/Claude dogfood I1/I2 parity is leftover #5; Cursor is the - # proving host. --mode installed so a templates/ copy cannot hide drift. - bash sdlc-spdd/scripts/validate-command-adapters.sh --target "${scratch}" --mode installed + - name: Validate installed command packs (not templates/) + run: bash sdlc-spdd/scripts/validate-command-adapters.sh --target . --mode installed diff --git a/sdlc-spdd/docs/adf-template-library-and-vue3-console.md b/sdlc-spdd/docs/adf-template-library-and-vue3-console.md index fe6255d1..a3dde732 100644 --- a/sdlc-spdd/docs/adf-template-library-and-vue3-console.md +++ b/sdlc-spdd/docs/adf-template-library-and-vue3-console.md @@ -118,12 +118,14 @@ Flask can serve the Vite build when `SDLC_VUE_CONSOLE_DIST=console-ui/dist` (or | Tab | Vue3 | Playwright | |-----|------|------------| -| Persistence (load + save) | ✅ | ✅ | +| Dashboard (status + suggestions + configure) | ✅ | ✅ | +| Persistence (load + save + parity) | ✅ | ✅ | | Templates (feature/spike/bug + write) | ✅ | ✅ | | Install | ✅ | ✅ | | SQLite | ✅ | ✅ | -| Rollback | ✅ | ✅ | -| Guide | ✅ | ✅ stubbed probe + dual-env defaults; **live** via `guide_live` | +| Rollback | ✅ | ✅ list + dry-run restore | +| Guide | ✅ | ✅ stubbed probe + config save + dual-env defaults; **live** via `guide_live` | +| Issues (integrations + link/sync dry-run) | ✅ | ✅ Jira tracker select + link/sync | | ADF viewer + init-from-ADF | ✅ | ✅ stubbed lifecycle; **live** via `adf_viewer_live` | ### Live dual-repo gates (the former “no” gaps) diff --git a/sdlc-spdd/docs/spdd-compliance.md b/sdlc-spdd/docs/spdd-compliance.md index de3457c4..d28b072a 100644 --- a/sdlc-spdd/docs/spdd-compliance.md +++ b/sdlc-spdd/docs/spdd-compliance.md @@ -4,7 +4,7 @@ This project follows Structured Prompt-Driven Development (SPDD) as described in Martin Fowler's article, [Structured-Prompt-Driven Development](https://martinfowler.com/articles/structured-prompt-driven/), inside a hybrid lifecycle influenced by [SDLC Agents](https://github.com/dsilahcilar/sdlc-agents). -This page is a **mapping** of those expectations onto this scaffold. It is not evidence that the method reduces drift (C-DRIFT) or that sessions are process-compliant (C-COMPLY). Empirical claims are allow-listed in [research questions and constructs](research/research-questions-and-constructs.md). +This page is a **mapping** of those expectations onto this scaffold. It is not evidence that the method reduces drift (C-DRIFT) or that sessions are process-compliant (C-COMPLY). Empirical claims are allow-listed in [research questions and constructs](../sdlc-spdd/docs/research/research-questions-and-constructs.md). SPDD treats prompts as first-class delivery artifacts that are version controlled, reviewed, reused, and improved over time. This scaffold implements that contract with REASONS Canvas files, lifecycle prompt templates, assistant command prompts, progress logs, review reports, sync logs, and reusable project memory. diff --git a/sdlc-spdd/docs/storage-v3.md b/sdlc-spdd/docs/storage-v3.md index 7054ed4f..42460a97 100644 --- a/sdlc-spdd/docs/storage-v3.md +++ b/sdlc-spdd/docs/storage-v3.md @@ -12,8 +12,8 @@ quietly and accepted at gates.** - **One home.** Framework-owned paths live under `<repo>/sdlc-spdd/`. Contracts, harness, scripts, and ledgers share one install layout (this orchestrator - dogfoods the same layout; `agent-context/` + root `scripts/` remain only as - framework *source*). + dogfoods the same layout). Install *source* lives under `templates/`; root + `scripts/` is orchestrator tooling only. Leftover `agent-context/` is archived. - **Ledger-first.** One committed JSONL lessons file is the system of record; the work registry is a separate append-only event log. Neither is hand-edited. - **Stage-then-accept.** Captures land in gitignored `.sdlc/staged/`; accept at diff --git a/sdlc-spdd/docs/triple-path-context.md b/sdlc-spdd/docs/triple-path-context.md index dc7aaa21..a5be27da 100644 --- a/sdlc-spdd/docs/triple-path-context.md +++ b/sdlc-spdd/docs/triple-path-context.md @@ -67,7 +67,7 @@ probe only apply on the defaults path. ## Persist ```bash -# Stage a lesson (decision | pitfall | pattern | session | analysis) +# Structured day — Work ID is real sdlc-engine context persist-lesson \ --kind pitfall \ --work-id FEAT-001-example \ @@ -75,8 +75,17 @@ sdlc-engine context persist-lesson \ --body "Never open PRs against embabel/guide" \ --no-guide # optional: skip Guide projection even if enabled +# Unstructured day — no fake FEAT; kind + area + body +sdlc-engine context persist-lesson \ + --kind pitfall \ + --area notify \ + --source adhoc-prompt \ + --body "Retry without an idempotency key double-posts." \ + --no-guide + # Promote staged records at the retro/sync gate sdlc-engine context accept --work-id FEAT-001-example +sdlc-engine context accept --ids 'pitfall:(none):notify:adhoc-prompt' ``` Writes go to `.sdlc/staged/lessons.jsonl` (stage; git stays quiet) and, on diff --git a/sdlc-spdd/scripts/accept-lessons.sh b/sdlc-spdd/scripts/accept-lessons.sh deleted file mode 100755 index f129b7d1..00000000 --- a/sdlc-spdd/scripts/accept-lessons.sh +++ /dev/null @@ -1,216 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# shellcheck source=/dev/null -source "${_SCRIPT_DIR}/lib/common.sh" -# shellcheck source=/dev/null -source "${_SCRIPT_DIR}/lib/paths.sh" - -usage() { - cat <<'EOF' -Usage: accept-lessons.sh [options] - -Promote staged lesson records into spdd/memory/lessons.jsonl (dedupe by id, last wins). -Non-promoted staged records remain unless --discard-rest. - -Options: - --target <path> Target project (default: .) - --work-id <ID> Promote only records for this Work ID - --ids <a,b,c> Promote only these record ids (comma-separated) - --discard-rest Drop non-promoted staged records in scope from stage - --commit git add ledger + git commit with memory message - --list Show staged records (id kind work_id title) - --help Print this help - -Also available via: ./scripts/sdlc.sh accept [options] - -Examples: - ./scripts/accept-lessons.sh --work-id FEAT-001-order-status-api - ./scripts/accept-lessons.sh --work-id FEAT-001 --commit - ./scripts/accept-lessons.sh --list -EOF -} - -TARGET="." -WORK_ID="" -IDS="" -DISCARD_REST=0 -DO_COMMIT=0 -LIST_ONLY=0 - -while [[ $# -gt 0 ]]; do - case "$1" in - --target) TARGET="${2:-}"; shift 2 ;; - --work-id) WORK_ID="${2:-}"; shift 2 ;; - --ids) IDS="${2:-}"; shift 2 ;; - --discard-rest) DISCARD_REST=1; shift ;; - --commit) DO_COMMIT=1; shift ;; - --list) LIST_ONLY=1; shift ;; - --help|-h) usage; exit 0 ;; - *) - echo "Unknown option: $1" >&2 - usage >&2 - exit 1 - ;; - esac -done - -TARGET="$(sdlc_resolve_target "${TARGET}")" -export SDLC_ROOT="${TARGET}" - -ledger="$(sdlc_ledger "${TARGET}")" -stage="$(sdlc_stage "${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 $? - 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[@]}" - fi -} - -if [[ "${LIST_ONLY}" -eq 1 ]]; then - if [[ ! -f "${stage}" ]]; then - echo "No staged records." - exit 0 - fi - python3 - <<PY -import json -from pathlib import Path -p = Path(${stage@Q}) -for line in p.read_text(encoding="utf-8").splitlines(): - line = line.strip() - if not line: - continue - try: - r = json.loads(line) - except json.JSONDecodeError: - continue - print(f"{r.get('id','')}\t{r.get('kind','')}\t{r.get('work_id','')}\t{r.get('title','')}") -PY - exit 0 -fi - -engine_mode="${SDLC_ENGINE:-auto}" -if [[ "${engine_mode}" == "python" ]] || { [[ "${engine_mode}" == "auto" ]] && _python_engine_available; }; then - result="$(_run_python_accept 2>&1)" || { echo "${result}" >&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 - <<PY -import json, os, tempfile -from pathlib import Path - -root = Path(${TARGET@Q}) -ledger = Path(${ledger@Q}) -stage = Path(${stage@Q}) -work_id = ${WORK_ID@Q} -ids_csv = ${IDS@Q} -discard_rest = ${DISCARD_REST} - -want_ids = set(x.strip() for x in ids_csv.split(",") if x.strip()) if ids_csv else None - -def read_jsonl(path): - if not path.is_file(): - return [] - out = [] - for line in path.read_text(encoding="utf-8").splitlines(): - line = line.strip() - if not line: - continue - try: - out.append(json.loads(line)) - except json.JSONDecodeError: - continue - return out - -def write_jsonl(path, records): - path.parent.mkdir(parents=True, exist_ok=True) - fd, tmp = tempfile.mkstemp(dir=str(path.parent), suffix=".tmp") - try: - with os.fdopen(fd, "w", encoding="utf-8") as fh: - for rec in records: - fh.write(json.dumps(rec, ensure_ascii=False) + "\n") - os.replace(tmp, path) - finally: - if os.path.exists(tmp): - os.unlink(tmp) - -def in_scope(rec): - if work_id and rec.get("work_id") != work_id: - return False - if want_ids is not None and rec.get("id") not in want_ids: - return False - return True - -staged = read_jsonl(stage) -promote = [r for r in staged if in_scope(r)] -by_id = {} -for rec in promote: - by_id[rec["id"]] = rec -promote = list(by_id.values()) - -accepted = {r["id"]: r for r in read_jsonl(ledger)} -for rec in promote: - accepted[rec["id"]] = rec -if promote: - write_jsonl(ledger, accepted.values()) - -if discard_rest: - remaining = [r for r in staged if work_id and r.get("work_id") != work_id] -else: - promoted_ids = {r["id"] for r in promote} - remaining = [r for r in staged if r.get("id") not in promoted_ids] -write_jsonl(stage, remaining) - -print(json.dumps({ - "accepted": [r["id"] for r in promote], - "accepted_count": len(promote), - "staged_remaining": len(remaining), - "ledger": str(ledger), -})) -PY -} - -_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}" -} - -result="$(_accept_shell)" -echo "${result}" -accepted_count="$(printf '%s' "${result}" | python3 -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 482d21ad..c7644940 100755 --- a/sdlc-spdd/scripts/capture-session-memory.sh +++ b/sdlc-spdd/scripts/capture-session-memory.sh @@ -7,6 +7,8 @@ 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" +# shellcheck source=/dev/null source "${_SCRIPT_DIR}/lib/areas.sh" # shellcheck source=/dev/null source "${_SCRIPT_DIR}/lib/milestone.sh" @@ -490,15 +492,7 @@ if [[ -n "${ROADMAP_NOTE}" ]]; then } >> "${roadmap_file}" fi -workflow_script="${HOME}/scripts/sdlc-workflow.sh" -if [[ ! -f "${workflow_script}" ]]; then - workflow_script="${TARGET}/agent-context/sdlc-workflow.sh" -fi -if [[ -f "${workflow_script}" ]]; then - SDLC_ROOT="${TARGET}" - # shellcheck source=/dev/null - source "${workflow_script}" - sdlc_workflow_record_capture "${WORK_ID}" "${PHASE}" -fi +# One engine: record the capture on the workflow state via sdlc-engine. +sdlc_engine_run "${TARGET}" session record-capture --work-id "${WORK_ID}" --phase "${PHASE}" >/dev/null 2>&1 || true 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/hooks/notify-team-registry.example.sh b/sdlc-spdd/scripts/hooks/notify-team-registry.example.sh deleted file mode 100644 index 28040aee..00000000 --- a/sdlc-spdd/scripts/hooks/notify-team-registry.example.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env bash -# Example SDLC_TEAM_REGISTRY_HOOK — copy to agent-context/hooks/notify-team-registry.sh -# and set: export SDLC_TEAM_REGISTRY_HOOK=./agent-context/hooks/notify-team-registry.sh -# -# Args: work_id status phase operation owner updated note -# -# Slack (set SDLC_TEAM_SLACK_WEBHOOK): -# https://api.slack.com/messaging/webhooks - -set -euo pipefail - -work_id="${1:-}" -status="${2:-}" -phase="${3:-}" -operation="${4:-}" -owner="${5:-}" -updated="${6:-}" -note="${7:-}" - -message="SDLC registry: *${work_id}* → ${status} (phase: ${phase}, op: ${operation:-none}, by: ${owner})" -[[ -n "${note}" ]] && message+=" — ${note}" - -if [[ -n "${SDLC_TEAM_SLACK_WEBHOOK:-}" ]]; then - payload="$(printf '{"text":"%s"}' "${message//\"/\\\"}")" - curl -fsS -X POST -H 'Content-type: application/json' \ - --data "${payload}" \ - "${SDLC_TEAM_SLACK_WEBHOOK}" >/dev/null 2>&1 || true -fi - -# Jira (optional — set SDLC_TEAM_JIRA_WEBHOOK to a script or URL your team approves): -if [[ -n "${SDLC_TEAM_JIRA_WEBHOOK:-}" ]]; then - curl -fsS -X POST -H 'Content-type: application/json' \ - --data "{\"work_id\":\"${work_id}\",\"status\":\"${status}\",\"owner\":\"${owner}\",\"note\":\"${note}\"}" \ - "${SDLC_TEAM_JIRA_WEBHOOK}" >/dev/null 2>&1 || true -fi - -echo "${message}" diff --git a/sdlc-spdd/scripts/lib/python.sh b/sdlc-spdd/scripts/lib/python.sh index bd2496ec..da45a6ab 100644 --- a/sdlc-spdd/scripts/lib/python.sh +++ b/sdlc-spdd/scripts/lib/python.sh @@ -74,3 +74,18 @@ resolve_engine_python() { export SDLC_PY return 0 } + +# Run the engine against a project root: sdlc_engine_run <root> <verb> [args...] +# Uses the orchestrator's engine source when present (dogfood), else the +# installed package. Callers decide whether a failure is fatal. +sdlc_engine_run() { + local root="$1" + shift + SDLC_ROOT="${root}" resolve_engine_python || return 1 + if [[ -d "${root}/engine/src/sdlc_engine" ]]; then + PYTHONPATH="${root}/engine/src${PYTHONPATH:+:${PYTHONPATH}}" \ + "${SDLC_PY}" -m sdlc_engine --root "${root}" "$@" + return $? + fi + "${SDLC_PY}" -m sdlc_engine --root "${root}" "$@" +} diff --git a/sdlc-spdd/scripts/lib/skills.sh b/sdlc-spdd/scripts/lib/skills.sh index 13b6223f..bb5b8b5e 100644 --- a/sdlc-spdd/scripts/lib/skills.sh +++ b/sdlc-spdd/scripts/lib/skills.sh @@ -133,129 +133,3 @@ _skills_prepend_frontmatter() { [[ "${body}" == *$'\n' ]] || echo } > "${dest}" } - -# Idempotent: playbooks/ + extensions/ → harness/skills/; drop manifest machinery. -migrate_playbooks_extensions_to_skills() { - local target="$1" - local dry_run="${2:-0}" - local home skills_dir playbooks_dir extensions_dir - home="$(sdlc_home "${target}")" - skills_dir="$(sdlc_skills_dir "${target}")" - playbooks_dir="" - extensions_dir="" - if [[ -d "${home}/playbooks" ]]; then - playbooks_dir="${home}/playbooks" - elif [[ -d "${home}/agent-context/playbooks" ]]; then - playbooks_dir="${home}/agent-context/playbooks" - fi - if [[ -d "${home}/extensions" ]]; then - extensions_dir="${home}/extensions" - elif [[ -d "${home}/agent-context/extensions" ]]; then - extensions_dir="${home}/agent-context/extensions" - fi - - if [[ "${dry_run}" -eq 1 ]]; then - echo "[dry-run] would ensure ${skills_dir}" - else - mkdir -p "${skills_dir}" - fi - - local f base skill phases aliases body dest agent_dir agent_phases - shopt -s nullglob - - if [[ -n "${playbooks_dir}" && -d "${playbooks_dir}" ]]; then - for f in "${playbooks_dir}"/*.md; do - base="$(basename "${f}")" - IFS=$'\t' read -r skill phases <<< "$(_skills_playbook_meta "${base}")" - [[ "${skill}" == "SKIP" ]] && continue - dest="${skills_dir}/${skill}.md" - if [[ -f "${dest}" ]]; then - continue - fi - body="$(<"${f}")" - if [[ "${skill}" == "java-feature" ]]; then - aliases="java" - else - aliases="" - fi - if [[ "${dry_run}" -eq 1 ]]; then - echo "[dry-run] would migrate playbook ${f} -> ${dest}" - else - _skills_prepend_frontmatter "${dest}" "${skill}" "${phases}" "${aliases}" "${body}" - fi - done - fi - - if [[ -n "${extensions_dir}" && -d "${extensions_dir}" ]]; then - for f in "${extensions_dir}"/skills/*.md; do - [[ -f "${f}" ]] || continue - base="$(basename "${f}" .md)" - dest="${skills_dir}/${base}.md" - [[ -f "${dest}" ]] && continue - body="$(<"${f}")" - if [[ "${dry_run}" -eq 1 ]]; then - echo "[dry-run] would migrate extension skill ${f} -> ${dest}" - else - if [[ "${body}" == ---* ]]; then - cp "${f}" "${dest}" - else - _skills_prepend_frontmatter "${dest}" "${base}" "" "" "${body}" - fi - fi - done - - for agent_dir in "${extensions_dir}"/*-agent "${extensions_dir}/_all-agents"; do - [[ -d "${agent_dir}" ]] || continue - agent_phases="$(_skills_agent_folder_phases "$(basename "${agent_dir}")")" - [[ -n "${agent_phases}" ]] || continue - for f in "${agent_dir}"/*.md; do - [[ -f "${f}" ]] || continue - base="$(basename "${f}" .md)" - case "${base}" in - README|manifest|example-manifest-extension) continue ;; - esac - dest="${skills_dir}/${base}.md" - [[ -f "${dest}" ]] && continue - body="$(<"${f}")" - if [[ "${dry_run}" -eq 1 ]]; then - echo "[dry-run] would migrate extension ${f} -> ${dest} (phases: ${agent_phases})" - else - _skills_prepend_frontmatter "${dest}" "${base}" "${agent_phases}" "" "${body}" - fi - done - done - fi - - shopt -u nullglob - - # Move phase-index beside harness core files when still under memory/. - local harness_dir phase_src phase_dest - harness_dir="$(sdlc_harness_dir "${target}")" - phase_dest="${harness_dir}/phase-index.md" - for phase_src in \ - "${home}/agent-context/memory/phase-index.md" \ - "${home}/memory/phase-index.md"; do - [[ -f "${phase_src}" && ! -f "${phase_dest}" ]] || continue - if [[ "${dry_run}" -eq 1 ]]; then - echo "[dry-run] would move ${phase_src} -> ${phase_dest}" - else - mkdir -p "${harness_dir}" - cp "${phase_src}" "${phase_dest}" - fi - done - - if [[ -n "${extensions_dir}" && -d "${extensions_dir}" ]]; then - if [[ "${dry_run}" -eq 1 ]]; then - echo "[dry-run] would remove legacy extensions tree ${extensions_dir}" - else - rm -rf "${extensions_dir}" - fi - fi - if [[ -n "${playbooks_dir}" && -d "${playbooks_dir}" ]]; then - if [[ "${dry_run}" -eq 1 ]]; then - echo "[dry-run] would remove legacy playbooks tree ${playbooks_dir}" - else - rm -rf "${playbooks_dir}" - fi - fi -} diff --git a/sdlc-spdd/scripts/resolve-agent-context.sh b/sdlc-spdd/scripts/resolve-agent-context.sh index 4d5280ed..037ab66c 100755 --- a/sdlc-spdd/scripts/resolve-agent-context.sh +++ b/sdlc-spdd/scripts/resolve-agent-context.sh @@ -340,7 +340,9 @@ add_phase_index_glob() { local pattern="$1" shopt -s nullglob local match - for match in "${FRAMEWORK_HOME}/${pattern}"; do + # ${pattern} is intentionally unquoted so the glob expands. + # shellcheck disable=SC2086 + for match in "${FRAMEWORK_HOME}"/${pattern}; do if [[ -f "${match}" ]]; then add_path "${match}" fi diff --git a/sdlc-spdd/scripts/sdlc-pointer.sh b/sdlc-spdd/scripts/sdlc-pointer.sh deleted file mode 100755 index 3a0f4334..00000000 --- a/sdlc-spdd/scripts/sdlc-pointer.sh +++ /dev/null @@ -1,133 +0,0 @@ -#!/usr/bin/env bash -# Simple pointer manager for SDLC chores/tasks -# Usage: -# source agent-context/sdlc-pointer.sh -# sdlc_init # call on initialization -# sdlc_set_pointer ID # sets pointer -# sdlc_get_pointer # prints pointer or empty -# sdlc_reset_pointer # clears pointer -# run_against_pointer "<expected>" -- <command...> -# -# Default storage: .sdlc/pointer in the repository root. - -if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then - set -euo pipefail -fi - -SDLC_ROOT="${SDLC_ROOT:-$(git -C "${PWD}" rev-parse --show-toplevel 2>/dev/null || pwd)}" -_paths_lib="${SDLC_ROOT}/scripts/lib/paths.sh" -if [[ ! -f "${_paths_lib}" ]]; then - _paths_lib="${SDLC_ROOT}/sdlc-spdd/scripts/lib/paths.sh" -fi -if [[ ! -f "${_paths_lib}" ]]; then - _paths_lib="${SDLC_ROOT}/scripts/sdlc-spdd/lib/paths.sh" -fi -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_ROOT}/.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 f27126a1..00000000 --- a/sdlc-spdd/scripts/sdlc-team-registry.sh +++ /dev/null @@ -1,981 +0,0 @@ -#!/usr/bin/env bash -# Team-visible Work ID registry — committed coordination layer on top of local .sdlc/ state. -# -# Local pointer (.sdlc/pointer) stays machine-private. -# 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}/scripts/lib/paths.sh" -if [[ ! -f "${_paths_lib}" ]]; then - _paths_lib="${SDLC_ROOT}/sdlc-spdd/scripts/lib/paths.sh" -fi -if [[ ! -f "${_paths_lib}" ]]; then - _paths_lib="${SDLC_ROOT}/scripts/sdlc-spdd/lib/paths.sh" -fi -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_ROOT}")" -SDLC_TEAM_REGISTRY_LEGACY="${SDLC_ROOT}/agent-context/work-registry.tsv" -SDLC_TEAM_REGISTRY_LOCK="${SDLC_DIR:-${SDLC_ROOT}/.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="${root}" - 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: <KEY> | 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}" ]] || [[ -f "${SDLC_TEAM_REGISTRY_LEGACY}" ]]; 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_ROOT}/.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 - <<PY -import json -from pathlib import Path -for line in Path(${SDLC_TEAM_REGISTRY_JSONL@Q}).read_text(encoding="utf-8").splitlines(): - line = line.strip() - if not line: - continue - try: - ev = json.loads(line) - except json.JSONDecodeError: - continue - if ev.get("work_id"): - print(json.dumps(ev, ensure_ascii=False)) -PY - return 0 - fi - # Read-only TSV fallback (never written). - if [[ ! -f "${SDLC_TEAM_REGISTRY_LEGACY}" ]]; then - return 0 - fi - python3 - <<PY -import json -from pathlib import Path -tsv = Path(${SDLC_TEAM_REGISTRY_LEGACY@Q}) -for line in tsv.read_text(encoding="utf-8").splitlines(): - if not line or line.startswith("#") or line.startswith("work_id"): - continue - parts = line.split("\t") - while len(parts) < 7: - parts.append("") - ev = { - "event": "legacy-tsv", - "work_id": parts[0], - "status": parts[1], - "phase": parts[2], - "operation": parts[3], - "owner": parts[4], - "ts": parts[5], - "note": parts[6], - } - print(json.dumps(ev, ensure_ascii=False)) -PY -} - -_team_registry_rows() { - _team_registry_init - python3 - <<PY -import json -from pathlib import Path - -by_id = {} -jsonl = Path(${SDLC_TEAM_REGISTRY_JSONL@Q}) -if jsonl.is_file() and jsonl.stat().st_size: - for line in jsonl.read_text(encoding="utf-8").splitlines(): - line = line.strip() - if not line: - continue - try: - ev = json.loads(line) - except json.JSONDecodeError: - continue - wid = ev.get("work_id", "") - if wid: - by_id[wid] = ev -legacy = Path(${SDLC_TEAM_REGISTRY_LEGACY@Q}) -if not by_id and legacy.is_file(): - for line in legacy.read_text(encoding="utf-8").splitlines(): - if not line or line.startswith("#") or line.startswith("work_id"): - continue - parts = line.split("\t") - while len(parts) < 7: - parts.append("") - wid = parts[0] - if not wid: - continue - by_id[wid] = { - "work_id": wid, - "status": parts[1], - "phase": parts[2], - "operation": parts[3], - "owner": parts[4], - "ts": parts[5], - "note": parts[6], - } -for wid in sorted(by_id): - ev = by_id[wid] - print("\t".join([ - ev.get("work_id", ""), - ev.get("status", "available"), - ev.get("phase", ""), - ev.get("operation", ""), - ev.get("owner", ""), - ev.get("ts", ""), - ev.get("note", ""), - ])) -PY -} - -_team_with_registry_lock() { - if command -v flock >/dev/null 2>&1; then - ( - flock -x 200 || exit 1 - "$@" - ) 200>"${SDLC_TEAM_REGISTRY_LOCK}" - else - "$@" - fi -} - - -_team_registry_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 - <<PY -import json -print(json.dumps({ - "event": ${event@Q}, - "work_id": ${work_id@Q}, - "status": ${status@Q}, - "phase": ${phase@Q}, - "operation": ${operation@Q}, - "owner": ${owner@Q}, - "note": ${note@Q}, - "ts": ${updated@Q}, -}, ensure_ascii=False)) -PY -)" - if declare -F sdlc_append_jsonl >/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_ROOT}/.sdlc}/workflows/${work_id}.state" - if [[ -f "${file}" ]]; then - phase="$(grep -m1 '^phase=' "${file}" 2>/dev/null | cut -d= -f2- || true)" - operation="$(grep -m1 '^operation=' "${file}" 2>/dev/null | cut -d= -f2- || true)" - fi - 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="${root}" - 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/<WORK-ID>.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="" - # No agent-context/features moves (storage v3). - - 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="${root}" - 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="${root}/.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="${root}" - 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 <WORK-ID> [--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 <WORK-ID>)" - fi - echo - echo "Hooks: set SDLC_TEAM_REGISTRY_HOOK to agent-context/hooks/notify-team-registry.sh" - echo "Discover all Work IDs: ./scripts/sdlc.sh list-work" -} - -# Locate the workflow manager: same dir as this script (v3 installs place both -# under <home>/scripts/), then <home>/scripts/, then legacy agent-context/. -_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" \ - "${SDLC_ROOT}/agent-context/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 <ID> --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 1ef54b57..00000000 --- a/sdlc-spdd/scripts/sdlc-workflow.sh +++ /dev/null @@ -1,2089 +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 agent-context/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/workflows/<WORK-ID>.state and .sdlc/workflows/<WORK-ID>.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 helpers (installed sdlc-spdd/scripts/lib, legacy -# scripts/sdlc-spdd/lib, or orchestrator scripts/lib). -_wf_readiness_lib="" -if [[ -f "${SDLC_ROOT}/sdlc-spdd/scripts/lib/readiness.sh" ]]; then - _wf_readiness_lib="${SDLC_ROOT}/sdlc-spdd/scripts/lib/readiness.sh" -elif [[ -f "${SDLC_ROOT}/scripts/sdlc-spdd/lib/readiness.sh" ]]; then - _wf_readiness_lib="${SDLC_ROOT}/scripts/sdlc-spdd/lib/readiness.sh" -elif [[ -f "${SDLC_ROOT}/scripts/lib/readiness.sh" ]]; then - _wf_readiness_lib="${SDLC_ROOT}/scripts/lib/readiness.sh" -fi -if [[ -n "${_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_ROOT:-.}/agent-context/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/<requirement>.md" ;; - plan) echo "/sdlc-spdd-plan @spdd/analysis/${work_id:-<WORK-ID>}-analysis.md" ;; - architect) echo "/sdlc-spdd-architect @spdd/canvas/${work_id:-<WORK-ID>}.md" ;; - code) echo "/sdlc-spdd-code @spdd/canvas/${work_id:-<WORK-ID>}.md operation ${operation:-<T##>}" ;; - api-test) echo "/sdlc-spdd-api-test @spdd/canvas/${work_id:-<WORK-ID>}.md" ;; - review) echo "/sdlc-spdd-review @spdd/canvas/${work_id:-<WORK-ID>}.md" ;; - prompt-update) echo "/sdlc-spdd-prompt-update @spdd/canvas/${work_id:-<WORK-ID>}.md" ;; - retro) echo "/sdlc-spdd-retro @spdd/canvas/${work_id:-<WORK-ID>}.md" ;; - sync) echo "/sdlc-spdd-sync @spdd/canvas/${work_id:-<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 "<WORK-ID>" - return 0 - fi - if [[ -z "${phase}" ]]; then - phase="$(_wf_read_state_var "$(_wf_state_file "${work_id}")" phase init)" - fi - if [[ -x "${root}/sdlc-spdd/scripts/start-agent-session.sh" ]]; then - echo "./sdlc-spdd/scripts/start-agent-session.sh --target . --work-id ${work_id} --phase ${phase}" - elif [[ -x "${root}/scripts/sdlc-spdd/start-agent-session.sh" ]]; then - echo "./scripts/sdlc-spdd/start-agent-session.sh --target . --work-id ${work_id} --phase ${phase}" - else - echo "./scripts/start-agent-session.sh --target . --work-id ${work_id} --phase ${phase}" - fi -} - -sdlc_workflow_shell_capture() { - local work_id="${1:-}" - local phase="${2:-}" - 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 \"<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 \"<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 <WORK-ID>\`." - 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 <<EOF -| Field | Value | -|-------|-------| -| Work ID | ${work_id} | -| Workflow status | $([[ "${active}" == "1" ]] && echo active || echo shelved) | -| Phase | ${phase} ($(( $(_wf_phase_index "${phase}") + 1 ))/${#SDLC_PHASE_ORDER[@]}) | -| Readiness | ${readiness:-absent} | -| Jira | ${jira_status:-unknown} | -| Next operation | ${op_title:-${operation:-none}} | -| Assistant command | $(sdlc_workflow_recommended_command "${phase}" "${work_id}" "${operation}") | -| After this phase | \`./scripts/sdlc.sh advance\` | -| Capture (guarded) | \`$(_wf_shell_helper_path) capture --summary "<summary>"\` | -| 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 <WORK-ID> # 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 <WORK-ID>" >&2 - return 2 - fi - sdlc_workflow_sync "${work_id}" >/dev/null - local phase start_script - phase="$(_wf_read_state_var "$(_wf_state_file "${work_id}")" phase init)" - start_script="${SDLC_ROOT}/sdlc-spdd/scripts/start-agent-session.sh" - if [[ ! -x "${start_script}" ]]; then - start_script="${SDLC_ROOT}/scripts/sdlc-spdd/start-agent-session.sh" - fi - if [[ ! -x "${start_script}" ]]; then - start_script="${SDLC_ROOT}/scripts/start-agent-session.sh" - fi - if [[ ! -x "${start_script}" ]]; then - echo "sdlc_workflow_start: start-agent-session.sh not found" >&2 - return 1 - fi - "${start_script}" --target "${SDLC_ROOT}" --work-id "${work_id}" --phase "${phase}" -} - -sdlc_workflow_status_json() { - local work_id="${1:-}" - if [[ -z "${work_id}" ]]; then - work_id="$(sdlc_get_pointer)" - fi - - local pointer phases_json gates_json shelved_json="" - pointer="$(sdlc_get_pointer)" - - phases_json="" - local p first=1 - for p in "${SDLC_PHASE_ORDER[@]}"; do - [[ "${first}" -eq 1 ]] || phases_json+="," - phases_json+="\"${p}\"" - first=0 - done - - gates_json="" - if [[ -n "${work_id}" ]]; then - _wf_ensure_state "${work_id}" - local file phase operation active gi gate state - file="$(_wf_state_file "${work_id}")" - phase="$(_wf_read_state_var "${file}" phase init)" - operation="$(_wf_read_state_var "${file}" operation)" - active="$(_wf_read_state_var "${file}" active 1)" - first=1 - gates_json="{" - for ((gi = 0; gi < ${#SDLC_GATE_NAMES[@]}; gi++)); do - gate="${SDLC_GATE_NAMES[$gi]}" - state="$(_wf_read_state_var "${file}" "gate_${gate}" pending)" - [[ "${first}" -eq 1 ]] || gates_json+="," - gates_json+="\"${gate}\":\"${state}\"" - first=0 - done - gates_json+="}" - - 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 <<EOF -SDLC workflow helper — short paths for humans and agents - - ${helper} # full status (auto-syncs from artifacts) - ${helper} next # concise "what do I do now?" - ${helper} start # open session brief at current phase - ${helper} capture --summary "..." # guarded capture (pointer must match) - ${helper} complete --summary "..." --verify-command CMD --verify-exit N --verify-result pass - ${helper} status --json - - ${helper} resume <WORK-ID> [--phase PHASE] [--force] - ${helper} advance [--to PHASE] [--force] - ${helper} gate <PHASE> [--work-id ID] # check prerequisites to enter a phase (exit 0/1) - ${helper} skip <PHASE> --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 <WORK-ID> [--force] [--branch NAME] [--pr #N] [--jira KEY] - ${helper} release --reason "why" - ${helper} sync-team # mark done/cancelled from canvas Final Status - ${helper} archive <WORK-ID> # 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/<WORK-ID>.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}" - if [[ -f "${root}/scripts/lib/paths.sh" ]]; then - printf '%s' "${root}/scripts/lib/paths.sh" - elif [[ -f "${root}/sdlc-spdd/scripts/lib/paths.sh" ]]; then - printf '%s' "${root}/sdlc-spdd/scripts/lib/paths.sh" - elif [[ -f "${root}/scripts/sdlc-spdd/lib/paths.sh" ]]; then - printf '%s' "${root}/scripts/sdlc-spdd/lib/paths.sh" - fi -} - -_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 - <<PY -import json -from pathlib import Path - -wid = ${work_id@Q} -kinds = set(x.strip() for x in ${kinds_csv@Q}.split(",") if x.strip()) if ${kinds_csv@Q} else None -count = 0 -for path in (Path(${ledger@Q}), Path(${stage@Q})): - if not path.is_file(): - continue - for line in path.read_text(encoding="utf-8").splitlines(): - line = line.strip() - if not line: - continue - try: - rec = json.loads(line) - except json.JSONDecodeError: - continue - if rec.get("work_id") != wid: - continue - if kinds is not None and rec.get("kind") not in kinds: - continue - count += 1 -print(count) -PY -} - -_wf_has_progress_evidence() { - local work_id="$1" - local root="${SDLC_ROOT}" - local n - n="$(_wf_ledger_record_count "${root}" "${work_id}" "")" - [[ "${n:-0}" -gt 0 ]] -} - -_wf_has_retro_evidence() { - local work_id="$1" - local root="${SDLC_ROOT}" - local n - n="$(_wf_ledger_record_count "${root}" "${work_id}" "decision,pitfall,pattern")" - [[ "${n:-0}" -gt 0 ]] -} - -# --- phase gates (requirements-first enforcement) --- - -_wf_python_gate_available() { - # Gate SUT is Python when importable (REF-001). SDLC_ENGINE=shell no longer - # skips that path. SDLC_GATE_ENGINE=shell is the explicit non-SUT fallback. - if [[ "${SDLC_GATE_ENGINE:-}" == "shell" ]]; then - return 1 - fi - if [[ -d "${SDLC_ROOT}/engine/src/sdlc_engine" ]]; then - PYTHONPATH="${SDLC_ROOT}/engine/src${PYTHONPATH:+:${PYTHONPATH}}" \ - python3 -c 'import sdlc_engine' 2>/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 <phase> [--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 - <<PY -import json -from pathlib import Path - -wid = ${work_id@Q} -lines = [] -for path in (Path(${ledger@Q}), Path(${stage@Q})): - if not path.is_file(): - continue - for line in path.read_text(encoding="utf-8").splitlines(): - line = line.strip() - if not line: - continue - try: - rec = json.loads(line) - except json.JSONDecodeError: - continue - if rec.get("work_id") != wid: - continue - title = rec.get("title") or rec.get("body", "")[:80] - lines.append(f"- {rec.get('ts','')}: [{rec.get('kind','')}] {title}") -for ln in lines: - print(ln) -PY -} - -_wf_home_path() { - _wf_ensure_paths_lib || { printf '%s' "${SDLC_ROOT}"; return; } - sdlc_home "${SDLC_ROOT}" -} - -_wf_infer_phase_from_artifacts() { - local work_id="$1" - local root="${SDLC_ROOT}" - local home - home="$(_wf_home_path)" - local inferred="init" - local req analysis canvas review sync_log progress_evidence - - 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 - inferred="analysis" - fi - if [[ -f "${analysis}" ]]; then - inferred="plan" - fi - if [[ -f "${canvas}" ]]; then - inferred="architect" - if grep -Eqi 'ready[[:space:]]+for[[:space:]]+coding' "${canvas}" 2>/dev/null; then - inferred="code" - fi - fi - if [[ -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() { - if [[ -x "${SDLC_ROOT}/sdlc-spdd/scripts/sdlc.sh" ]]; then - echo "./sdlc-spdd/scripts/sdlc.sh" - elif [[ -x "${SDLC_ROOT}/scripts/sdlc-spdd/sdlc.sh" ]]; then - echo "./scripts/sdlc-spdd/sdlc.sh" - else - echo "./scripts/sdlc.sh" - fi -} - -_wf_sync_impl() { - local work_id="$1" - local file stored inferred resolved gate_line gate value current - _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 <WORK-ID>" >&2 - return 2 - fi - - if [[ -z "${phase}" ]]; then - phase="$(_wf_read_state_var "$(_wf_state_file "${work_id}")" phase resume)" - fi - - local capture_script="${SDLC_ROOT}/sdlc-spdd/scripts/capture-session-memory.sh" - if [[ ! -x "${capture_script}" ]]; then - capture_script="${SDLC_ROOT}/scripts/sdlc-spdd/capture-session-memory.sh" - fi - if [[ ! -x "${capture_script}" ]]; then - capture_script="${SDLC_ROOT}/scripts/capture-session-memory.sh" - fi - if [[ ! -x "${capture_script}" ]]; then - echo "sdlc_workflow_capture: capture-session-memory.sh not found" >&2 - return 1 - fi - - run_against_pointer "${work_id}" -- "${capture_script}" \ - --target "${SDLC_ROOT}" \ - --work-id "${work_id}" \ - --phase "${phase}" \ - "${passthrough[@]}" -} - -sdlc_workflow_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 <WORK-ID> # 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_ROOT}/sdlc-spdd/scripts/accept-lessons.sh" - if [[ ! -x "${accept_script}" ]]; then - accept_script="${SDLC_ROOT}/scripts/sdlc-spdd/accept-lessons.sh" - fi - if [[ ! -x "${accept_script}" ]]; then - accept_script="${SDLC_ROOT}/scripts/accept-lessons.sh" - fi - 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/sdlc.sh b/sdlc-spdd/scripts/sdlc.sh index b5ee1ba9..6e69cb6f 100755 --- a/sdlc-spdd/scripts/sdlc.sh +++ b/sdlc-spdd/scripts/sdlc.sh @@ -1,60 +1,31 @@ #!/usr/bin/env bash -# Short entry point for SDLC pointer + workflow helpers. -# Installed to sdlc-spdd/scripts/sdlc.sh in target projects (storage v3); -# lives at scripts/sdlc.sh in the orchestrator repo. +# Thin entry point for the SDLC-SPDD engine (one engine: Python sdlc-engine). +# Installed to sdlc-spdd/scripts/sdlc.sh in target projects; 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 +# Every verb is dispatched to `python -m sdlc_engine`. There is no shell +# workflow implementation and no SDLC_ENGINE switch (REF-003). 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" -elif [[ -f "${SCRIPT_DIR}/../../agent-context/sdlc-workflow.sh" ]]; then - # Legacy sprawled install: <root>/scripts/sdlc-spdd/sdlc.sh. +if [[ "$(basename "$(dirname "${SCRIPT_DIR}")")" == "sdlc-spdd" ]]; then + # Installed target or dogfood home: <root>/sdlc-spdd/scripts/sdlc.sh ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" - WORKFLOW="${ROOT}/agent-context/sdlc-workflow.sh" 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 checkout: <root>/scripts/sdlc.sh + ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" fi -# Script-derived ROOT finds the orch .venv + engine source. --target/--root -# only changes the project the engine operates on (otherwise `next` runs -# against the orch clone and prints "no active Work ID"). +# Script-derived ROOT finds the .venv + engine source. --target/--root only +# changes the project the engine operates on. PROJECT_ROOT="${ROOT}" _forward=() while [[ $# -gt 0 ]]; do case "$1" in --target|--root) - if [[ -z "${2:-}" ]]; then - echo "sdlc: $1 requires a directory path" >&2 - exit 2 - fi - if [[ ! -d "$2" ]]; then - echo "sdlc: $1 is not a directory: $2" >&2 + if [[ -z "${2:-}" || ! -d "${2:-}" ]]; then + echo "sdlc: $1 requires an existing directory" >&2 exit 2 fi PROJECT_ROOT="$(cd "$2" && pwd)" @@ -82,7 +53,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 @@ -90,133 +60,46 @@ if [[ -f "${SCRIPT_DIR}/lib/python.sh" ]]; then elif [[ -f "${ROOT}/scripts/lib/python.sh" ]]; then # shellcheck source=scripts/lib/python.sh source "${ROOT}/scripts/lib/python.sh" +else + echo "sdlc: scripts/lib/python.sh not found next to $0" >&2 + exit 1 fi -_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 - PYTHONPATH="${ROOT}/engine/src${PYTHONPATH:+:${PYTHONPATH}}" \ - "${SDLC_PY}" -c 'import sdlc_engine' 2>/dev/null - return $? - fi - "${SDLC_PY}" -c 'import sdlc_engine' 2>/dev/null -} +if [[ -n "${SDLC_ENGINE:-}" || -n "${SDLC_GATE_ENGINE:-}" ]]; then + echo "sdlc: SDLC_ENGINE / SDLC_GATE_ENGINE were removed; the Python engine is the only engine (REF-003)" >&2 + exit 2 +fi -# 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 -} +resolve_engine_python || exit 1 + +_pypath="" +if [[ -d "${ROOT}/engine/src/sdlc_engine" ]]; then + _pypath="${ROOT}/engine/src${PYTHONPATH:+:${PYTHONPATH}}" +fi -_run_python_engine() { - local args=("$@") - resolve_engine_python || exit 1 - if [[ -d "${ROOT}/engine/src/sdlc_engine" ]]; then - PYTHONPATH="${ROOT}/engine/src${PYTHONPATH:+:${PYTHONPATH}}" \ - exec "${SDLC_PY}" -m sdlc_engine --root "${PROJECT_ROOT}" "${args[@]}" - fi - exec "${SDLC_PY}" -m sdlc_engine --root "${PROJECT_ROOT}" "${args[@]}" -} +if ! PYTHONPATH="${_pypath:-${PYTHONPATH:-}}" "${SDLC_PY}" -c 'import sdlc_engine' 2>/dev/null; then + echo "sdlc: sdlc_engine is not importable by ${SDLC_PY}" >&2 + echo "Install with: ./scripts/setup-engine-venv.sh # Python 3.12 (or: pip install -e ./engine)" >&2 + exit 1 +fi cmd="${1:-next}" if [[ $# -gt 0 ]]; then shift fi -# Python-engine-only commands that must work even when SDLC_ENGINE=shell. -# Normalize hyphen aliases: local-* → local <verb>, db-* → db <verb>. -_py_only_args=() +# Hyphen aliases kept for chat commands and older docs. +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" "$@") - ;; - work-init-from-adf|init-from-adf) - _py_only_args=("work" "init-from-adf" "$@") - ;; - context) - _py_only_args=("context" "$@") - ;; - guide-query) - _py_only_args=("context" "guide-query" "$@") - ;; - installer|console|dashboard) - _py_only_args=("${cmd}" "$@") - ;; -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 - ;; - shell) - ;; - *) - echo "sdlc: unknown SDLC_ENGINE='${ENGINE_MODE}' (use auto|python|shell)" >&2 - exit 2 - ;; + help|-h|--help) args=("--help") ;; + local-*) args=("local" "${cmd#local-}" "$@") ;; + db-*) args=("db" "${cmd#db-}" "$@") ;; + work-init-from-adf|init-from-adf) args=("work" "init-from-adf" "$@") ;; + guide-query) args=("context" "guide-query" "$@") ;; + *) 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 - exit 1 +if [[ -n "${_pypath}" ]]; then + PYTHONPATH="${_pypath}" exec "${SDLC_PY}" -m sdlc_engine --root "${PROJECT_ROOT}" "${args[@]}" fi - -exec "${WORKFLOW}" "${cmd}" "$@" +exec "${SDLC_PY}" -m sdlc_engine --root "${PROJECT_ROOT}" "${args[@]}" diff --git a/sdlc-spdd/scripts/start-agent-session.sh b/sdlc-spdd/scripts/start-agent-session.sh index bdae3e2f..c4892036 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' @@ -102,45 +104,17 @@ TARGET="$(sdlc_resolve_target "${TARGET}")" export SDLC_ROOT="${TARGET}" HOME="$(sdlc_home "${TARGET}")" -pointer_script="${HOME}/scripts/sdlc-pointer.sh" -if [[ ! -f "${pointer_script}" ]]; then - pointer_script="${TARGET}/agent-context/sdlc-pointer.sh" -fi -if [[ -f "${pointer_script}" && -n "${WORK_ID}" ]]; then - SDLC_ROOT="${TARGET}" - # shellcheck source=/dev/null - source "${pointer_script}" - sdlc_set_pointer "${WORK_ID}" >/dev/null -fi - -workflow_script="${HOME}/scripts/sdlc-workflow.sh" -if [[ ! -f "${workflow_script}" ]]; then - workflow_script="${TARGET}/agent-context/sdlc-workflow.sh" -fi -team_script="${HOME}/scripts/sdlc-team-registry.sh" -if [[ ! -f "${team_script}" ]]; then - team_script="${TARGET}/agent-context/sdlc-team-registry.sh" -fi +# One engine: pointer, workflow state, and registry live in sdlc-engine. workflow_brief_md="Workflow tools not installed." 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 + sdlc_engine_run "${TARGET}" pointer set "${WORK_ID}" >/dev/null 2>&1 || true + sdlc_engine_run "${TARGET}" session touch --work-id "${WORK_ID}" --phase "${PHASE}" --milestone "${MILESTONE}" >/dev/null 2>&1 || true + sdlc_engine_run "${TARGET}" sync --work-id "${WORK_ID}" >/dev/null 2>&1 || true + workflow_brief_md="$(sdlc_engine_run "${TARGET}" session brief --work-id "${WORK_ID}" 2>/dev/null || echo "Workflow tools not installed.")" + jira_status="$(sdlc_engine_run "${TARGET}" session jira-status --work-id "${WORK_ID}" 2>/dev/null || true)" + jira_ask_prompt="$(sdlc_engine_run "${TARGET}" session jira-ask --work-id "${WORK_ID}" 2>/dev/null || true)" fi timestamp="$(sdlc_timestamp_iso)" @@ -229,13 +203,16 @@ case "${PHASE}" in ;; esac -# Prefer workflow helper when installed — honors Ready For Coding gate for code phase. +# The engine decides the command — honors the Ready For Coding gate for the code phase. 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}")" +if [[ "${QUIET}" -eq 0 && -n "${WORK_ID}" ]]; then + _rec="$(sdlc_engine_run "${TARGET}" session recommend --work-id "${WORK_ID}" --phase "${PHASE}" 2>/dev/null || true)" + if [[ -n "${_rec}" ]]; then + recommended_command="${_rec}" + fi fi if [[ "${QUIET}" -eq 1 ]]; then recommended_command="Quiet mode: retrieve context via SQLite/Guide/context store; no T## dogfood command." @@ -320,23 +297,7 @@ sqlite_section_md="" sqlite_lookup_loaded=0 if [[ -n "${WORK_ID}" ]]; then _run_db_lookup() { - local out="" - if [[ -x "${TARGET}/scripts/sdlc-spdd/sdlc.sh" ]]; then - out="$( - SDLC_ENGINE=python SDLC_ROOT="${TARGET}" \ - "${TARGET}/scripts/sdlc-spdd/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}" + sdlc_engine_run "${TARGET}" 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 diff --git a/sdlc-spdd/scripts/verify-project-install.sh b/sdlc-spdd/scripts/verify-project-install.sh index 84b3c1d5..1d3e610b 100755 --- a/sdlc-spdd/scripts/verify-project-install.sh +++ b/sdlc-spdd/scripts/verify-project-install.sh @@ -189,12 +189,8 @@ 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 "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 \ Runtime "create work from milestone script" "${HOME_REL}/scripts/create-work-from-milestone.sh" executable \ Runtime "sync roadmap script" "${HOME_REL}/scripts/sync-roadmap-from-spdd.sh" executable \ Runtime "summarize session notes script" "${HOME_REL}/scripts/summarize-session-notes.sh" executable \ @@ -233,6 +229,10 @@ legacy_checks=( Legacy "legacy session briefs" "${legacy_ac}/sessions" absent Legacy "legacy work registry" "${legacy_ac}/${legacy_wr}.tsv" absent Legacy "legacy workflow manager" "${legacy_ac}/sdlc-workflow.sh" absent + Legacy "retired bash workflow twin" "${HOME_REL}/scripts/sdlc-workflow.sh" absent + Legacy "retired bash registry twin" "${HOME_REL}/scripts/sdlc-team-registry.sh" absent + Legacy "retired bash pointer twin" "${HOME_REL}/scripts/sdlc-pointer.sh" absent + Legacy "retired accept-lessons script" "${HOME_REL}/scripts/accept-lessons.sh" absent Legacy "legacy agent-context tree" "${legacy_ac}" absent Legacy "legacy runtime scripts" "scripts/sdlc-spdd" absent Legacy "legacy root requirements" "requirements" absent From 8aa6089bd8ea143f0f04b0a1e27f4156c8707347 Mon Sep 17 00:00:00 2001 From: Cursor Agent <cursoragent@cursor.com> Date: Mon, 14 Sep 2026 00:12:14 +0000 Subject: [PATCH 14/22] REF-002: engine and shell resolve every path through the sdlc-spdd home - adf_work, sync_local, local_sessions, installer (guide/viewer runtime, playground, guide_ops) use Project.home/spdd_dir/requirements_dir/sdlc_dir - detect_target reports any pre-v3 path as unsupported (matches upgrade refusal) - sync-roadmap default path is <home>/ROADMAP.md; shell wrapper delegates to engine - scripts/lib/paths.sh sdlc_home drops the root fallback - adapter installers always rewrite to sdlc-spdd/ paths; session brief hints v3-only Co-authored-by: John Menke <jmjava@gmail.com> --- engine/src/sdlc_engine/adf_work.py | 15 +-- engine/src/sdlc_engine/cli_parser.py | 2 +- engine/src/sdlc_engine/installer/detect.py | 3 +- engine/src/sdlc_engine/installer/guide.py | 3 +- engine/src/sdlc_engine/installer/guide_ops.py | 4 +- .../sdlc_engine/installer/guide_runtime.py | 3 +- .../src/sdlc_engine/installer/playground.py | 20 ++-- .../sdlc_engine/installer/playground_fakes.py | 4 +- .../sdlc_engine/installer/viewer_runtime.py | 3 +- engine/src/sdlc_engine/local_sessions.py | 10 +- engine/src/sdlc_engine/sync_local.py | 6 +- engine/tests_unit/test_storage_migrate.py | 69 ----------- scripts/create-work-from-milestone.sh | 2 +- scripts/install-claude-commands.sh | 15 +-- scripts/install-copilot-prompts.sh | 7 +- scripts/install-cursor-commands.sh | 9 +- scripts/lib/paths.sh | 9 +- scripts/start-agent-session.sh | 18 +-- scripts/sync-roadmap-from-spdd.sh | 111 +++--------------- scripts/validate-reasons-canvas.sh | 4 +- 20 files changed, 73 insertions(+), 244 deletions(-) delete mode 100644 engine/tests_unit/test_storage_migrate.py diff --git a/engine/src/sdlc_engine/adf_work.py b/engine/src/sdlc_engine/adf_work.py index 751d905e..1708fcfd 100644 --- a/engine/src/sdlc_engine/adf_work.py +++ b/engine/src/sdlc_engine/adf_work.py @@ -178,8 +178,10 @@ def init_from_adf( except ValueError: rel_adf = str(path) - canvas_rel = f"spdd/canvas/{wid}.md" - req_rel = f"requirements/milestones/{wid}.md" + canvas_abs = self.project.canvas_path(wid) + req_abs = self.project.milestone_path(wid) + canvas_rel = self.project.rel(canvas_abs) + req_rel = self.project.rel(req_abs) next_cmd = f"/sdlc-spdd-analysis @{req_rel}" if dry_run: @@ -195,8 +197,7 @@ def init_from_adf( dry_run=True, ) - existing_canvas = self.project.root / canvas_rel - if existing_canvas.is_file(): + if canvas_abs.is_file(): raise FileExistsError(f"Canvas already exists: {canvas_rel}") if claim and not dry_run: @@ -429,12 +430,12 @@ def init_from_adf( ] ) - canvas_path = self.project.root / canvas_rel - req_path = self.project.root / req_rel + canvas_path = canvas_abs + req_path = req_abs # Stay-set only (#86) — no committed per-feature mirror folders. canvas_path.parent.mkdir(parents=True, exist_ok=True) req_path.parent.mkdir(parents=True, exist_ok=True) - progress = self.project.root / "spdd" / "memory" / "entries" / "progress.md" + progress = self.project.spdd_dir / "memory" / "entries" / "progress.md" progress.parent.mkdir(parents=True, exist_ok=True) canvas_path.write_text(canvas, encoding="utf-8") diff --git a/engine/src/sdlc_engine/cli_parser.py b/engine/src/sdlc_engine/cli_parser.py index 20db6cdc..86a88145 100644 --- a/engine/src/sdlc_engine/cli_parser.py +++ b/engine/src/sdlc_engine/cli_parser.py @@ -149,7 +149,7 @@ def build_parser() -> argparse.ArgumentParser: sl.set_defaults(func=cmd_sync_links) sr = sub.add_parser("sync-roadmap", help="Refresh ROADMAP.md managed summary from canvases") - sr.add_argument("--roadmap", default="ROADMAP.md") + sr.add_argument("--roadmap", default="ROADMAP.md", help="Path relative to the sdlc-spdd/ home (or absolute)") sr.add_argument("--dry-run", action="store_true") sr.set_defaults(func=cmd_sync_roadmap) diff --git a/engine/src/sdlc_engine/installer/detect.py b/engine/src/sdlc_engine/installer/detect.py index a214d4de..5bd59588 100644 --- a/engine/src/sdlc_engine/installer/detect.py +++ b/engine/src/sdlc_engine/installer/detect.py @@ -39,7 +39,8 @@ def detect_target(target: Path | str) -> dict[str, Any]: if not exists: mode = "missing" recommendation = "create" - elif unsupported_found and not (root / "sdlc-spdd").is_dir(): + elif unsupported_found: + # Matches upgrade-project.sh: any pre-v3 path blocks upgrade until removed. mode = "unsupported" recommendation = "reinit" elif markers_found: diff --git a/engine/src/sdlc_engine/installer/guide.py b/engine/src/sdlc_engine/installer/guide.py index f5e7c686..87926ecb 100644 --- a/engine/src/sdlc_engine/installer/guide.py +++ b/engine/src/sdlc_engine/installer/guide.py @@ -11,6 +11,7 @@ from typing import Any from .guide_runtime import DEFAULT_GIT_URL +from ..project import Project CONFIG_REL = Path(".sdlc") / "guide-config.json" @@ -53,7 +54,7 @@ def config_path(target: Path | str) -> Path: - return Path(target).expanduser().resolve() / CONFIG_REL + return Project(Path(target).expanduser().resolve()).sdlc_dir / CONFIG_REL.name def _looks_like_guide_home(path: Path) -> bool: diff --git a/engine/src/sdlc_engine/installer/guide_ops.py b/engine/src/sdlc_engine/installer/guide_ops.py index ee46fbaa..ca58c9d2 100644 --- a/engine/src/sdlc_engine/installer/guide_ops.py +++ b/engine/src/sdlc_engine/installer/guide_ops.py @@ -9,7 +9,7 @@ from pathlib import Path from typing import Any -from sdlc_engine.project import HOME_DIR_NAME +from sdlc_engine.project import Project def _guide_base(host: str, port: int) -> str: @@ -173,7 +173,7 @@ def purge_all_content_elements_docker( def default_operator_directories(orchestrator_root: Path | str) -> list[str]: root = Path(orchestrator_root).expanduser().resolve() - home = root / HOME_DIR_NAME if (root / HOME_DIR_NAME).is_dir() else root + home = Project(root).home return [ str(home / "spdd" / "canvas"), str(home / "spdd" / "analysis"), diff --git a/engine/src/sdlc_engine/installer/guide_runtime.py b/engine/src/sdlc_engine/installer/guide_runtime.py index 619708d6..8bde92c3 100644 --- a/engine/src/sdlc_engine/installer/guide_runtime.py +++ b/engine/src/sdlc_engine/installer/guide_runtime.py @@ -11,6 +11,7 @@ from typing import Any from ..io_util import clear_file, load_json_dict, save_json_dict +from ..project import Project from ..timeutil import utc_now as _utc_now from .process_util import pid_alive as _pid_alive from .process_util import run_cmd @@ -55,7 +56,7 @@ def relax_codegen_gradle_network_timeout(home: Path | str) -> bool: def runtime_path(target: Path | str) -> Path: - return Path(target).expanduser().resolve() / RUNTIME_REL + return Project(Path(target).expanduser().resolve()).sdlc_dir / RUNTIME_REL.name def _load_runtime(target: Path | str) -> dict[str, Any]: diff --git a/engine/src/sdlc_engine/installer/playground.py b/engine/src/sdlc_engine/installer/playground.py index 615f36d5..efcf0dce 100644 --- a/engine/src/sdlc_engine/installer/playground.py +++ b/engine/src/sdlc_engine/installer/playground.py @@ -8,6 +8,7 @@ from typing import Any from ..io_util import save_json_dict +from ..project import Project from ..timeutil import utc_now from .runner import orchestrator_root @@ -23,11 +24,11 @@ def default_playground_dir(orch: Path | str | None = None) -> Path: root = Path(orch) if orch is not None else orchestrator_root() - return root / ".sdlc" / PLAYGROUND_DIRNAME + return Project(root).sdlc_dir / PLAYGROUND_DIRNAME def is_playground(target: Path | str) -> bool: - return (Path(target).expanduser().resolve() / ".sdlc" / MARKER_NAME).is_file() + return (Project(Path(target).expanduser().resolve()).sdlc_dir / MARKER_NAME).is_file() def materialize_playground( @@ -48,13 +49,14 @@ def materialize_playground( root.mkdir(parents=True, exist_ok=True) ts = utc_now() - sdlc = root / ".sdlc" - memory = root / "spdd" / "memory" - canvas_dir = root / "spdd" / "canvas" - analysis_dir = root / "spdd" / "analysis" - reviews_dir = root / "spdd" / "reviews" - milestones = root / "requirements" / "milestones" - adf_dir = root / "adf" + project = Project(root) + sdlc = project.sdlc_dir + memory = project.spdd_dir / "memory" + canvas_dir = project.spdd_dir / "canvas" + analysis_dir = project.spdd_dir / "analysis" + reviews_dir = project.spdd_dir / "reviews" + milestones = project.requirements_dir / "milestones" + adf_dir = root / "adf" # ADF viewer store lives at the project root sessions = sdlc / "sessions" staged = sdlc / "staged" backup = root / ".sdlc-spdd-upgrade-backups" / "20260815T120000Z" diff --git a/engine/src/sdlc_engine/installer/playground_fakes.py b/engine/src/sdlc_engine/installer/playground_fakes.py index 524e4326..ee7d6fae 100644 --- a/engine/src/sdlc_engine/installer/playground_fakes.py +++ b/engine/src/sdlc_engine/installer/playground_fakes.py @@ -32,11 +32,11 @@ def runtime_path(target: Path | str) -> Path: - return Path(target).expanduser().resolve() / ".sdlc" / RUNTIME_NAME + return Project(Path(target).expanduser().resolve()).sdlc_dir / RUNTIME_NAME def fake_guide_home(target: Path | str) -> Path: - return Path(target).expanduser().resolve() / ".sdlc" / FAKE_GUIDE_DIRNAME + return Project(Path(target).expanduser().resolve()).sdlc_dir / FAKE_GUIDE_DIRNAME def issue_refs(work_id: str) -> tuple[str, str]: diff --git a/engine/src/sdlc_engine/installer/viewer_runtime.py b/engine/src/sdlc_engine/installer/viewer_runtime.py index 4f621f3d..3bfd61ef 100644 --- a/engine/src/sdlc_engine/installer/viewer_runtime.py +++ b/engine/src/sdlc_engine/installer/viewer_runtime.py @@ -14,6 +14,7 @@ from urllib.parse import quote from ..io_util import clear_file, load_json_dict, save_json_dict +from ..project import Project from ..timeutil import utc_now as _utc_now from .process_util import pid_alive as _pid_alive from .process_util import run_cmd @@ -25,7 +26,7 @@ def runtime_path(target: Path | str) -> Path: - return Path(target).expanduser().resolve() / RUNTIME_REL + return Project(Path(target).expanduser().resolve()).sdlc_dir / RUNTIME_REL.name def _load_runtime(target: Path | str) -> dict[str, Any]: diff --git a/engine/src/sdlc_engine/local_sessions.py b/engine/src/sdlc_engine/local_sessions.py index 6f943b66..2b9871f0 100644 --- a/engine/src/sdlc_engine/local_sessions.py +++ b/engine/src/sdlc_engine/local_sessions.py @@ -147,8 +147,8 @@ def _next_number(self, prefix: str) -> int: max_n = 0 pattern = re.compile(rf"^{re.escape(prefix)}-(\d+)-", re.IGNORECASE) roots = [ - self.project.root / "spdd" / "canvas", - self.project.root / "requirements" / "milestones", + self.project.spdd_dir / "canvas", + self.project.requirements_dir / "milestones", self.root, ] for base in roots: @@ -331,7 +331,7 @@ def capture(self, summary: str, *, session_id: str | None = None) -> LocalSessio # Optional daily session-notes tag (committed) — only when explicitly wanted via env if os.environ.get("SDLC_LOCAL_SESSION_NOTES", "0") == "1": day = utc_date() - notes_dir = self.project.root / "session-notes" + notes_dir = self.project.session_notes_dir notes_dir.mkdir(parents=True, exist_ok=True) path = notes_dir / f"{day}.md" block = f"\n### local:{sid}\n\n{summary.strip()}\n" @@ -457,7 +457,7 @@ def promote( notes_text = self._notes_path(sid).read_text(encoding="utf-8").strip() # Milestone requirement - req_dir = self.project.root / "requirements" / "milestones" + req_dir = self.project.requirements_dir / "milestones" req_dir.mkdir(parents=True, exist_ok=True) req_path = req_dir / f"{work_id}.md" req_path.write_text( @@ -516,7 +516,7 @@ def promote( ) # Canvas - canvas_dir = self.project.root / "spdd" / "canvas" + canvas_dir = self.project.spdd_dir / "canvas" canvas_dir.mkdir(parents=True, exist_ok=True) work_type_label = { "FEAT": "Feature", diff --git a/engine/src/sdlc_engine/sync_local.py b/engine/src/sdlc_engine/sync_local.py index 49bfe9f2..30eafcd5 100644 --- a/engine/src/sdlc_engine/sync_local.py +++ b/engine/src/sdlc_engine/sync_local.py @@ -41,8 +41,8 @@ def __init__(self, project: Project | None = None, registry: TeamRegistry | None def sync_roadmap(self, *, roadmap: str = "ROADMAP.md", dry_run: bool = False) -> str: roadmap_path = Path(roadmap) if not roadmap_path.is_absolute(): - roadmap_path = self.project.root / roadmap_path - canvas_dir = self.project.root / "spdd" / "canvas" + roadmap_path = self.project.home / roadmap_path + canvas_dir = self.project.spdd_dir / "canvas" rows: list[str] = [] files = sorted(canvas_dir.glob("*.md")) if canvas_dir.is_dir() else [] for path in files: @@ -101,7 +101,7 @@ def check_links(self, work_id: str | None = None) -> list[DriftFinding]: links = collect_links(self.project, wid, rows.get(wid)) findings.extend(self._findings_for(links)) # ROADMAP staleness: if markers missing or summary older than newest canvas mtime — soft check - roadmap = self.project.root / "ROADMAP.md" + roadmap = self.project.roadmap_path if roadmap.is_file(): text = roadmap.read_text(encoding="utf-8") if START_MARKER not in text or END_MARKER not in text: diff --git a/engine/tests_unit/test_storage_migrate.py b/engine/tests_unit/test_storage_migrate.py deleted file mode 100644 index 82f3acde..00000000 --- a/engine/tests_unit/test_storage_migrate.py +++ /dev/null @@ -1,69 +0,0 @@ -"""Storage v3 migration: legacy fixture → ledger records + export.""" - -from __future__ import annotations - -from pathlib import Path - -from sdlc_engine.lessons_ledger import LessonsLedger -from sdlc_engine.project import Project -from sdlc_engine.storage_migrate import StorageMigration - - -def _legacy_fixture(root: Path) -> None: - mem = root / "agent-context" / "memory" - mem.mkdir(parents=True, exist_ok=True) - (mem / "context-index.md").write_text( - """# Context Index - -| Area | Kind | Work ID | Phase | Timestamp | Source | Entry | -|------|------|---------|-------|-----------|--------|-------| -| engine | pitfall | FEAT-LEG | code | 2026-08-08T00:00:00Z | test | Legacy pitfall text | -""", - encoding="utf-8", - ) - (mem / "known-pitfalls.md").write_text( - "## FEAT-LEG — 2026-08-08\n\n- Area: engine\n\nBody from file\n", - encoding="utf-8", - ) - (root / "agent-context" / "work-registry.tsv").write_text( - "# header\nwork_id\tstatus\tphase\toperation\towner\tupdated\tnote\n" - "FEAT-LEG\tavailable\tinit\t\tdev\t2026-08-08T00:00:00Z\t\n", - encoding="utf-8", - ) - - -def test_migration_dry_run_detects_legacy(tmp_path: Path) -> None: - _legacy_fixture(tmp_path) - mig = StorageMigration(Project(tmp_path)) - detect = mig.detect() - assert detect["needs_migration"] is True - assert "agent-context/memory" in detect["legacy_present"] - - -def test_migration_reads_registry_tsv_at_root_when_home_is_sdlc_spdd(tmp_path: Path) -> None: - home = tmp_path / "sdlc-spdd" - home.mkdir() - (tmp_path / "agent-context").mkdir() - (tmp_path / "agent-context" / "work-registry.tsv").write_text( - "# header\nwork_id\tstatus\tphase\toperation\towner\tupdated\tnote\n" - "FEAT-ROOT\tavailable\tinit\t\tdev\t2026-08-08T00:00:00Z\t\n", - encoding="utf-8", - ) - mig = StorageMigration(Project(tmp_path)) - out = mig.run(dry_run=False) - assert out["ok"] is True - assert out["records_migrated"]["registry_tsv"] == 1 - assert (home / ".sdlc" / "storage-v3-migrated").is_file() - - -def test_migration_appends_records_and_exports(tmp_path: Path) -> None: - _legacy_fixture(tmp_path) - mig = StorageMigration(Project(tmp_path)) - out = mig.run(dry_run=False) - assert out["ok"] is True - assert out["records_migrated"]["context_index"] >= 1 - ledger = LessonsLedger(Project(tmp_path)) - ids = ledger.accepted_ids() - assert any("FEAT-LEG" in i for i in ids) - assert (tmp_path / ".sdlc" / "storage-v3-migrated").is_file() - assert not (tmp_path / "agent-context" / "memory").exists() diff --git a/scripts/create-work-from-milestone.sh b/scripts/create-work-from-milestone.sh index 23ad00af..817a2a66 100755 --- a/scripts/create-work-from-milestone.sh +++ b/scripts/create-work-from-milestone.sh @@ -481,7 +481,7 @@ done if ((${#created_work_ids[@]} > 0)); then echo - echo "Next SPDD prompts (see docs/sdlc-spdd/spdd-prompt-standard.md):" + echo "Next SPDD prompts (see sdlc-spdd/docs/spdd-prompt-standard.md):" for work_id in "${created_work_ids[@]}"; do req_ref="@${requirement_parent_rel}/${work_id}.md" echo diff --git a/scripts/install-claude-commands.sh b/scripts/install-claude-commands.sh index 4af77f19..f38bd5ff 100755 --- a/scripts/install-claude-commands.sh +++ b/scripts/install-claude-commands.sh @@ -49,19 +49,12 @@ MEMORY_DEST="${TARGET}/CLAUDE.md" # shellcheck source=lib/framework-install.sh source "${SCRIPT_DIR}/lib/framework-install.sh" -# Storage v3 targets keep IDE stubs at the repo root but reference paths under -# the single-folder home sdlc-spdd/. -REWRITE_V3=0 -[[ -d "${TARGET}/sdlc-spdd" ]] && REWRITE_V3=1 +# IDE stubs stay at the repo root but reference paths under the sdlc-spdd/ home. -# Rewrite a template into a temp copy when installing into a v3 target so the -# adapter content references sdlc-spdd/ paths (upsert included). +# Rewrite a template into a temp copy so the adapter content references +# sdlc-spdd/ paths (upsert included). prepare_adapter_src() { local src="$1" - if [[ "${REWRITE_V3}" -eq 0 ]]; then - printf '%s' "${src}" - return - fi local tmp tmp="$(mktemp)" cp "${src}" "${tmp}" @@ -85,7 +78,7 @@ copy_if_missing() { fi mkdir -p "$(dirname "${dest}")" cp "${src}" "${dest}" - [[ "${REWRITE_V3}" -eq 1 ]] && framework_rewrite_adapter_paths "${dest}" + framework_rewrite_adapter_paths "${dest}" installed+=("${dest}") } diff --git a/scripts/install-copilot-prompts.sh b/scripts/install-copilot-prompts.sh index 4a74f957..5fc4890e 100755 --- a/scripts/install-copilot-prompts.sh +++ b/scripts/install-copilot-prompts.sh @@ -48,10 +48,7 @@ PROMPTS_DEST="${TARGET}/.github/prompts" # shellcheck source=lib/framework-install.sh source "${SCRIPT_DIR}/lib/framework-install.sh" -# Storage v3 targets keep IDE stubs at the repo root but reference paths under -# the single-folder home sdlc-spdd/. -REWRITE_V3=0 -[[ -d "${TARGET}/sdlc-spdd" ]] && REWRITE_V3=1 +# IDE stubs stay at the repo root but reference paths under the sdlc-spdd/ home. installed=() skipped=() @@ -65,7 +62,7 @@ copy_if_missing() { fi mkdir -p "$(dirname "${dest}")" cp "${src}" "${dest}" - [[ "${REWRITE_V3}" -eq 1 ]] && framework_rewrite_adapter_paths "${dest}" + framework_rewrite_adapter_paths "${dest}" installed+=("${dest}") } diff --git a/scripts/install-cursor-commands.sh b/scripts/install-cursor-commands.sh index 261e844e..8a69aefc 100755 --- a/scripts/install-cursor-commands.sh +++ b/scripts/install-cursor-commands.sh @@ -51,10 +51,7 @@ mkdir -p "${RULES_DEST}" # shellcheck source=lib/framework-install.sh source "${SCRIPT_DIR}/lib/framework-install.sh" -# Storage v3 targets keep IDE stubs at the repo root but reference paths under -# the single-folder home sdlc-spdd/. -REWRITE_V3=0 -[[ -d "${TARGET}/sdlc-spdd" ]] && REWRITE_V3=1 +# IDE stubs stay at the repo root but reference paths under the sdlc-spdd/ home. installed=() skipped=() @@ -67,7 +64,7 @@ for src in "${REPO_ROOT}"/templates/cursor/*.md; do continue fi cp "${src}" "${out}" - [[ "${REWRITE_V3}" -eq 1 ]] && framework_rewrite_adapter_paths "${out}" + framework_rewrite_adapter_paths "${out}" installed+=("${out}") done @@ -79,7 +76,7 @@ for src in "${REPO_ROOT}"/templates/cursor/rules/*.mdc; do continue fi cp "${src}" "${out}" - [[ "${REWRITE_V3}" -eq 1 ]] && framework_rewrite_adapter_paths "${out}" + framework_rewrite_adapter_paths "${out}" installed+=("${out}") done diff --git a/scripts/lib/paths.sh b/scripts/lib/paths.sh index 2e6956bf..3155ccdb 100644 --- a/scripts/lib/paths.sh +++ b/scripts/lib/paths.sh @@ -49,18 +49,15 @@ sdlc_root() { git -C "${PWD}" rev-parse --show-toplevel 2>/dev/null || pwd } -# Framework home: SDLC_HOME > <root>/sdlc-spdd if dir > root. +# Framework home (storage v3): SDLC_HOME, else <root>/sdlc-spdd. Mirrors +# sdlc_engine.project.Project.home — there is no root-layout fallback. sdlc_home() { local root="${1:-$(sdlc_root)}" if [[ -n "${SDLC_HOME:-}" ]]; then printf '%s' "${SDLC_HOME}" return 0 fi - if [[ -d "${root}/sdlc-spdd" ]]; then - printf '%s' "${root}/sdlc-spdd" - else - printf '%s' "${root}" - fi + printf '%s' "${root}/sdlc-spdd" } sdlc_runtime_dir() { diff --git a/scripts/start-agent-session.sh b/scripts/start-agent-session.sh index c4892036..e3fb9f31 100755 --- a/scripts/start-agent-session.sh +++ b/scripts/start-agent-session.sh @@ -248,24 +248,14 @@ fi active_milestone="$(resolve_milestone "${HOME}" "${WORK_ID}" "${MILESTONE}" relative || true)" today_note_rel="session-notes/$(sdlc_timestamp_day).md" -# Command + docs hints in the brief must match the actual layout: v3 installs -# use sdlc-spdd/scripts + sdlc-spdd/docs; the orchestrator repo keeps scripts/. -if [[ "${HOME}" != "${TARGET}" ]]; then - scripts_hint="./sdlc-spdd/scripts" - sdlc_sh_hint="./sdlc-spdd/scripts/sdlc.sh" - docs_hint="sdlc-spdd/docs" -else - scripts_hint="./scripts" - sdlc_sh_hint="./scripts/sdlc.sh" - docs_hint="docs/sdlc-spdd" - [[ -d "${TARGET}/docs/sdlc-spdd" ]] || docs_hint="docs" -fi +# Command + docs hints in the brief match the storage v3 layout (sdlc-spdd/ home). +scripts_hint="./sdlc-spdd/scripts" +sdlc_sh_hint="./sdlc-spdd/scripts/sdlc.sh" +docs_hint="sdlc-spdd/docs" resolve_script="" if [[ -x "${HOME}/scripts/resolve-agent-context.sh" ]]; then resolve_script="${HOME}/scripts/resolve-agent-context.sh" -elif [[ -x "${TARGET}/scripts/sdlc-spdd/resolve-agent-context.sh" ]]; then - resolve_script="${TARGET}/scripts/sdlc-spdd/resolve-agent-context.sh" elif [[ -x "$(dirname "${BASH_SOURCE[0]}")/resolve-agent-context.sh" ]]; then resolve_script="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/resolve-agent-context.sh" fi diff --git a/scripts/sync-roadmap-from-spdd.sh b/scripts/sync-roadmap-from-spdd.sh index 3b905337..f3a79703 100755 --- a/scripts/sync-roadmap-from-spdd.sh +++ b/scripts/sync-roadmap-from-spdd.sh @@ -1,19 +1,24 @@ #!/usr/bin/env bash +# Thin wrapper: the roadmap summary is generated by the Python engine +# (sdlc-engine sync-roadmap). Kept so existing prompts and docs keep working. set -euo pipefail _SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=/dev/null source "${_SCRIPT_DIR}/lib/common.sh" +# shellcheck source=/dev/null +source "${_SCRIPT_DIR}/lib/python.sh" usage() { cat <<'EOF' Usage: sync-roadmap-from-spdd.sh [--target <path>] [--roadmap <file>] [--dry-run] -Update a managed SDLC-SPDD summary section in ROADMAP.md from spdd/canvas/*.md. - -The script preserves all roadmap content outside these markers: +Update the managed SDLC-SPDD summary section in sdlc-spdd/ROADMAP.md from +sdlc-spdd/spdd/canvas/*.md. Content outside these markers is preserved: <!-- SDLC-SPDD-ROADMAP-SUMMARY:START --> <!-- SDLC-SPDD-ROADMAP-SUMMARY:END --> + +--roadmap is relative to the sdlc-spdd/ home unless absolute. EOF } @@ -48,101 +53,13 @@ while [[ $# -gt 0 ]]; do done TARGET="$(sdlc_resolve_target "${TARGET}")" -if [[ "${ROADMAP}" != /* ]]; then - ROADMAP="${TARGET}/${ROADMAP}" -fi -canvas_dir="${TARGET}/spdd/canvas" -start_marker="<!-- SDLC-SPDD-ROADMAP-SUMMARY:START -->" -end_marker="<!-- SDLC-SPDD-ROADMAP-SUMMARY:END -->" -tmp_summary="$(mktemp)" -tmp_roadmap="$(mktemp)" +engine_args=(sync-roadmap --roadmap "${ROADMAP}") +[[ "${DRY_RUN}" -eq 1 ]] && engine_args+=(--dry-run) +sdlc_engine_run "${TARGET}" "${engine_args[@]}" -field_value() { - local label="$1" - local file="$2" - sed -n "s/^- ${label}:[[:space:]]*//p" "${file}" | sed -n '1p' -} - -{ - echo - echo "## SDLC-SPDD Work Summary" +if [[ "${DRY_RUN}" -eq 0 ]]; then echo - echo "Generated: $(sdlc_timestamp_iso)" - echo - echo "| Work ID | Title | Type | Status | Milestone | Source | Canvas |" - echo "|---------|-------|------|--------|-----------|--------|--------|" -} > "${tmp_summary}" - -shopt -s nullglob -canvas_files=("${canvas_dir}"/*.md) -shopt -u nullglob - -if ((${#canvas_files[@]} == 0)); then - echo "| none | - | - | - | - | - |" >> "${tmp_summary}" -else - for file in "${canvas_files[@]}"; do - work_id="$(field_value "Work ID" "${file}")" - work_type="$(field_value "Work Type" "${file}")" - status="$(field_value "Status" "${file}")" - milestone="$(field_value "Milestone" "${file}")" - source_url="$(field_value "Source URL" "${file}")" - title="$(sed -n 's/^# REASONS Canvas: .* - //p' "${file}" | sed -n '1p')" - rel="${file#${TARGET}/}" - work_id="${work_id:-$(basename "${file}" .md)}" - title="${title:-TBD}" - work_type="${work_type:-TBD}" - status="${status:-TBD}" - milestone="${milestone:-TBD}" - source_url="${source_url:-TBD}" - echo "| ${work_id} | ${title} | ${work_type} | ${status} | ${milestone} | ${source_url} | ${rel} |" >> "${tmp_summary}" - done -fi - -if [[ "${DRY_RUN}" -eq 1 ]]; then - echo "${start_marker}" - cat "${tmp_summary}" - echo "${end_marker}" - rm -f "${tmp_summary}" "${tmp_roadmap}" - exit 0 -fi - -if [[ ! -f "${ROADMAP}" ]]; then - { - echo "# Roadmap" - echo - } > "${ROADMAP}" -fi - -if grep -Fq "${start_marker}" "${ROADMAP}" && grep -Fq "${end_marker}" "${ROADMAP}"; then - awk -v start="${start_marker}" -v end="${end_marker}" -v summary="${tmp_summary}" ' - $0 == start { - print - while ((getline line < summary) > 0) { - print line - } - in_block = 1 - next - } - $0 == end { - in_block = 0 - print - next - } - !in_block { print } - ' "${ROADMAP}" > "${tmp_roadmap}" - mv "${tmp_roadmap}" "${ROADMAP}" -else - { - echo - echo "${start_marker}" - cat "${tmp_summary}" - echo "${end_marker}" - } >> "${ROADMAP}" + echo "Planning review prompt (see sdlc-spdd/docs/planning-prompt-standard.md):" + echo " Read @sdlc-spdd/ROADMAP.md. Review the SDLC-SPDD Work Summary table. Does Current Focus match the active Work ID and phase?" fi - -rm -f "${tmp_summary}" "${tmp_roadmap}" -echo "Updated roadmap summary: ${ROADMAP}" -echo -echo "Planning review prompt (see docs/sdlc-spdd/planning-prompt-standard.md):" -echo " Read @ROADMAP.md. Review the SDLC-SPDD Work Summary table. Does Current Focus match the active Work ID and phase?" diff --git a/scripts/validate-reasons-canvas.sh b/scripts/validate-reasons-canvas.sh index 94695b44..c83794f9 100755 --- a/scripts/validate-reasons-canvas.sh +++ b/scripts/validate-reasons-canvas.sh @@ -115,7 +115,7 @@ validate_file() { printf ' - %s\n' "${missing[@]}" >&2 work_id="$(basename "${file}" .md)" echo >&2 - echo "SPDD fix prompts (see docs/sdlc-spdd/spdd-prompt-standard.md):" >&2 + echo "SPDD fix prompts (see sdlc-spdd/docs/spdd-prompt-standard.md):" >&2 echo " /sdlc-spdd-plan @requirements/milestones/${work_id}.md @milestone-1.md" >&2 echo " (milestone work) or /sdlc-spdd-plan @requirements/<file>.md (ad-hoc)" >&2 echo " Or complete missing REASONS sections in ${file} before /sdlc-spdd-architect." >&2 @@ -128,7 +128,7 @@ validate_file() { work_id="$(basename "${file}" .md)" echo "Valid canvas: ${file}" echo - echo "Next SPDD prompts (see docs/sdlc-spdd/spdd-prompt-standard.md):" + echo "Next SPDD prompts (see sdlc-spdd/docs/spdd-prompt-standard.md):" echo " /sdlc-spdd-architect @spdd/canvas/${work_id}.md" echo " Then when Ready For Coding: /sdlc-spdd-code @spdd/canvas/${work_id}.md operation T01" return 0 From e87791496cb5261fef484441d503fcd31da8a39b Mon Sep 17 00:00:00 2001 From: Cursor Agent <cursoragent@cursor.com> Date: Mon, 14 Sep 2026 00:12:14 +0000 Subject: [PATCH 15/22] TEST: unit/integration suites target the sdlc-spdd home layout - Route fixture paths through <root>/sdlc-spdd - Drop storage_migrate/agent_context_upgrade tests and legacy seeds - detect tests: v3 markers upgrade, pre-v3 layout unsupported - public import test targets sdlc_engine.commands Co-authored-by: John Menke <jmjava@gmail.com> --- engine/tests_integration/test_installer.py | 25 +++-- .../test_installer_adf_api.py | 19 ++-- .../test_installer_adf_live.py | 2 +- .../test_installer_coverage_gaps.py | 1 - .../test_installer_dashboard_api.py | 14 +-- .../test_installer_guide_api.py | 6 +- .../test_installer_jira_api.py | 15 ++- .../test_installer_persistence_api.py | 4 +- .../test_installer_playground_api.py | 2 +- engine/tests_integration/test_viewer_store.py | 1 - engine/tests_unit/test_adf_templates.py | 12 +-- engine/tests_unit/test_adf_work.py | 20 ++-- engine/tests_unit/test_canvas.py | 4 +- engine/tests_unit/test_cli.py | 20 ++-- .../test_context_store_guide_parity.py | 10 +- .../test_context_store_persistence.py | 13 ++- engine/tests_unit/test_db.py | 4 +- engine/tests_unit/test_db_graph_v5.py | 22 ++--- engine/tests_unit/test_db_schema_upgrade.py | 14 +-- engine/tests_unit/test_engine_shared.py | 8 +- engine/tests_unit/test_guide_client.py | 1 - engine/tests_unit/test_hard_review_gaps.py | 68 +++---------- .../tests_unit/test_installer_playground.py | 32 +++--- .../test_installer_runtime_units.py | 10 +- .../test_installer_templates_api.py | 4 +- engine/tests_unit/test_integration_config.py | 3 +- engine/tests_unit/test_issues_mocked.py | 18 ++-- engine/tests_unit/test_jira_format.py | 20 ++-- engine/tests_unit/test_local_sessions.py | 28 +++--- engine/tests_unit/test_persistence_options.py | 45 +++++---- engine/tests_unit/test_process_metrics.py | 14 +-- engine/tests_unit/test_registry_archive.py | 30 +++--- .../test_remaining_cleanup_slices.py | 97 +++---------------- engine/tests_unit/test_sunset.py | 10 +- engine/tests_unit/test_sync_links.py | 12 +-- engine/tests_unit/test_workflow.py | 4 +- engine/tests_unit/test_workflow_gates.py | 10 +- 37 files changed, 253 insertions(+), 369 deletions(-) diff --git a/engine/tests_integration/test_installer.py b/engine/tests_integration/test_installer.py index 15610822..3bb18cf1 100644 --- a/engine/tests_integration/test_installer.py +++ b/engine/tests_integration/test_installer.py @@ -32,13 +32,24 @@ def test_detect_fresh_directory(tmp_path: Path) -> None: def test_detect_upgrade_markers(tmp_path: Path) -> None: - marker = tmp_path / "scripts" / "sdlc-spdd" / "sdlc.sh" + marker = tmp_path / "sdlc-spdd" / "scripts" / "sdlc.sh" marker.parent.mkdir(parents=True) marker.write_text("#!/bin/bash\n", encoding="utf-8") info = detect_target(tmp_path) assert info["mode"] == "upgrade" assert info["recommendation"] == "upgrade" - assert "scripts/sdlc-spdd/sdlc.sh" in info["markers"] + assert "sdlc-spdd/scripts/sdlc.sh" in info["markers"] + + +def test_detect_pre_v3_layout_is_unsupported(tmp_path: Path) -> None: + marker = tmp_path / "scripts" / "sdlc-spdd" / "sdlc.sh" + marker.parent.mkdir(parents=True) + marker.write_text("#!/bin/bash\n", encoding="utf-8") + info = detect_target(tmp_path) + assert info["mode"] == "unsupported" + assert info["recommendation"] == "reinit" + assert "scripts/sdlc-spdd/sdlc.sh" in info["unsupported_markers"] + assert info["markers"] == [] def test_detect_missing(tmp_path: Path) -> None: @@ -91,7 +102,7 @@ def test_api_run_dry_install(tmp_path: Path) -> None: def test_api_run_dry_upgrade(tmp_path: Path) -> None: - marker = tmp_path / "agent-context" / "sdlc-workflow.sh" + marker = tmp_path / "sdlc-spdd" / "scripts" / "sdlc.sh" marker.parent.mkdir(parents=True) marker.write_text("#!/bin/bash\n", encoding="utf-8") app = create_app(tmp_path) @@ -111,9 +122,9 @@ def test_api_run_dry_upgrade(tmp_path: Path) -> None: def test_sqlite_status_and_rebuild(tmp_path: Path) -> None: - (tmp_path / "spdd" / "canvas").mkdir(parents=True) - (tmp_path / "spdd" / "memory").mkdir(parents=True) - (tmp_path / "spdd" / "memory" / "registry.jsonl").write_text("", encoding="utf-8") + (tmp_path / "sdlc-spdd" / "spdd" / "canvas").mkdir(parents=True) + (tmp_path / "sdlc-spdd" / "spdd" / "memory").mkdir(parents=True) + (tmp_path / "sdlc-spdd" / "spdd" / "memory" / "registry.jsonl").write_text("", encoding="utf-8") app = create_app(tmp_path) client = app.test_client() missing = client.post("/api/sqlite/status", json={"target": str(tmp_path)}) @@ -186,7 +197,7 @@ def test_guide_config_roundtrip(tmp_path: Path) -> None: ) assert saved["profile"] == "menke-2" assert saved["neo4j_bolt_port"] == 17687 - assert (tmp_path / ".sdlc" / "guide-config.json").is_file() + assert (tmp_path / "sdlc-spdd" / ".sdlc" / "guide-config.json").is_file() items = checklist(saved, {"tcp_open": False}, neo4j={"bolt_open": False}) assert any(i["id"] == "tcp" and i["ok"] is False for i in items) assert any(i["id"] == "neo4j" and i["ok"] is False for i in items) diff --git a/engine/tests_integration/test_installer_adf_api.py b/engine/tests_integration/test_installer_adf_api.py index 73a34d03..94ab04e2 100644 --- a/engine/tests_integration/test_installer_adf_api.py +++ b/engine/tests_integration/test_installer_adf_api.py @@ -5,7 +5,6 @@ import json from pathlib import Path from typing import Any -from unittest.mock import MagicMock import pytest @@ -86,7 +85,7 @@ def fake_killpg(pid: int, sig: int) -> None: assert body["ok"] is True assert body["result"]["ok"] is True assert calls and "sdlc_engine.viewer" in calls[0] - assert (tmp_path / ".sdlc" / "adf-viewer-runtime.json").is_file() + assert (tmp_path / "sdlc-spdd" / ".sdlc" / "adf-viewer-runtime.json").is_file() # Already running → 400 again = client.post( @@ -99,7 +98,7 @@ def fake_killpg(pid: int, sig: int) -> None: stop = client.post("/api/adf/stop", json={"target": str(tmp_path)}) assert stop.status_code == 200 assert stop.get_json()["ok"] is True - assert not (tmp_path / ".sdlc" / "adf-viewer-runtime.json").is_file() + assert not (tmp_path / "sdlc-spdd" / ".sdlc" / "adf-viewer-runtime.json").is_file() # Restart after stop monkeypatch.setattr(vr, "_pid_alive", lambda pid: False) @@ -178,9 +177,9 @@ def test_api_adf_init_work_with_claim(tmp_path: Path, monkeypatch: pytest.Monkey body = res.get_json() assert body["ok"] is True assert body["work_id"] == "FEAT-013-api-claim" - reg = (tmp_path / "spdd" / "memory" / "registry.jsonl").read_text(encoding="utf-8") + reg = (tmp_path / "sdlc-spdd" / "spdd" / "memory" / "registry.jsonl").read_text(encoding="utf-8") assert "console-claim" in reg - assert (tmp_path / ".sdlc" / "pointer").read_text(encoding="utf-8").strip() == ( + assert (tmp_path / "sdlc-spdd" / ".sdlc" / "pointer").read_text(encoding="utf-8").strip() == ( "FEAT-013-api-claim" ) @@ -188,8 +187,8 @@ def test_api_adf_init_work_with_claim(tmp_path: Path, monkeypatch: pytest.Monkey def test_api_adf_init_work_claim_conflict(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("SDLC_USER", "bob") wid = "FEAT-013-api-conflict" - (tmp_path / "spdd" / "memory").mkdir(parents=True, exist_ok=True) - (tmp_path / "spdd" / "memory" / "registry.jsonl").write_text( + (tmp_path / "sdlc-spdd" / "spdd" / "memory").mkdir(parents=True, exist_ok=True) + (tmp_path / "sdlc-spdd" / "spdd" / "memory" / "registry.jsonl").write_text( json.dumps( { "event": "claim", @@ -214,7 +213,7 @@ def test_api_adf_init_work_claim_conflict(tmp_path: Path, monkeypatch: pytest.Mo ) assert res.status_code == 400 assert "alice" in res.get_json()["error"] - assert not (tmp_path / "spdd" / "canvas" / f"{wid}.md").exists() + assert not (tmp_path / "sdlc-spdd" / "spdd" / "canvas" / f"{wid}.md").exists() def test_api_adf_init_work_validation(tmp_path: Path) -> None: @@ -300,7 +299,7 @@ def test_api_adf_browse_and_init_work(tmp_path: Path) -> None: assert body["ok"] is True assert body["work_id"] == "FEAT-013-console-adf-init" assert body["source_issue"] == "ORCH-7" - assert (tmp_path / "spdd" / "canvas" / "FEAT-013-console-adf-init.md").is_file() + assert (tmp_path / "sdlc-spdd" / "spdd" / "canvas" / "FEAT-013-console-adf-init.md").is_file() assert "sdlc-spdd-analysis" in body["next_command"] assert "work init-from-adf" in body["cli"] @@ -318,7 +317,7 @@ def test_api_adf_browse_and_init_work(tmp_path: Path) -> None: dry_body = dry.get_json() assert dry_body["ok"] is True assert dry_body["dry_run"] is True - assert not (tmp_path / "spdd" / "canvas" / "FEAT-014-dry-only.md").exists() + assert not (tmp_path / "sdlc-spdd" / "spdd" / "canvas" / "FEAT-014-dry-only.md").exists() def test_console_page_includes_adf_init_ui(tmp_path: Path) -> None: diff --git a/engine/tests_integration/test_installer_adf_live.py b/engine/tests_integration/test_installer_adf_live.py index 7d099ab4..b23d2747 100644 --- a/engine/tests_integration/test_installer_adf_live.py +++ b/engine/tests_integration/test_installer_adf_live.py @@ -102,7 +102,7 @@ def test_live_adf_start_probe_stop_via_api(tmp_path: Path) -> None: time.sleep(0.15) assert not vr._pid_alive(pid), f"pid {pid} still alive after stop" assert not vr._tcp_open(host, port), f"port {port} still open after stop" - assert not (tmp_path / ".sdlc" / "adf-viewer-runtime.json").is_file() + assert not (tmp_path / "sdlc-spdd" / ".sdlc" / "adf-viewer-runtime.json").is_file() after = client.post("/api/adf", json=body) assert after.status_code == 200 diff --git a/engine/tests_integration/test_installer_coverage_gaps.py b/engine/tests_integration/test_installer_coverage_gaps.py index f0a8577f..4ae629a2 100644 --- a/engine/tests_integration/test_installer_coverage_gaps.py +++ b/engine/tests_integration/test_installer_coverage_gaps.py @@ -3,7 +3,6 @@ from __future__ import annotations import io -import json import os import subprocess import urllib.error diff --git a/engine/tests_integration/test_installer_dashboard_api.py b/engine/tests_integration/test_installer_dashboard_api.py index 54ad36ba..e8d7752b 100644 --- a/engine/tests_integration/test_installer_dashboard_api.py +++ b/engine/tests_integration/test_installer_dashboard_api.py @@ -91,14 +91,14 @@ def _lesson(kind: str, work_id: str, ts: str, title: str, source: str = "retro") def _seed_project(root: Path) -> None: """Populated project: accepted + staged records, registry events, workflow history.""" _write_jsonl( - root / "spdd" / "memory" / "lessons.jsonl", + root / "sdlc-spdd" / "spdd" / "memory" / "lessons.jsonl", [ _lesson("decision", OLD_WID, "2026-08-01T10:00:00Z", "Old decision"), _lesson("pitfall", OLD_WID, "2026-08-03T10:00:00Z", "Old pitfall"), ], ) _write_jsonl( - root / "spdd" / "memory" / "registry.jsonl", + root / "sdlc-spdd" / "spdd" / "memory" / "registry.jsonl", [ { "event": "claim", @@ -123,23 +123,23 @@ def _seed_project(root: Path) -> None: ], ) _write_jsonl( - root / ".sdlc" / "staged" / "lessons.jsonl", + root / "sdlc-spdd" / ".sdlc" / "staged" / "lessons.jsonl", [_lesson("session", WID, "2026-08-05T10:00:00Z", "Working session", source="capture")], ) - workflows = root / ".sdlc" / "workflows" + workflows = root / "sdlc-spdd" / ".sdlc" / "workflows" workflows.mkdir(parents=True, exist_ok=True) (workflows / f"{WID}.history").write_text( "2026-08-02T09:00:00Z\tcreate\twork_id=" + WID + "\n" "2026-08-06T10:00:00Z\tadvance\tphase=plan\n", encoding="utf-8", ) - (root / ".sdlc" / "pointer").write_text(WID, encoding="utf-8") - milestone = root / "requirements" / "milestones" / f"{WID}.md" + (root / "sdlc-spdd" / ".sdlc" / "pointer").write_text(WID, encoding="utf-8") + milestone = root / "sdlc-spdd" / "requirements" / "milestones" / f"{WID}.md" milestone.parent.mkdir(parents=True, exist_ok=True) milestone.write_text( f"# {WID}\n\n## Jira\n\n- PROJ-1\n\n## GitHub\n\n- #12\n", encoding="utf-8" ) - analysis = root / "spdd" / "analysis" / f"{WID}-analysis.md" + analysis = root / "sdlc-spdd" / "spdd" / "analysis" / f"{WID}-analysis.md" analysis.parent.mkdir(parents=True, exist_ok=True) analysis.write_text(f"# {WID} analysis\n", encoding="utf-8") diff --git a/engine/tests_integration/test_installer_guide_api.py b/engine/tests_integration/test_installer_guide_api.py index ea37d5c6..dba8e23e 100644 --- a/engine/tests_integration/test_installer_guide_api.py +++ b/engine/tests_integration/test_installer_guide_api.py @@ -3,8 +3,6 @@ from __future__ import annotations from pathlib import Path -from typing import Any -from unittest.mock import MagicMock import pytest @@ -42,8 +40,8 @@ def test_api_rollback_endpoint(tmp_path: Path) -> None: (backup / "manifest.json").write_text( '{"files": ["spdd/memory/lessons.jsonl"]}\n', encoding="utf-8" ) - (backup / "spdd" / "memory").mkdir(parents=True) - (backup / "spdd" / "memory" / "lessons.jsonl").write_text( + (backup / "sdlc-spdd" / "spdd" / "memory").mkdir(parents=True) + (backup / "sdlc-spdd" / "spdd" / "memory" / "lessons.jsonl").write_text( '{"id": "from-backup"}\n', encoding="utf-8" ) diff --git a/engine/tests_integration/test_installer_jira_api.py b/engine/tests_integration/test_installer_jira_api.py index 9f8c73d7..b024a53f 100644 --- a/engine/tests_integration/test_installer_jira_api.py +++ b/engine/tests_integration/test_installer_jira_api.py @@ -2,7 +2,6 @@ from __future__ import annotations -import json from pathlib import Path import pytest @@ -15,7 +14,7 @@ def _seed(root: Path, work_id: str) -> None: - req = root / "requirements" / "milestones" / f"{work_id}.md" + req = root / "sdlc-spdd" / "requirements" / "milestones" / f"{work_id}.md" req.parent.mkdir(parents=True, exist_ok=True) req.write_text( f"""--- @@ -44,7 +43,7 @@ def _seed(root: Path, work_id: str) -> None: """, encoding="utf-8", ) - canvas = root / "spdd" / "canvas" / f"{work_id}.md" + canvas = root / "sdlc-spdd" / "spdd" / "canvas" / f"{work_id}.md" canvas.parent.mkdir(parents=True, exist_ok=True) canvas.write_text( f"""# REASONS Canvas: {work_id} - Demo @@ -58,8 +57,8 @@ def _seed(root: Path, work_id: str) -> None: """, encoding="utf-8", ) - (root / "spdd" / "memory").mkdir(parents=True, exist_ok=True) - reg = root / "spdd" / "memory" / "registry.jsonl" + (root / "sdlc-spdd" / "spdd" / "memory").mkdir(parents=True, exist_ok=True) + reg = root / "sdlc-spdd" / "spdd" / "memory" / "registry.jsonl" if not reg.is_file(): reg.write_text("", encoding="utf-8") TeamRegistry(Project.resolve(root)).claim(work_id) @@ -87,7 +86,7 @@ def test_api_jira_link_dry_run_and_apply(tmp_path: Path) -> None: assert body["dry_run"] is True assert any("would set" in a for a in body["actions"]) - req_text = (tmp_path / "requirements" / "milestones" / f"{work_id}.md").read_text() + req_text = (tmp_path / "sdlc-spdd" / "requirements" / "milestones" / f"{work_id}.md").read_text() assert "PROJ-123" not in req_text apply = client.post( @@ -104,10 +103,10 @@ def test_api_jira_link_dry_run_and_apply(tmp_path: Path) -> None: applied = apply.get_json() assert applied["linked"] is True - req_text = (tmp_path / "requirements" / "milestones" / f"{work_id}.md").read_text() + req_text = (tmp_path / "sdlc-spdd" / "requirements" / "milestones" / f"{work_id}.md").read_text() assert "Key: PROJ-123" in req_text assert 'jira_key: "PROJ-123"' in req_text - canvas = (tmp_path / "spdd" / "canvas" / f"{work_id}.md").read_text() + canvas = (tmp_path / "sdlc-spdd" / "spdd" / "canvas" / f"{work_id}.md").read_text() assert "Source Issue: PROJ-123" in canvas status = client.post( diff --git a/engine/tests_integration/test_installer_persistence_api.py b/engine/tests_integration/test_installer_persistence_api.py index 60eb0c7d..b4f4a658 100644 --- a/engine/tests_integration/test_installer_persistence_api.py +++ b/engine/tests_integration/test_installer_persistence_api.py @@ -40,7 +40,7 @@ def test_persistence_status_and_save(tmp_path: Path) -> None: assert saved["notes"] == "installer-api" assert saved.get("config_path") assert saved.get("saved") is True - assert (tmp_path / ".sdlc" / "persistence-config.json").is_file() + assert (tmp_path / "sdlc-spdd" / ".sdlc" / "persistence-config.json").is_file() # Second save from the returned shape must not silently drop backends. res = client.post( @@ -126,7 +126,7 @@ def test_persistence_parity_endpoint(tmp_path: Path) -> None: # Corrupt the cache to force drift (a missing db would just be rebuilt). import sqlite3 - with sqlite3.connect(tmp_path / ".sdlc" / "index.sqlite") as conn: + with sqlite3.connect(tmp_path / "sdlc-spdd" / ".sdlc" / "index.sqlite") as conn: conn.execute("DELETE FROM lessons") res = client.post("/api/persistence/parity", json={"target": str(tmp_path)}) assert res.status_code == 200 diff --git a/engine/tests_integration/test_installer_playground_api.py b/engine/tests_integration/test_installer_playground_api.py index 968e6b41..5c72d67a 100644 --- a/engine/tests_integration/test_installer_playground_api.py +++ b/engine/tests_integration/test_installer_playground_api.py @@ -141,7 +141,7 @@ def test_sqlite_work_detail_reads_git_docs(tmp_path: Path) -> None: assert unknown["requirement"]["exists"] is False assert unknown["canvas"]["exists"] is False - req = dest / "requirements" / "milestones" / f"{active}.md" + req = dest / "sdlc-spdd" / "requirements" / "milestones" / f"{active}.md" req.write_text("X" * 80_001, encoding="utf-8") truncated = client.post("/api/sqlite/work", json={**body, "work_id": active}).get_json() assert truncated["requirement"]["truncated"] is True diff --git a/engine/tests_integration/test_viewer_store.py b/engine/tests_integration/test_viewer_store.py index 3d6d5eb9..035ded1c 100644 --- a/engine/tests_integration/test_viewer_store.py +++ b/engine/tests_integration/test_viewer_store.py @@ -2,7 +2,6 @@ from __future__ import annotations -import json from pathlib import Path import pytest diff --git a/engine/tests_unit/test_adf_templates.py b/engine/tests_unit/test_adf_templates.py index 727d6f44..e46f50b9 100644 --- a/engine/tests_unit/test_adf_templates.py +++ b/engine/tests_unit/test_adf_templates.py @@ -43,9 +43,9 @@ def test_bind_variables_and_unknown_empty() -> None: def test_render_feature_from_milestone(tmp_path: Path) -> None: root = tmp_path - (root / "requirements" / "milestones").mkdir(parents=True) + (root / "sdlc-spdd" / "requirements" / "milestones").mkdir(parents=True) work_id = "FEAT-900-template-demo" - (root / "requirements" / "milestones" / f"{work_id}.md").write_text( + (root / "sdlc-spdd" / "requirements" / "milestones" / f"{work_id}.md").write_text( """# Requirement ## Summary @@ -88,9 +88,9 @@ def test_render_feature_from_milestone(tmp_path: Path) -> None: def test_render_writes_output(tmp_path: Path) -> None: root = tmp_path - (root / "requirements" / "milestones").mkdir(parents=True) + (root / "sdlc-spdd" / "requirements" / "milestones").mkdir(parents=True) work_id = "BUG-901-template-bug" - (root / "requirements" / "milestones" / f"{work_id}.md").write_text( + (root / "sdlc-spdd" / "requirements" / "milestones" / f"{work_id}.md").write_text( "## Summary\n\nFix the thing.\n", encoding="utf-8", ) @@ -122,9 +122,9 @@ def test_unknown_combo_raises() -> None: def test_cli_template_list_and_render(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None: - (tmp_path / "requirements" / "milestones").mkdir(parents=True) + (tmp_path / "sdlc-spdd" / "requirements" / "milestones").mkdir(parents=True) work_id = "SPIKE-902-cli-template" - (tmp_path / "requirements" / "milestones" / f"{work_id}.md").write_text( + (tmp_path / "sdlc-spdd" / "requirements" / "milestones" / f"{work_id}.md").write_text( "## Summary\n\nCLI template smoke.\n", encoding="utf-8", ) diff --git a/engine/tests_unit/test_adf_work.py b/engine/tests_unit/test_adf_work.py index f0fdedfe..c2b9c14d 100644 --- a/engine/tests_unit/test_adf_work.py +++ b/engine/tests_unit/test_adf_work.py @@ -63,11 +63,11 @@ def test_init_from_adf_creates_artifacts(tmp_path: Path) -> None: # Stay-set only (#86): no committed per-feature mirror folders. assert result.feature_dir == "" assert not (root / "agent-context" / "features" / result.work_id).exists() - progress = root / "spdd" / "memory" / "entries" / "progress.md" + progress = root / "sdlc-spdd" / "spdd" / "memory" / "entries" / "progress.md" assert progress.is_file() assert result.work_id in progress.read_text(encoding="utf-8") # --no-claim still pins the pointer so analysis can resume immediately. - assert (root / ".sdlc" / "pointer").read_text(encoding="utf-8").strip() == result.work_id + assert (root / "sdlc-spdd" / ".sdlc" / "pointer").read_text(encoding="utf-8").strip() == result.work_id canvas = (root / result.canvas_path).read_text(encoding="utf-8") assert "Source System: ADF" in canvas assert "Browse ADF and create a canvas." in canvas @@ -201,8 +201,8 @@ def test_init_from_adf_same_owner_reclaim_allowed( root = tmp_path / "app" root.mkdir() wid = "FEAT-013-same-owner" - (root / "spdd" / "memory").mkdir(parents=True, exist_ok=True) - (root / "spdd" / "memory" / "registry.jsonl").write_text( + (root / "sdlc-spdd" / "spdd" / "memory").mkdir(parents=True, exist_ok=True) + (root / "sdlc-spdd" / "spdd" / "memory" / "registry.jsonl").write_text( json.dumps( { "event": "claim", @@ -230,7 +230,7 @@ def test_init_from_adf_same_owner_reclaim_allowed( def test_init_from_adf_allocates_next_number(tmp_path: Path) -> None: root = tmp_path / "app" root.mkdir() - canvas_dir = root / "spdd" / "canvas" + canvas_dir = root / "sdlc-spdd" / "spdd" / "canvas" canvas_dir.mkdir(parents=True) (canvas_dir / "FEAT-002-existing.md").write_text("# existing\n", encoding="utf-8") adf = root / "next.adf.json" @@ -252,9 +252,9 @@ def test_init_from_adf_claims_and_sets_pointer(tmp_path: Path, monkeypatch: pyte work_id="FEAT-013-claimed-from-adf", claim=True, ) - pointer = (root / ".sdlc" / "pointer").read_text(encoding="utf-8").strip() + pointer = (root / "sdlc-spdd" / ".sdlc" / "pointer").read_text(encoding="utf-8").strip() assert pointer == "FEAT-013-claimed-from-adf" - reg = (root / "spdd" / "memory" / "registry.jsonl").read_text(encoding="utf-8") + reg = (root / "sdlc-spdd" / "spdd" / "memory" / "registry.jsonl").read_text(encoding="utf-8") assert "FEAT-013-claimed-from-adf" in reg assert "adf-claimer" in reg assert "init-from-adf:" in reg @@ -270,8 +270,8 @@ def test_init_from_adf_claim_conflict_before_write( root.mkdir() wid = "FEAT-013-claim-conflict" # Seed an active claim owned by alice without a canvas yet. - (root / "spdd" / "memory").mkdir(parents=True, exist_ok=True) - (root / "spdd" / "memory" / "registry.jsonl").write_text( + (root / "sdlc-spdd" / "spdd" / "memory").mkdir(parents=True, exist_ok=True) + (root / "sdlc-spdd" / "spdd" / "memory" / "registry.jsonl").write_text( json.dumps( { "event": "claim", @@ -293,7 +293,7 @@ def test_init_from_adf_claim_conflict_before_write( AdfWorkService(Project.resolve(root)).init_from_adf( adf, work_id=wid, claim=True ) - assert not (root / "spdd" / "canvas" / f"{wid}.md").exists() + assert not (root / "sdlc-spdd" / "spdd" / "canvas" / f"{wid}.md").exists() def test_init_from_adf_empty_body_and_outside_root(tmp_path: Path) -> None: diff --git a/engine/tests_unit/test_canvas.py b/engine/tests_unit/test_canvas.py index 462189b0..4ba73a93 100644 --- a/engine/tests_unit/test_canvas.py +++ b/engine/tests_unit/test_canvas.py @@ -411,10 +411,10 @@ def _init_scope_repo(tmp_path: Path) -> Path: _git(root, "config", "user.email", "test@example.com") _git(root, "config", "user.name", "Test") (root / "README.md").write_text("# demo\n", encoding="utf-8") - canvas_dir = root / "spdd" / "canvas" + canvas_dir = root / "sdlc-spdd" / "spdd" / "canvas" canvas_dir.mkdir(parents=True) (canvas_dir / "SCOPE-I2.md").write_text(_SCOPE_CANVAS, encoding="utf-8") - _git(root, "add", "README.md", "spdd/canvas/SCOPE-I2.md") + _git(root, "add", "README.md", "sdlc-spdd/spdd/canvas/SCOPE-I2.md") _git(root, "commit", "-m", "init") current = subprocess.check_output( ["git", "-C", str(root), "branch", "--show-current"], diff --git a/engine/tests_unit/test_cli.py b/engine/tests_unit/test_cli.py index 89d5c3db..f9663e07 100644 --- a/engine/tests_unit/test_cli.py +++ b/engine/tests_unit/test_cli.py @@ -7,7 +7,7 @@ def test_cli_claim_next_archive(tmp_path: Path, monkeypatch) -> None: monkeypatch.setenv("SDLC_USER", "cli-user") work_id = "FEAT-030-cli" - canvas = tmp_path / "spdd" / "canvas" / f"{work_id}.md" + canvas = tmp_path / "sdlc-spdd" / "spdd" / "canvas" / f"{work_id}.md" canvas.parent.mkdir(parents=True) canvas.write_text( f"# {work_id}\n\n## Final Status\n\n- Status: Complete\n", @@ -16,8 +16,8 @@ def test_cli_claim_next_archive(tmp_path: Path, monkeypatch) -> None: assert main(["--root", str(tmp_path), "claim", work_id]) == 0 assert main(["--root", str(tmp_path), "next"]) == 0 assert main(["--root", str(tmp_path), "archive", work_id]) == 0 - assert not (tmp_path / "spdd" / "canvas" / f"{work_id}.md").exists() - assert not (tmp_path / "spdd" / "canvas" / "archive").exists() + assert not (tmp_path / "sdlc-spdd" / "spdd" / "canvas" / f"{work_id}.md").exists() + assert not (tmp_path / "sdlc-spdd" / "spdd" / "canvas" / "archive").exists() assert main(["--root", str(tmp_path), "version"]) == 0 @@ -77,7 +77,7 @@ def test_cli_work_init_from_adf(tmp_path: Path, capsys) -> None: out = capsys.readouterr().out assert "FEAT-013-cli-adf-init" in out assert "[dry-run]" in out - assert not (tmp_path / "spdd" / "canvas" / "FEAT-013-cli-adf-init.md").exists() + assert not (tmp_path / "sdlc-spdd" / "spdd" / "canvas" / "FEAT-013-cli-adf-init.md").exists() rc = main( [ @@ -98,7 +98,7 @@ def test_cli_work_init_from_adf(tmp_path: Path, capsys) -> None: out = capsys.readouterr().out assert "Created FEAT-013-cli-adf-init" in out assert "/sdlc-spdd-analysis @" in out - canvas = tmp_path / "spdd" / "canvas" / "FEAT-013-cli-adf-init.md" + canvas = tmp_path / "sdlc-spdd" / "spdd" / "canvas" / "FEAT-013-cli-adf-init.md" assert canvas.is_file() text = canvas.read_text(encoding="utf-8") assert "CLI title" in text @@ -172,10 +172,10 @@ def test_cli_work_init_from_adf_claims_by_default(tmp_path: Path, monkeypatch, c assert rc == 0 out = capsys.readouterr().out assert "Created FEAT-013-cli-claim-default" in out - reg_path = tmp_path / "spdd" / "memory" / "registry.jsonl" + reg_path = tmp_path / "sdlc-spdd" / "spdd" / "memory" / "registry.jsonl" assert reg_path.is_file() assert "cli-claim" in reg_path.read_text(encoding="utf-8") - assert (tmp_path / ".sdlc" / "pointer").read_text(encoding="utf-8").strip() == ( + assert (tmp_path / "sdlc-spdd" / ".sdlc" / "pointer").read_text(encoding="utf-8").strip() == ( "FEAT-013-cli-claim-default" ) @@ -201,8 +201,8 @@ def test_cli_work_init_from_adf_invalid_json(tmp_path: Path, capsys) -> None: def test_cli_work_init_from_adf_claim_conflict(tmp_path: Path, monkeypatch, capsys) -> None: monkeypatch.setenv("SDLC_USER", "bob") wid = "FEAT-013-cli-conflict" - (tmp_path / "spdd" / "memory").mkdir(parents=True, exist_ok=True) - reg_jsonl = tmp_path / "spdd" / "memory" / "registry.jsonl" + (tmp_path / "sdlc-spdd" / "spdd" / "memory").mkdir(parents=True, exist_ok=True) + reg_jsonl = tmp_path / "sdlc-spdd" / "spdd" / "memory" / "registry.jsonl" reg_jsonl.write_text( json.dumps( { @@ -237,4 +237,4 @@ def test_cli_work_init_from_adf_claim_conflict(tmp_path: Path, monkeypatch, caps ) assert rc == 1 assert "alice" in capsys.readouterr().err - assert not (tmp_path / "spdd" / "canvas" / f"{wid}.md").exists() + assert not (tmp_path / "sdlc-spdd" / "spdd" / "canvas" / f"{wid}.md").exists() diff --git a/engine/tests_unit/test_context_store_guide_parity.py b/engine/tests_unit/test_context_store_guide_parity.py index 601668c7..75166712 100644 --- a/engine/tests_unit/test_context_store_guide_parity.py +++ b/engine/tests_unit/test_context_store_guide_parity.py @@ -14,16 +14,16 @@ def _seed(root: Path, work_id: str) -> None: - req = root / "requirements" / "milestones" / f"{work_id}.md" + req = root / "sdlc-spdd" / "requirements" / "milestones" / f"{work_id}.md" req.parent.mkdir(parents=True, exist_ok=True) req.write_text(f"# Requirement {work_id}\n", encoding="utf-8") - canvas = root / "spdd" / "canvas" / f"{work_id}.md" + canvas = root / "sdlc-spdd" / "spdd" / "canvas" / f"{work_id}.md" canvas.parent.mkdir(parents=True, exist_ok=True) canvas.write_text( f"# REASONS Canvas: {work_id}\n\n## Metadata\n\n- Work ID: {work_id}\n", encoding="utf-8", ) - (root / "spdd" / "memory").mkdir(parents=True, exist_ok=True) + (root / "sdlc-spdd" / "spdd" / "memory").mkdir(parents=True, exist_ok=True) def test_lesson_ids_from_subgraph_collects_record_ids() -> None: @@ -209,9 +209,9 @@ def __exit__(self, *a): with patch("sdlc_engine.context_store.urllib.request.urlopen", return_value=Resp()): out = store.project_to_guide() assert out["status"] == 200 - index = tmp_path / "spdd" / "memory" / "context-index.md" + index = tmp_path / "sdlc-spdd" / "spdd" / "memory" / "context-index.md" assert index.is_file() - assert out.get("ingestIndex") == "spdd/memory/context-index.md" + assert out.get("ingestIndex") == "sdlc-spdd/spdd/memory/context-index.md" assert wid in index.read_text(encoding="utf-8") diff --git a/engine/tests_unit/test_context_store_persistence.py b/engine/tests_unit/test_context_store_persistence.py index 3891470a..11f559c2 100644 --- a/engine/tests_unit/test_context_store_persistence.py +++ b/engine/tests_unit/test_context_store_persistence.py @@ -6,7 +6,6 @@ from pathlib import Path from unittest.mock import MagicMock, patch -import pytest from sdlc_engine.cli import main from sdlc_engine.context_store import ContextStore @@ -16,10 +15,10 @@ def _seed_canvas(root: Path, work_id: str) -> None: - req = root / "requirements" / "milestones" / f"{work_id}.md" + req = root / "sdlc-spdd" / "requirements" / "milestones" / f"{work_id}.md" req.parent.mkdir(parents=True, exist_ok=True) req.write_text(f"# Requirement: {work_id}\n\n## Summary\nProof.\n", encoding="utf-8") - canvas = root / "spdd" / "canvas" / f"{work_id}.md" + canvas = root / "sdlc-spdd" / "spdd" / "canvas" / f"{work_id}.md" canvas.parent.mkdir(parents=True, exist_ok=True) canvas.write_text( f"""# REASONS Canvas: {work_id} @@ -32,7 +31,7 @@ def _seed_canvas(root: Path, work_id: str) -> None: """, encoding="utf-8", ) - root.joinpath("spdd/memory").mkdir(parents=True, exist_ok=True) + root.joinpath("sdlc-spdd/spdd/memory").mkdir(parents=True, exist_ok=True) def test_persist_lesson_stages_git_and_sqlite(tmp_path: Path) -> None: @@ -54,7 +53,7 @@ def test_persist_lesson_stages_git_and_sqlite(tmp_path: Path) -> None: assert result.git.get("staged") is True assert result.sqlite.get("ok") is True - staged_path = tmp_path / ".sdlc" / "staged" / "lessons.jsonl" + staged_path = tmp_path / "sdlc-spdd" / ".sdlc" / "staged" / "lessons.jsonl" assert staged_path.is_file() assert "Never open PRs" in staged_path.read_text(encoding="utf-8") @@ -79,7 +78,7 @@ def test_persist_accept_and_parity(tmp_path: Path) -> None: project_guide=False, ) store.accept(work_id=wid, project_guide=False) - ledger_path = tmp_path / "spdd" / "memory" / "lessons.jsonl" + ledger_path = tmp_path / "sdlc-spdd" / "spdd" / "memory" / "lessons.jsonl" assert ledger_path.is_file() parity = store.parity(repair=False) assert parity["sqlite"]["enabled"] is True @@ -131,7 +130,7 @@ def test_cli_persist_lesson_area_only_no_work_id(tmp_path: Path) -> None: ] ) assert rc == 0 - staged = (tmp_path / ".sdlc" / "staged" / "lessons.jsonl").read_text(encoding="utf-8") + staged = (tmp_path / "sdlc-spdd" / ".sdlc" / "staged" / "lessons.jsonl").read_text(encoding="utf-8") assert "FEAT-" not in staged assert '"work_id": ""' in staged assert "pitfall:(none):notify:adhoc-prompt" in staged diff --git a/engine/tests_unit/test_db.py b/engine/tests_unit/test_db.py index a6036804..231fc7c7 100644 --- a/engine/tests_unit/test_db.py +++ b/engine/tests_unit/test_db.py @@ -7,7 +7,7 @@ def _seed(root: Path, work_id: str, *, jira: str = "ORCH-1", status: str = "In Progress") -> None: - req = root / "requirements" / "milestones" / f"{work_id}.md" + req = root / "sdlc-spdd" / "requirements" / "milestones" / f"{work_id}.md" req.parent.mkdir(parents=True, exist_ok=True) req.write_text( f"""# Requirement: {work_id} @@ -27,7 +27,7 @@ def _seed(root: Path, work_id: str, *, jira: str = "ORCH-1", status: str = "In P """, encoding="utf-8", ) - canvas = root / "spdd" / "canvas" / f"{work_id}.md" + canvas = root / "sdlc-spdd" / "spdd" / "canvas" / f"{work_id}.md" canvas.parent.mkdir(parents=True, exist_ok=True) canvas.write_text( f"""# REASONS Canvas: {work_id} - Indexed demo diff --git a/engine/tests_unit/test_db_graph_v5.py b/engine/tests_unit/test_db_graph_v5.py index b098bf0e..fdf87a97 100644 --- a/engine/tests_unit/test_db_graph_v5.py +++ b/engine/tests_unit/test_db_graph_v5.py @@ -11,10 +11,10 @@ def _seed_v5_tree(root: Path, work_id: str) -> None: - req = root / "requirements" / "milestones" / f"{work_id}.md" + req = root / "sdlc-spdd" / "requirements" / "milestones" / f"{work_id}.md" req.parent.mkdir(parents=True, exist_ok=True) req.write_text(f"# Requirement: {work_id}\n\n## Summary\nV5 tree.\n", encoding="utf-8") - canvas = root / "spdd" / "canvas" / f"{work_id}.md" + canvas = root / "sdlc-spdd" / "spdd" / "canvas" / f"{work_id}.md" canvas.parent.mkdir(parents=True, exist_ok=True) canvas.write_text( f"""# REASONS Canvas: {work_id} @@ -27,14 +27,14 @@ def _seed_v5_tree(root: Path, work_id: str) -> None: """, encoding="utf-8", ) - (root / "spdd" / "analysis").mkdir(parents=True, exist_ok=True) - (root / "spdd" / "analysis" / f"{work_id}-analysis.md").write_text("# Analysis\n", encoding="utf-8") - (root / "spdd" / "reviews").mkdir(parents=True, exist_ok=True) - (root / "spdd" / "reviews" / f"{work_id}-review.md").write_text("# Review\n", encoding="utf-8") - (root / "spdd" / "sync").mkdir(parents=True, exist_ok=True) - (root / "spdd" / "sync" / f"{work_id}-sync.md").write_text("# Sync\n", encoding="utf-8") - - mem = root / "spdd" / "memory" + (root / "sdlc-spdd" / "spdd" / "analysis").mkdir(parents=True, exist_ok=True) + (root / "sdlc-spdd" / "spdd" / "analysis" / f"{work_id}-analysis.md").write_text("# Analysis\n", encoding="utf-8") + (root / "sdlc-spdd" / "spdd" / "reviews").mkdir(parents=True, exist_ok=True) + (root / "sdlc-spdd" / "spdd" / "reviews" / f"{work_id}-review.md").write_text("# Review\n", encoding="utf-8") + (root / "sdlc-spdd" / "spdd" / "sync").mkdir(parents=True, exist_ok=True) + (root / "sdlc-spdd" / "spdd" / "sync" / f"{work_id}-sync.md").write_text("# Sync\n", encoding="utf-8") + + mem = root / "sdlc-spdd" / "spdd" / "memory" mem.mkdir(parents=True, exist_ok=True) ledger = LessonsLedger(Project(root)) for kind, body in ( @@ -84,7 +84,7 @@ def _seed_v5_tree(root: Path, work_id: str) -> None: ) ) - hot = root / ".sdlc" / "sessions" + hot = root / "sdlc-spdd" / ".sdlc" / "sessions" hot.mkdir(parents=True, exist_ok=True) (hot / f"20260808T120000Z-{work_id}-code.md").write_text("# Hot session\n", encoding="utf-8") diff --git a/engine/tests_unit/test_db_schema_upgrade.py b/engine/tests_unit/test_db_schema_upgrade.py index de4393eb..029f1b2f 100644 --- a/engine/tests_unit/test_db_schema_upgrade.py +++ b/engine/tests_unit/test_db_schema_upgrade.py @@ -23,10 +23,10 @@ def _seed_stay_set(root: Path, work_id: str) -> None: - req = root / "requirements" / "milestones" / f"{work_id}.md" + req = root / "sdlc-spdd" / "requirements" / "milestones" / f"{work_id}.md" req.parent.mkdir(parents=True, exist_ok=True) req.write_text(f"# Requirement: {work_id}\n\n## Summary\nOld cache.\n", encoding="utf-8") - canvas = root / "spdd" / "canvas" / f"{work_id}.md" + canvas = root / "sdlc-spdd" / "spdd" / "canvas" / f"{work_id}.md" canvas.parent.mkdir(parents=True, exist_ok=True) canvas.write_text( f"""# REASONS Canvas: {work_id} @@ -39,7 +39,7 @@ def _seed_stay_set(root: Path, work_id: str) -> None: """, encoding="utf-8", ) - root.joinpath("spdd/memory").mkdir(parents=True, exist_ok=True) + root.joinpath("sdlc-spdd/spdd/memory").mkdir(parents=True, exist_ok=True) def _write_v4_sqlite( @@ -129,7 +129,7 @@ def _v5_persist_insert(conn: sqlite3.Connection) -> None: def test_v5_persist_row_fails_on_old_schema_without_upgrade(tmp_path: Path) -> None: """Confirm: the persistence write cannot land until the old format is upgraded.""" - db_path = tmp_path / ".sdlc" / "index.sqlite" + db_path = tmp_path / "sdlc-spdd" / ".sdlc" / "index.sqlite" _write_v4_sqlite(db_path) conn = sqlite3.connect(str(db_path)) with pytest.raises(sqlite3.OperationalError, match="staged|title|phase|keywords"): @@ -147,7 +147,7 @@ def test_persist_upgrades_v4_sqlite_in_place_then_succeeds(tmp_path: Path) -> No wid = "FEAT-OLD-V4" _seed_stay_set(tmp_path, wid) save_config(tmp_path, {"backends": ["git-pointers", "sqlite"]}) - db_path = tmp_path / ".sdlc" / "index.sqlite" + db_path = tmp_path / "sdlc-spdd" / ".sdlc" / "index.sqlite" _write_v4_sqlite(db_path, work_id=wid) store = ContextStore(Project(tmp_path), guide_base_url="http://127.0.0.1:9") @@ -180,7 +180,7 @@ def test_persist_upgrades_stale_v5_label_with_v4_columns(tmp_path: Path) -> None wid = "FEAT-STALE-LABEL" _seed_stay_set(tmp_path, wid) save_config(tmp_path, {"backends": ["git-pointers", "sqlite"]}) - db_path = tmp_path / ".sdlc" / "index.sqlite" + db_path = tmp_path / "sdlc-spdd" / ".sdlc" / "index.sqlite" _write_v4_sqlite(db_path, work_id=wid, schema_version=SCHEMA_VERSION) store = ContextStore(Project(tmp_path), guide_base_url="http://127.0.0.1:9") @@ -205,7 +205,7 @@ def test_persist_upgrades_stale_v5_label_with_v4_columns(tmp_path: Path) -> None def test_ensure_schema_rebuilds_unreadable_legacy_db(tmp_path: Path) -> None: _seed_stay_set(tmp_path, "FEAT-NO-META") - db_path = tmp_path / ".sdlc" / "index.sqlite" + db_path = tmp_path / "sdlc-spdd" / ".sdlc" / "index.sqlite" _write_unreadable_legacy_sqlite(db_path) idx = LocalIndex(Project(tmp_path)) idx.ensure_schema() diff --git a/engine/tests_unit/test_engine_shared.py b/engine/tests_unit/test_engine_shared.py index 2c2d6d57..b61cd295 100644 --- a/engine/tests_unit/test_engine_shared.py +++ b/engine/tests_unit/test_engine_shared.py @@ -57,13 +57,13 @@ def test_link_normalizers_share_placeholder_set() -> None: def test_project_rel_and_json_io(tmp_path: Path) -> None: project = Project(tmp_path) - inside = tmp_path / "spdd" / "memory" / "lessons.jsonl" + inside = tmp_path / "sdlc-spdd" / "spdd" / "memory" / "lessons.jsonl" inside.parent.mkdir(parents=True) inside.write_text("{}\n", encoding="utf-8") - assert project.rel(inside) == "spdd/memory/lessons.jsonl" + assert project.rel(inside) == "sdlc-spdd/spdd/memory/lessons.jsonl" assert rel_to(tmp_path, Path("/tmp/outside-engine-rel")) == "/tmp/outside-engine-rel" - cfg = tmp_path / ".sdlc" / "runtime.json" + cfg = tmp_path / "sdlc-spdd" / ".sdlc" / "runtime.json" assert load_json_dict(cfg) == {} save_json_dict(cfg, {"ok": True}) assert load_json_dict(cfg) == {"ok": True} @@ -83,7 +83,7 @@ def test_process_util_pid_and_run() -> None: def test_cli_and_db_module_split_keeps_public_imports() -> None: from sdlc_engine.cli import build_parser, main - from sdlc_engine.cli_commands import cmd_next, cmd_version + from sdlc_engine.commands import cmd_next, cmd_version from sdlc_engine.db import SCHEMA_VERSION, LocalIndex, RebuildStats, format_rows from sdlc_engine.db_query import IndexQueryMixin from sdlc_engine.db_rebuild import IndexRebuildMixin diff --git a/engine/tests_unit/test_guide_client.py b/engine/tests_unit/test_guide_client.py index aca880b6..67fb573d 100644 --- a/engine/tests_unit/test_guide_client.py +++ b/engine/tests_unit/test_guide_client.py @@ -5,7 +5,6 @@ import json from unittest.mock import MagicMock, patch -import pytest from sdlc_engine.guide_client import GuideClient, SPDD_MCP_TOOLS diff --git a/engine/tests_unit/test_hard_review_gaps.py b/engine/tests_unit/test_hard_review_gaps.py index 92f55ff9..ffb4d2db 100644 --- a/engine/tests_unit/test_hard_review_gaps.py +++ b/engine/tests_unit/test_hard_review_gaps.py @@ -3,13 +3,10 @@ from __future__ import annotations import json -import os import subprocess from pathlib import Path -import pytest -from sdlc_engine.agent_context_upgrade import AgentContextUpgrade from sdlc_engine.cli import main from sdlc_engine.context_store import ContextStore from sdlc_engine.db import LocalIndex @@ -22,15 +19,15 @@ def _seed_stay_set(root: Path, work_id: str, *, ready_for_coding: bool = False) -> None: - (root / "requirements" / "milestones").mkdir(parents=True, exist_ok=True) - (root / "requirements" / "milestones" / f"{work_id}.md").write_text( + (root / "sdlc-spdd" / "requirements" / "milestones").mkdir(parents=True, exist_ok=True) + (root / "sdlc-spdd" / "requirements" / "milestones" / f"{work_id}.md").write_text( f"# Requirement: {work_id}\n\n## Summary\nHard review.\n", encoding="utf-8", ) status = "Ready For Coding" if ready_for_coding else "In Progress" readiness = "Ready For Coding" if ready_for_coding else "Needs Analysis" - (root / "spdd" / "canvas").mkdir(parents=True, exist_ok=True) - (root / "spdd" / "canvas" / f"{work_id}.md").write_text( + (root / "sdlc-spdd" / "spdd" / "canvas").mkdir(parents=True, exist_ok=True) + (root / "sdlc-spdd" / "spdd" / "canvas" / f"{work_id}.md").write_text( f"""# REASONS Canvas: {work_id} ## Metadata @@ -56,7 +53,7 @@ def _seed_stay_set(root: Path, work_id: str, *, ready_for_coding: bool = False) def _staged_records(root: Path) -> list[dict]: - staged = root / ".sdlc" / "staged" / "lessons.jsonl" + staged = root / "sdlc-spdd" / ".sdlc" / "staged" / "lessons.jsonl" if not staged.is_file(): return [] return [ @@ -69,7 +66,7 @@ def _staged_records(root: Path) -> list[dict]: def test_capture_stages_session_record_only(tmp_path: Path) -> None: wid = "FEAT-940-capture-lean" _seed_stay_set(tmp_path, wid) - hot = tmp_path / ".sdlc" / "sessions" + hot = tmp_path / "sdlc-spdd" / ".sdlc" / "sessions" hot.mkdir(parents=True) (hot / "current-session.md").write_text( "# Hot brief\nTouching scripts/lib for capture.\n", @@ -103,9 +100,9 @@ def test_capture_stages_session_record_only(tmp_path: Path) -> None: assert sessions assert sessions[-1]["area"] == "scripts/lib" # Committed ledger untouched; no legacy index/mirror trees created. - assert not (tmp_path / "spdd" / "memory" / "lessons.jsonl").exists() - assert not (tmp_path / "spdd" / "memory" / "context-index.md").exists() - assert not (tmp_path / "spdd" / "memory" / "entries").exists() + assert not (tmp_path / "sdlc-spdd" / "spdd" / "memory" / "lessons.jsonl").exists() + assert not (tmp_path / "sdlc-spdd" / "spdd" / "memory" / "context-index.md").exists() + assert not (tmp_path / "sdlc-spdd" / "spdd" / "memory" / "entries").exists() assert not (tmp_path / "agent-context" / "features").exists() @@ -128,55 +125,16 @@ def test_create_feature_stages_record_no_mirrors(tmp_path: Path) -> None: check=False, ) assert proc.returncode == 0, proc.stderr + proc.stdout - canvases = list((tmp_path / "spdd" / "canvas").glob("FEAT-*.md")) + canvases = list((tmp_path / "sdlc-spdd" / "spdd" / "canvas").glob("FEAT-*.md")) assert len(canvases) == 1 wid = canvases[0].stem - assert (tmp_path / "requirements" / "milestones" / f"{wid}.md").is_file() + assert (tmp_path / "sdlc-spdd" / "requirements" / "milestones" / f"{wid}.md").is_file() assert not (tmp_path / "agent-context" / "features").exists() - assert not (tmp_path / "spdd" / "memory" / "entries").exists() + assert not (tmp_path / "sdlc-spdd" / "spdd" / "memory" / "entries").exists() records = _staged_records(tmp_path) assert any(r["kind"] == "session" and r["work_id"] == wid for r in records) -def test_upgrade_memory_only_exports_then_idempotent(tmp_path: Path) -> None: - mem = tmp_path / "agent-context" / "memory" - mem.mkdir(parents=True) - (mem / "context-index.md").write_text( - """# Context Index - -| Area | Kind | Work ID | Phase | Timestamp | Source | Entry | -|------|------|---------|-------|-----------|--------|-------| -| engine | pitfall | FEAT-X | sync | 2026-08-08T00:00:00Z | t | export me | -""", - encoding="utf-8", - ) - up = AgentContextUpgrade(Project(tmp_path)) - first = up.run(dry_run=False, rebuild_db=False) - assert first.ok - assert first.moved - assert (tmp_path / ".sdlc" / "storage-v3-migrated").is_file() - second = up.run(dry_run=False, rebuild_db=False) - assert second.ok - assert any("delegated" in n.lower() or "idempotent" in n.lower() for n in second.notes) - - -def test_upgrade_archives_features_then_second_run_idempotent(tmp_path: Path) -> None: - wid = "FEAT-941-up" - feat = tmp_path / "agent-context" / "features" / wid - feat.mkdir(parents=True) - (feat / "progress-log.md").write_text("x\n", encoding="utf-8") - sess = tmp_path / "agent-context" / "sessions" - sess.mkdir(parents=True) - (sess / "current-session.md").write_text("legacy\n", encoding="utf-8") - up = AgentContextUpgrade(Project(tmp_path)) - a = up.run(dry_run=False, rebuild_db=False) - assert a.ok - assert a.moved - assert not (tmp_path / "agent-context" / "features").exists() - b = up.run(dry_run=False, rebuild_db=False) - assert b.ok - - def test_cli_context_retrieve_assembles_paths(tmp_path: Path, capsys) -> None: wid = "FEAT-942-retrieve" area = "scripts/lib" @@ -226,7 +184,7 @@ def test_registry_lean_jsonl_and_sqlite_on_claim(tmp_path: Path, monkeypatch) -> reg = TeamRegistry(Project(tmp_path)) row = reg.claim(wid, phase="code", note="lean registry proof") assert row.status == "active" - lean = tmp_path / "spdd" / "memory" / "registry.jsonl" + lean = tmp_path / "sdlc-spdd" / "spdd" / "memory" / "registry.jsonl" assert lean.is_file() events = reg.lean_events(work_id=wid) assert len(events) >= 1 diff --git a/engine/tests_unit/test_installer_playground.py b/engine/tests_unit/test_installer_playground.py index 4b61dfe1..ced5482f 100644 --- a/engine/tests_unit/test_installer_playground.py +++ b/engine/tests_unit/test_installer_playground.py @@ -28,26 +28,26 @@ def test_materialize_playground_writes_tabs_data(tmp_path: Path) -> None: dest = materialize_playground(tmp_path / "play", orch=tmp_path) assert is_playground(dest) active = WORKS[0][0] - assert (dest / ".sdlc" / "pointer").read_text(encoding="utf-8").strip() == active - assert (dest / "spdd" / "canvas" / f"{active}.md").is_file() - assert (dest / "requirements" / "milestones" / f"{active}.md").is_file() + assert (dest / "sdlc-spdd" / ".sdlc" / "pointer").read_text(encoding="utf-8").strip() == active + assert (dest / "sdlc-spdd" / "spdd" / "canvas" / f"{active}.md").is_file() + assert (dest / "sdlc-spdd" / "requirements" / "milestones" / f"{active}.md").is_file() assert (dest / "adf" / f"{active}.adf.json").is_file() - assert (dest / "spdd" / "memory" / "lessons.jsonl").read_text(encoding="utf-8").count("\n") == 3 - assert (dest / "spdd" / "memory" / "registry.jsonl").is_file() - assert (dest / ".sdlc" / "staged" / "lessons.jsonl").is_file() + assert (dest / "sdlc-spdd" / "spdd" / "memory" / "lessons.jsonl").read_text(encoding="utf-8").count("\n") == 3 + assert (dest / "sdlc-spdd" / "spdd" / "memory" / "registry.jsonl").is_file() + assert (dest / "sdlc-spdd" / ".sdlc" / "staged" / "lessons.jsonl").is_file() assert (dest / "PLAYGROUND.md").is_file() backups = list((dest / ".sdlc-spdd-upgrade-backups").iterdir()) assert backups - assert (dest / ".sdlc" / "persistence-config.json").is_file() - assert (dest / ".sdlc" / "integrations-config.json").is_file() - integ = (dest / ".sdlc" / "integrations-config.json").read_text(encoding="utf-8") + assert (dest / "sdlc-spdd" / ".sdlc" / "persistence-config.json").is_file() + assert (dest / "sdlc-spdd" / ".sdlc" / "integrations-config.json").is_file() + integ = (dest / "sdlc-spdd" / ".sdlc" / "integrations-config.json").read_text(encoding="utf-8") assert FAKE_JIRA_TOKEN in integ assert FAKE_GH_TOKEN in integ - assert (dest / ".sdlc" / "fake-guide" / "compose.yaml").is_file() - assert (dest / ".sdlc" / "fake-guide" / "scripts" / "append-ingest.sh").is_file() - assert (dest / ".sdlc" / "playground-runtime.json").is_file() + assert (dest / "sdlc-spdd" / ".sdlc" / "fake-guide" / "compose.yaml").is_file() + assert (dest / "sdlc-spdd" / ".sdlc" / "fake-guide" / "scripts" / "append-ingest.sh").is_file() + assert (dest / "sdlc-spdd" / ".sdlc" / "playground-runtime.json").is_file() jira_key, gh_num = issue_refs(active) - req = (dest / "requirements" / "milestones" / f"{active}.md").read_text(encoding="utf-8") + req = (dest / "sdlc-spdd" / "requirements" / "milestones" / f"{active}.md").read_text(encoding="utf-8") assert jira_key in req assert gh_num in req @@ -83,7 +83,7 @@ def test_health_reports_playground(tmp_path: Path) -> None: def test_fake_guide_start_stop_and_ingest(tmp_path: Path) -> None: dest = materialize_playground(tmp_path / "play") - cfg = {"guide_home": str(dest / ".sdlc" / "fake-guide"), "host": "127.0.0.1", "port": 21337} + cfg = {"guide_home": str(dest / "sdlc-spdd" / ".sdlc" / "fake-guide"), "host": "127.0.0.1", "port": 21337} payload = fake_guide_payload(dest, cfg, orch=tmp_path) assert payload["playground"] is True assert payload["probe"]["tcp_open"] is True @@ -125,13 +125,13 @@ def test_fake_issue_sync_dry_run_and_apply(tmp_path: Path) -> None: dest, work_id=work_id, system="github", direction="push", apply=True ) assert applied["ok"] is True - req = (dest / "requirements" / "milestones" / f"{work_id}.md").read_text(encoding="utf-8") + req = (dest / "sdlc-spdd" / "requirements" / "milestones" / f"{work_id}.md").read_text(encoding="utf-8") assert "Playground github push" in req def test_fake_guide_remaining_actions(tmp_path: Path) -> None: dest = materialize_playground(tmp_path / "play") - cfg = {"guide_home": str(dest / ".sdlc" / "fake-guide"), "profile": "sdlc-spdd"} + cfg = {"guide_home": str(dest / "sdlc-spdd" / ".sdlc" / "fake-guide"), "profile": "sdlc-spdd"} assert fake_guide_action(dest, cfg, "ensure")["ok"] is True assert fake_guide_action(dest, cfg, "neo4j_stop")["ok"] is True assert load_runtime(dest)["neo4j_up"] is False diff --git a/engine/tests_unit/test_installer_runtime_units.py b/engine/tests_unit/test_installer_runtime_units.py index c2b2460d..af1dba74 100644 --- a/engine/tests_unit/test_installer_runtime_units.py +++ b/engine/tests_unit/test_installer_runtime_units.py @@ -107,7 +107,7 @@ def test_guide_runtime_start_stop_guide( started = gr.start_guide(tmp_path, cfg, ingest=False, ensure_neo4j=True) assert started["ok"] is True assert started["pid"] == 5555 - assert (tmp_path / ".sdlc" / "guide-runtime.json").is_file() + assert (tmp_path / "sdlc-spdd" / ".sdlc" / "guide-runtime.json").is_file() again = gr.start_guide(tmp_path, cfg, ingest=False, ensure_neo4j=False) assert again["ok"] is False @@ -361,8 +361,8 @@ def __exit__(self, *a: Any) -> None: assert probe["http_ok"] is True # Corrupt runtime file - rt = tmp_path / ".sdlc" - rt.mkdir() + rt = tmp_path / "sdlc-spdd" / ".sdlc" + rt.mkdir(parents=True) (rt / "adf-viewer-runtime.json").write_text("{not-json", encoding="utf-8") assert vr._load_runtime(tmp_path) == {} @@ -462,8 +462,8 @@ def raise_url(*a: Any, **k: Any) -> Any: assert failed_neo["ok"] is False # stop_guide killpg/kill branches - (tmp_path / ".sdlc").mkdir(exist_ok=True) - (tmp_path / ".sdlc" / "guide-runtime.json").write_text( + (tmp_path / "sdlc-spdd" / ".sdlc").mkdir(exist_ok=True) + (tmp_path / "sdlc-spdd" / ".sdlc" / "guide-runtime.json").write_text( json.dumps({"pid": 7777, "port": 21339}), encoding="utf-8" ) state = {"n": 0} diff --git a/engine/tests_unit/test_installer_templates_api.py b/engine/tests_unit/test_installer_templates_api.py index f7daaed9..c76ad78a 100644 --- a/engine/tests_unit/test_installer_templates_api.py +++ b/engine/tests_unit/test_installer_templates_api.py @@ -12,8 +12,8 @@ def _seed_work(root: Path, work_id: str) -> None: - (root / "requirements" / "milestones").mkdir(parents=True) - (root / "requirements" / "milestones" / f"{work_id}.md").write_text( + (root / "sdlc-spdd" / "requirements" / "milestones").mkdir(parents=True) + (root / "sdlc-spdd" / "requirements" / "milestones" / f"{work_id}.md").write_text( f"## Summary\n\nTemplate API for {work_id}.\n", encoding="utf-8", ) diff --git a/engine/tests_unit/test_integration_config.py b/engine/tests_unit/test_integration_config.py index 66d7ec85..1cb6c3ab 100644 --- a/engine/tests_unit/test_integration_config.py +++ b/engine/tests_unit/test_integration_config.py @@ -8,7 +8,6 @@ from sdlc_engine.integration_config import ( integration_env, - load_config, resolve_integrations, save_config, status_dict, @@ -91,4 +90,4 @@ def test_v3_home_writes_under_sdlc_spdd_runtime(tmp_path: Path) -> None: save_config(project, {"tracker": "github", "github": {"repo": "acme/app"}}) cfg = tmp_path / "sdlc-spdd" / ".sdlc" / "integrations-config.json" assert cfg.is_file() - assert not (tmp_path / ".sdlc" / "integrations-config.json").exists() + assert not (tmp_path / ".sdlc").exists() diff --git a/engine/tests_unit/test_issues_mocked.py b/engine/tests_unit/test_issues_mocked.py index 852b91f8..994a187f 100644 --- a/engine/tests_unit/test_issues_mocked.py +++ b/engine/tests_unit/test_issues_mocked.py @@ -12,7 +12,7 @@ def _seed_req(root: Path, work_id: str, *, jira_key: str = "TBD", gh: str = "") -> Path: - req = root / "requirements" / "milestones" / f"{work_id}.md" + req = root / "sdlc-spdd" / "requirements" / "milestones" / f"{work_id}.md" req.parent.mkdir(parents=True, exist_ok=True) req.write_text( f"""# Requirement: {work_id} @@ -42,7 +42,7 @@ def _seed_req(root: Path, work_id: str, *, jira_key: str = "TBD", gh: str = "") """, encoding="utf-8", ) - canvas = root / "spdd" / "canvas" / f"{work_id}.md" + canvas = root / "sdlc-spdd" / "spdd" / "canvas" / f"{work_id}.md" canvas.parent.mkdir(parents=True, exist_ok=True) canvas.write_text( f"""# REASONS Canvas: {work_id} - Sync demo @@ -104,11 +104,11 @@ def fake_urlopen(req, timeout=30): # noqa: ANN001 payload = json.loads(seen["body"].decode()) desc = payload["fields"]["description"] assert isinstance(desc, dict) and desc.get("type") == "doc" - text = (tmp_path / "requirements" / "milestones" / f"{work_id}.md").read_text( + text = (tmp_path / "sdlc-spdd" / "requirements" / "milestones" / f"{work_id}.md").read_text( encoding="utf-8" ) assert "Key: ORCH-99" in text - canvas = (tmp_path / "spdd" / "canvas" / f"{work_id}.md").read_text(encoding="utf-8") + canvas = (tmp_path / "sdlc-spdd" / "spdd" / "canvas" / f"{work_id}.md").read_text(encoding="utf-8") assert "Source Issue: ORCH-99" in canvas assert "Source System: Jira" in canvas @@ -148,7 +148,7 @@ def fake_urlopen(req, timeout=30): # noqa: ANN001 svc = IssueSyncService(Project(tmp_path), urlopen=fake_urlopen) report = svc.pull(work_id, "jira", apply=True) assert "Pulled summary from Jira" in report - text = (tmp_path / "requirements" / "milestones" / f"{work_id}.md").read_text( + text = (tmp_path / "sdlc-spdd" / "requirements" / "milestones" / f"{work_id}.md").read_text( encoding="utf-8" ) assert "Summary: Pulled summary from Jira" in text @@ -171,12 +171,12 @@ def fake_gh(cmd: list[str], cwd: Path) -> CompletedProcess: svc = IssueSyncService(Project(tmp_path), gh_runner=fake_gh) out = svc.push(work_id, "github", apply=True) assert "4242" in out - text = (tmp_path / "requirements" / "milestones" / f"{work_id}.md").read_text( + text = (tmp_path / "sdlc-spdd" / "requirements" / "milestones" / f"{work_id}.md").read_text( encoding="utf-8" ) assert "Number: 4242" in text assert "URL: https://github.com/example/repo/issues/4242" in text - canvas = (tmp_path / "spdd" / "canvas" / f"{work_id}.md").read_text(encoding="utf-8") + canvas = (tmp_path / "sdlc-spdd" / "spdd" / "canvas" / f"{work_id}.md").read_text(encoding="utf-8") assert "Source Issue: #4242" in canvas or "Source Issue: 4242" in canvas @@ -199,7 +199,7 @@ def fake_gh(cmd: list[str], cwd: Path) -> CompletedProcess: svc = IssueSyncService(Project(tmp_path), gh_runner=fake_gh) report = svc.pull(work_id, "github", apply=True) assert "Remote GH title" in report - text = (tmp_path / "requirements" / "milestones" / f"{work_id}.md").read_text( + text = (tmp_path / "sdlc-spdd" / "requirements" / "milestones" / f"{work_id}.md").read_text( encoding="utf-8" ) assert "Title: Remote GH title" in text @@ -228,7 +228,7 @@ def fake_gh(cmd: list[str], cwd: Path) -> CompletedProcess: def test_empty_metadata_bullets_do_not_swallow_next_line(tmp_path: Path) -> None: from sdlc_engine.links import parse_canvas_metadata - canvas = tmp_path / "spdd" / "canvas" / "FEAT-306-empty.md" + canvas = tmp_path / "sdlc-spdd" / "spdd" / "canvas" / "FEAT-306-empty.md" canvas.parent.mkdir(parents=True, exist_ok=True) canvas.write_text( """# REASONS Canvas: FEAT-306-empty - Empty bullets diff --git a/engine/tests_unit/test_jira_format.py b/engine/tests_unit/test_jira_format.py index da953b1d..adaf2bba 100644 --- a/engine/tests_unit/test_jira_format.py +++ b/engine/tests_unit/test_jira_format.py @@ -100,7 +100,7 @@ def test_build_github_markdown_matches_jira_sections() -> None: def test_github_draft_from_structured_sections(tmp_path: Path) -> None: work_id = "FEAT-502-gh-template" - req = tmp_path / "requirements" / "milestones" / f"{work_id}.md" + req = tmp_path / "sdlc-spdd" / "requirements" / "milestones" / f"{work_id}.md" req.parent.mkdir(parents=True) req.write_text( f"""# Requirement: {work_id} @@ -146,7 +146,7 @@ def test_jira_push_sends_adf_on_cloud(tmp_path: Path, monkeypatch) -> None: monkeypatch.setenv("JIRA_API_TOKEN", "token") monkeypatch.setenv("JIRA_PROJECT", "ORCH") work_id = "FEAT-402-adf-push" - req = tmp_path / "requirements" / "milestones" / f"{work_id}.md" + req = tmp_path / "sdlc-spdd" / "requirements" / "milestones" / f"{work_id}.md" req.parent.mkdir(parents=True) req.write_text( f"""# Requirement: {work_id} @@ -174,8 +174,8 @@ def test_jira_push_sends_adf_on_cloud(tmp_path: Path, monkeypatch) -> None: """, encoding="utf-8", ) - (tmp_path / "spdd" / "canvas").mkdir(parents=True) - (tmp_path / "spdd" / "canvas" / f"{work_id}.md").write_text( + (tmp_path / "sdlc-spdd" / "spdd" / "canvas").mkdir(parents=True) + (tmp_path / "sdlc-spdd" / "spdd" / "canvas" / f"{work_id}.md").write_text( f"# REASONS Canvas: {work_id}\n\n## Metadata\n\n- Work ID: {work_id}\n" "- Source System:\n- Source Issue:\n\n## Final Status\n\n- Status: Draft\n", encoding="utf-8", @@ -219,7 +219,7 @@ def test_jira_push_wiki_when_forced(tmp_path: Path, monkeypatch) -> None: monkeypatch.setenv("JIRA_API_VERSION", "2") monkeypatch.setenv("JIRA_DESCRIPTION_FORMAT", "wiki") work_id = "FEAT-403-wiki" - req = tmp_path / "requirements" / "milestones" / f"{work_id}.md" + req = tmp_path / "sdlc-spdd" / "requirements" / "milestones" / f"{work_id}.md" req.parent.mkdir(parents=True) req.write_text( f"""# Requirement: {work_id} @@ -239,8 +239,8 @@ def test_jira_push_wiki_when_forced(tmp_path: Path, monkeypatch) -> None: """, encoding="utf-8", ) - (tmp_path / "spdd" / "canvas").mkdir(parents=True) - (tmp_path / "spdd" / "canvas" / f"{work_id}.md").write_text( + (tmp_path / "sdlc-spdd" / "spdd" / "canvas").mkdir(parents=True) + (tmp_path / "sdlc-spdd" / "spdd" / "canvas" / f"{work_id}.md").write_text( f"# C\n\n## Metadata\n\n- Work ID: {work_id}\n- Source Issue:\n\n## Final Status\n\n- Status: Draft\n", encoding="utf-8", ) @@ -272,7 +272,7 @@ def test_jira_pull_adf_description_to_milestone(tmp_path: Path, monkeypatch) -> monkeypatch.setenv("JIRA_EMAIL", "bot@example.com") monkeypatch.setenv("JIRA_API_TOKEN", "token") work_id = "FEAT-404-pull-adf" - req = tmp_path / "requirements" / "milestones" / f"{work_id}.md" + req = tmp_path / "sdlc-spdd" / "requirements" / "milestones" / f"{work_id}.md" req.parent.mkdir(parents=True) req.write_text( f"""# Requirement: {work_id} @@ -411,7 +411,7 @@ def test_jira_push_updates_existing_key_with_raw_adf(tmp_path: Path, monkeypatch monkeypatch.setenv("JIRA_PROJECT", "ORCH") monkeypatch.delenv("JIRA_DESCRIPTION_FALLBACK", raising=False) work_id = "FEAT-405-update-adf" - req = tmp_path / "requirements" / "milestones" / f"{work_id}.md" + req = tmp_path / "sdlc-spdd" / "requirements" / "milestones" / f"{work_id}.md" req.parent.mkdir(parents=True) req.write_text( f"""# Requirement: {work_id} @@ -639,7 +639,7 @@ def test_no_silent_adf_to_wiki_fallback_by_default(tmp_path: Path, monkeypatch) monkeypatch.setenv("JIRA_PROJECT", "ORCH") monkeypatch.delenv("JIRA_DESCRIPTION_FALLBACK", raising=False) work_id = "FEAT-406-no-fallback" - req = tmp_path / "requirements" / "milestones" / f"{work_id}.md" + req = tmp_path / "sdlc-spdd" / "requirements" / "milestones" / f"{work_id}.md" req.parent.mkdir(parents=True) req.write_text( f"""# Requirement: {work_id} diff --git a/engine/tests_unit/test_local_sessions.py b/engine/tests_unit/test_local_sessions.py index 49bd130d..a9621e81 100644 --- a/engine/tests_unit/test_local_sessions.py +++ b/engine/tests_unit/test_local_sessions.py @@ -15,14 +15,14 @@ def test_local_start_capture_promote(tmp_path: Path, monkeypatch) -> None: session = svc.start(name="scratch-sync", intent="Explore detached agent capture") assert is_local_id(session.id) assert session.id.startswith("LOCAL-") - assert (tmp_path / ".sdlc" / "pointer").read_text(encoding="utf-8").strip() == session.id - assert (tmp_path / ".sdlc" / "local-sessions" / session.id / "session.json").is_file() - assert (tmp_path / ".sdlc" / "current-local-session.md").is_file() - assert not (tmp_path / ".sdlc" / "local-sessions" / session.id / "brief.md").is_file() + assert (tmp_path / "sdlc-spdd" / ".sdlc" / "pointer").read_text(encoding="utf-8").strip() == session.id + assert (tmp_path / "sdlc-spdd" / ".sdlc" / "local-sessions" / session.id / "session.json").is_file() + assert (tmp_path / "sdlc-spdd" / ".sdlc" / "current-local-session.md").is_file() + assert not (tmp_path / "sdlc-spdd" / ".sdlc" / "local-sessions" / session.id / "brief.md").is_file() assert not (tmp_path / "agent-context" / "sessions" / "current-session.md").is_file() svc.capture("Tried a detached approach") - notes = (tmp_path / ".sdlc" / "local-sessions" / session.id / "notes.md").read_text( + notes = (tmp_path / "sdlc-spdd" / ".sdlc" / "local-sessions" / session.id / "notes.md").read_text( encoding="utf-8" ) assert "Tried a detached approach" in notes @@ -36,18 +36,18 @@ def test_local_start_capture_promote(tmp_path: Path, monkeypatch) -> None: assert session.status == "promoted" assert session.promoted_to == work_id assert work_id.startswith("FEAT-") - assert (tmp_path / "spdd" / "canvas" / f"{work_id}.md").is_file() - assert (tmp_path / "requirements" / "milestones" / f"{work_id}.md").is_file() + assert (tmp_path / "sdlc-spdd" / "spdd" / "canvas" / f"{work_id}.md").is_file() + assert (tmp_path / "sdlc-spdd" / "requirements" / "milestones" / f"{work_id}.md").is_file() # Stay-set only (#86) — promote must not create feature mirrors. assert not (tmp_path / "agent-context" / "features" / work_id).exists() - progress = (tmp_path / ".sdlc" / "staged" / "lessons.jsonl").read_text( + progress = (tmp_path / "sdlc-spdd" / ".sdlc" / "staged" / "lessons.jsonl").read_text( encoding="utf-8" ) assert work_id in progress assert session.id in progress - pointer = (tmp_path / ".sdlc" / "pointer").read_text(encoding="utf-8").strip() + pointer = (tmp_path / "sdlc-spdd" / ".sdlc" / "pointer").read_text(encoding="utf-8").strip() assert pointer == work_id - reg = (tmp_path / "spdd" / "memory" / "registry.jsonl").read_text(encoding="utf-8") + reg = (tmp_path / "sdlc-spdd" / "spdd" / "memory" / "registry.jsonl").read_text(encoding="utf-8") assert work_id in reg assert f"promoted-from:{session.id}" in reg @@ -80,9 +80,9 @@ def test_next_without_pointer_hints_local_start(tmp_path: Path) -> None: def test_cli_quick_start(tmp_path: Path, monkeypatch) -> None: monkeypatch.setenv("SDLC_USER", "cli") assert main(["--root", str(tmp_path), "quick", "fix the flaky test"]) == 0 - pointer = (tmp_path / ".sdlc" / "pointer").read_text(encoding="utf-8").strip() + pointer = (tmp_path / "sdlc-spdd" / ".sdlc" / "pointer").read_text(encoding="utf-8").strip() assert pointer.startswith("LOCAL-") - brief = (tmp_path / ".sdlc" / "current-local-session.md").read_text(encoding="utf-8") + brief = (tmp_path / "sdlc-spdd" / ".sdlc" / "current-local-session.md").read_text(encoding="utf-8") assert "fix the flaky test" in brief @@ -114,11 +114,11 @@ def test_promote_from_git_backfill(tmp_path: Path, monkeypatch) -> None: from_git="main..HEAD", claim=False, ) - notes = (root / ".sdlc" / "local-sessions" / session.id / "notes.md").read_text( + notes = (root / "sdlc-spdd" / ".sdlc" / "local-sessions" / session.id / "notes.md").read_text( encoding="utf-8" ) assert "add feat file" in notes - canvas = (root / "spdd" / "canvas" / f"{work_id}.md").read_text(encoding="utf-8") + canvas = (root / "sdlc-spdd" / "spdd" / "canvas" / f"{work_id}.md").read_text(encoding="utf-8") assert "add feat file" in canvas diff --git a/engine/tests_unit/test_persistence_options.py b/engine/tests_unit/test_persistence_options.py index 9349f9cb..8ce9c99a 100644 --- a/engine/tests_unit/test_persistence_options.py +++ b/engine/tests_unit/test_persistence_options.py @@ -5,7 +5,6 @@ import json from pathlib import Path -import pytest from sdlc_engine.cli import main from sdlc_engine.context_store import ContextStore @@ -36,7 +35,7 @@ def test_save_and_load_persistence_config(tmp_path: Path) -> None: assert BACKEND_GIT in saved["backends"] assert BACKEND_SQLITE in saved["backends"] assert BACKEND_GUIDE not in saved["backends"] - assert (tmp_path / ".sdlc" / "persistence-config.json").is_file() + assert (tmp_path / "sdlc-spdd" / ".sdlc" / "persistence-config.json").is_file() status = status_dict(Project(tmp_path)) assert status["enabled"]["sqlite"] is True assert status["enabled"]["guide-dice"] is False @@ -54,8 +53,8 @@ def test_env_CONTEXT_BACKENDS_overrides_file(tmp_path: Path, monkeypatch) -> Non def test_persist_skips_disabled_sqlite_and_guide(tmp_path: Path, monkeypatch) -> None: monkeypatch.setenv("CONTEXT_BACKENDS", "git-pointers") project = Project(tmp_path) - (tmp_path / "spdd" / "canvas").mkdir(parents=True) - (tmp_path / "spdd" / "canvas" / "FEAT-persist-off.md").write_text( + (tmp_path / "sdlc-spdd" / "spdd" / "canvas").mkdir(parents=True) + (tmp_path / "sdlc-spdd" / "spdd" / "canvas" / "FEAT-persist-off.md").write_text( "# canvas\n", encoding="utf-8" ) store = ContextStore(project) @@ -70,7 +69,7 @@ def test_persist_skips_disabled_sqlite_and_guide(tmp_path: Path, monkeypatch) -> assert result.guide.get("skipped") is True assert result.ok is True assert result.partial is False - assert not (tmp_path / ".sdlc" / "index.sqlite").is_file() + assert not (tmp_path / "sdlc-spdd" / ".sdlc" / "index.sqlite").is_file() def test_cli_context_backends_set(tmp_path: Path, monkeypatch, capsys) -> None: @@ -100,7 +99,7 @@ def test_ledger_lessons_ingested_on_rebuild(tmp_path: Path) -> None: from sdlc_engine.lessons_ledger import LessonRecord, LessonsLedger wid = "FEAT-lean-progress-only" - tmp_path.joinpath("spdd/memory").mkdir(parents=True) + tmp_path.joinpath("sdlc-spdd/spdd/memory").mkdir(parents=True) LessonsLedger(Project(tmp_path)).append_accepted( LessonRecord( id="", @@ -121,7 +120,7 @@ def test_ledger_staged_flag_on_rebuild(tmp_path: Path) -> None: from sdlc_engine.lessons_ledger import LessonRecord, LessonsLedger wid = "FEAT-capture-progress" - tmp_path.joinpath("spdd/memory").mkdir(parents=True) + tmp_path.joinpath("sdlc-spdd/spdd/memory").mkdir(parents=True) ledger = LessonsLedger(Project(tmp_path)) ledger.stage( LessonRecord( @@ -147,8 +146,8 @@ def test_persist_partial_when_sqlite_fails(tmp_path: Path, monkeypatch) -> None: monkeypatch.delenv("CONTEXT_BACKENDS", raising=False) save_config(tmp_path, {"backends": ["git-pointers", "sqlite"]}) project = Project(tmp_path) - (tmp_path / "spdd" / "canvas").mkdir(parents=True) - (tmp_path / "spdd" / "canvas" / "FEAT-partial.md").write_text( + (tmp_path / "sdlc-spdd" / "spdd" / "canvas").mkdir(parents=True) + (tmp_path / "sdlc-spdd" / "spdd" / "canvas" / "FEAT-partial.md").write_text( "# canvas\n", encoding="utf-8" ) store = ContextStore(project) @@ -168,14 +167,14 @@ def boom(*_a, **_k): assert result.partial is True assert result.sqlite.get("ok") is False assert any("sqlite:" in e for e in result.errors) - assert (tmp_path / ".sdlc" / "staged" / "lessons.jsonl").is_file() + assert (tmp_path / "sdlc-spdd" / ".sdlc" / "staged" / "lessons.jsonl").is_file() def test_retrieve_honors_backend_gating(tmp_path: Path, monkeypatch) -> None: monkeypatch.setenv("CONTEXT_BACKENDS", "git-pointers") project = Project(tmp_path) - (tmp_path / "spdd" / "canvas").mkdir(parents=True) - (tmp_path / "spdd" / "canvas" / "FEAT-retrieve-off.md").write_text( + (tmp_path / "sdlc-spdd" / "spdd" / "canvas").mkdir(parents=True) + (tmp_path / "sdlc-spdd" / "spdd" / "canvas" / "FEAT-retrieve-off.md").write_text( "# canvas\n", encoding="utf-8" ) # Seed sqlite while backends allow it, then gate retrieve off. @@ -202,13 +201,13 @@ def test_workflow_next_honors_quiet(tmp_path: Path, monkeypatch) -> None: monkeypatch.setenv("SDLC_QUIET", "ON") project = Project(tmp_path) wid = "FEAT-quiet-next" - (tmp_path / "requirements" / "milestones").mkdir(parents=True) - (tmp_path / "requirements" / "milestones" / f"{wid}.md").write_text( + (tmp_path / "sdlc-spdd" / "requirements" / "milestones").mkdir(parents=True) + (tmp_path / "sdlc-spdd" / "requirements" / "milestones" / f"{wid}.md").write_text( f"# Requirement: {wid}\n\n## Summary\nQuiet mode test.\n", encoding="utf-8", ) - (tmp_path / "spdd" / "canvas").mkdir(parents=True) - (tmp_path / "spdd" / "canvas" / f"{wid}.md").write_text( + (tmp_path / "sdlc-spdd" / "spdd" / "canvas").mkdir(parents=True) + (tmp_path / "sdlc-spdd" / "spdd" / "canvas" / f"{wid}.md").write_text( f"# REASONS Canvas: {wid}\n\n## Metadata\n- Readiness: Ready For Coding\n\n## R - Requirements\nQuiet-mode fixture requirement.\n\n## O - Operations\n\n### T01 - Do\n- Status: Not Started\n- Files: src/app.py\n", encoding="utf-8", ) @@ -230,19 +229,19 @@ def test_workflow_infers_code_from_ledger_records(tmp_path: Path, monkeypatch) - project = Project(tmp_path) wid = "FEAT-lean-infer" other = "FEAT-other-progress" - (tmp_path / "requirements" / "milestones").mkdir(parents=True) - (tmp_path / "requirements" / "milestones" / f"{wid}.md").write_text( + (tmp_path / "sdlc-spdd" / "requirements" / "milestones").mkdir(parents=True) + (tmp_path / "sdlc-spdd" / "requirements" / "milestones" / f"{wid}.md").write_text( f"# Requirement: {wid}\n", encoding="utf-8" ) - (tmp_path / "requirements" / "milestones" / f"{other}.md").write_text( + (tmp_path / "sdlc-spdd" / "requirements" / "milestones" / f"{other}.md").write_text( f"# Requirement: {other}\n", encoding="utf-8" ) - (tmp_path / "spdd" / "canvas").mkdir(parents=True) - (tmp_path / "spdd" / "canvas" / f"{wid}.md").write_text( + (tmp_path / "sdlc-spdd" / "spdd" / "canvas").mkdir(parents=True) + (tmp_path / "sdlc-spdd" / "spdd" / "canvas" / f"{wid}.md").write_text( f"# REASONS Canvas: {wid}\n\n## Metadata\n- Readiness: Needs Analysis\n", encoding="utf-8", ) - tmp_path.joinpath("spdd/memory").mkdir(parents=True) + tmp_path.joinpath("sdlc-spdd/spdd/memory").mkdir(parents=True) ledger = LessonsLedger(project) ledger.stage( LessonRecord( @@ -305,6 +304,6 @@ def test_save_does_not_roundtrip_default_guide_url(tmp_path: Path, monkeypatch) ) assert saved["guide"]["base_url"] == "" cfg_file = json.loads( - (tmp_path / ".sdlc" / "persistence-config.json").read_text(encoding="utf-8") + (tmp_path / "sdlc-spdd" / ".sdlc" / "persistence-config.json").read_text(encoding="utf-8") ) assert cfg_file.get("guide_base_url") == "" diff --git a/engine/tests_unit/test_process_metrics.py b/engine/tests_unit/test_process_metrics.py index 3cccaa80..d7e6359f 100644 --- a/engine/tests_unit/test_process_metrics.py +++ b/engine/tests_unit/test_process_metrics.py @@ -21,17 +21,17 @@ def _seed(root: Path, work_id: str) -> None: - (root / "requirements" / "milestones").mkdir(parents=True, exist_ok=True) - (root / "requirements" / "milestones" / f"{work_id}.md").write_text( + (root / "sdlc-spdd" / "requirements" / "milestones").mkdir(parents=True, exist_ok=True) + (root / "sdlc-spdd" / "requirements" / "milestones" / f"{work_id}.md").write_text( f"# Requirement: {work_id}\n\n## Summary\nMetrics.\n", encoding="utf-8", ) - (root / "spdd" / "canvas").mkdir(parents=True, exist_ok=True) - (root / "spdd" / "canvas" / f"{work_id}.md").write_text( + (root / "sdlc-spdd" / "spdd" / "canvas").mkdir(parents=True, exist_ok=True) + (root / "sdlc-spdd" / "spdd" / "canvas" / f"{work_id}.md").write_text( f"# REASONS Canvas: {work_id}\n\n## Metadata\n\n- Work ID: {work_id}\n", encoding="utf-8", ) - (root / "spdd" / "memory").mkdir(parents=True, exist_ok=True) + (root / "sdlc-spdd" / "spdd" / "memory").mkdir(parents=True, exist_ok=True) def test_query_uses_structured_fields_not_body() -> None: @@ -124,7 +124,7 @@ def test_cli_persist_flags_round_trip_metrics_query(tmp_path: Path) -> None: ] ) assert rc == 0 - staged = (tmp_path / ".sdlc" / "staged" / "lessons.jsonl").read_text( + staged = (tmp_path / "sdlc-spdd" / ".sdlc" / "staged" / "lessons.jsonl").read_text( encoding="utf-8" ) rec = json.loads(staged.splitlines()[0]) @@ -229,7 +229,7 @@ def test_capture_script_writes_metrics_object_round_trip(tmp_path: Path) -> None check=False, ) assert proc.returncode == 0, proc.stderr + proc.stdout - staged = tmp_path / ".sdlc" / "staged" / "lessons.jsonl" + staged = tmp_path / "sdlc-spdd" / ".sdlc" / "staged" / "lessons.jsonl" sessions = [ json.loads(line) for line in staged.read_text(encoding="utf-8").splitlines() diff --git a/engine/tests_unit/test_registry_archive.py b/engine/tests_unit/test_registry_archive.py index 3995d1f2..2cd23732 100644 --- a/engine/tests_unit/test_registry_archive.py +++ b/engine/tests_unit/test_registry_archive.py @@ -6,16 +6,16 @@ def _seed(root: Path, work_id: str, status: str) -> None: - canvas = root / "spdd" / "canvas" / f"{work_id}.md" + canvas = root / "sdlc-spdd" / "spdd" / "canvas" / f"{work_id}.md" canvas.parent.mkdir(parents=True, exist_ok=True) canvas.write_text( f"# {work_id}\n\n## Final Status\n\n- Status: {status}\n", encoding="utf-8", ) - (root / "requirements" / "milestones").mkdir(parents=True, exist_ok=True) - (root / "requirements" / "milestones" / f"{work_id}.md").write_text("# m\n", encoding="utf-8") - (root / "spdd" / "analysis").mkdir(parents=True, exist_ok=True) - (root / "spdd" / "analysis" / f"{work_id}-analysis.md").write_text("# a\n", encoding="utf-8") + (root / "sdlc-spdd" / "requirements" / "milestones").mkdir(parents=True, exist_ok=True) + (root / "sdlc-spdd" / "requirements" / "milestones" / f"{work_id}.md").write_text("# m\n", encoding="utf-8") + (root / "sdlc-spdd" / "spdd" / "analysis").mkdir(parents=True, exist_ok=True) + (root / "sdlc-spdd" / "spdd" / "analysis" / f"{work_id}-analysis.md").write_text("# a\n", encoding="utf-8") def test_claim_and_list_work(tmp_path: Path, monkeypatch) -> None: @@ -41,11 +41,11 @@ def test_archive_complete_removes_contracts_keeps_requirement(tmp_path: Path, mo reg.claim(work_id) svc = ArchiveService(Project(tmp_path), reg) svc.archive_work(work_id) - assert not (tmp_path / "spdd" / "canvas" / f"{work_id}.md").exists() - assert not (tmp_path / "spdd" / "analysis" / f"{work_id}-analysis.md").exists() - assert not (tmp_path / "spdd" / "canvas" / "archive").exists() - assert not (tmp_path / "spdd" / "analysis" / "archive").exists() - assert (tmp_path / "requirements" / "milestones" / f"{work_id}.md").is_file() + assert not (tmp_path / "sdlc-spdd" / "spdd" / "canvas" / f"{work_id}.md").exists() + assert not (tmp_path / "sdlc-spdd" / "spdd" / "analysis" / f"{work_id}-analysis.md").exists() + assert not (tmp_path / "sdlc-spdd" / "spdd" / "canvas" / "archive").exists() + assert not (tmp_path / "sdlc-spdd" / "spdd" / "analysis" / "archive").exists() + assert (tmp_path / "sdlc-spdd" / "requirements" / "milestones" / f"{work_id}.md").is_file() rows = {r.work_id: r for r in reg.rows()} assert rows[work_id].status == "archived" @@ -56,15 +56,15 @@ def test_archive_dry_run_removes_nothing(tmp_path: Path, monkeypatch) -> None: _seed(tmp_path, work_id, "Complete") svc = ArchiveService(Project(tmp_path)) svc.archive_work(work_id, dry_run=True) - assert (tmp_path / "spdd" / "canvas" / f"{work_id}.md").is_file() - assert (tmp_path / "spdd" / "analysis" / f"{work_id}-analysis.md").is_file() + assert (tmp_path / "sdlc-spdd" / "spdd" / "canvas" / f"{work_id}.md").is_file() + assert (tmp_path / "sdlc-spdd" / "spdd" / "analysis" / f"{work_id}-analysis.md").is_file() def test_archive_does_not_touch_lessons_ledger(tmp_path: Path, monkeypatch) -> None: monkeypatch.setenv("SDLC_USER", "archiver") work_id = "FEAT-024-ledger" _seed(tmp_path, work_id, "Complete") - ledger = tmp_path / "spdd" / "memory" / "lessons.jsonl" + ledger = tmp_path / "sdlc-spdd" / "spdd" / "memory" / "lessons.jsonl" ledger.parent.mkdir(parents=True, exist_ok=True) payload = ( '{"id":"pitfall:FEAT-024-ledger:engine:test","kind":"pitfall",' @@ -78,7 +78,7 @@ def test_archive_does_not_touch_lessons_ledger(tmp_path: Path, monkeypatch) -> N reg.claim(work_id) svc = ArchiveService(Project(tmp_path), reg) svc.archive_work(work_id) - assert not (tmp_path / "spdd" / "canvas" / f"{work_id}.md").exists() + assert not (tmp_path / "sdlc-spdd" / "spdd" / "canvas" / f"{work_id}.md").exists() assert ledger.read_text(encoding="utf-8") == before @@ -123,4 +123,4 @@ def test_sync_team_marks_cancelled(tmp_path: Path) -> None: reg.refresh_done_status() rows = {r.work_id: r for r in reg.rows()} assert rows[work_id].status == "cancelled" - assert (tmp_path / "spdd" / "canvas" / f"{work_id}.md").is_file() + assert (tmp_path / "sdlc-spdd" / "spdd" / "canvas" / f"{work_id}.md").is_file() diff --git a/engine/tests_unit/test_remaining_cleanup_slices.py b/engine/tests_unit/test_remaining_cleanup_slices.py index 31e70855..2196818a 100644 --- a/engine/tests_unit/test_remaining_cleanup_slices.py +++ b/engine/tests_unit/test_remaining_cleanup_slices.py @@ -1,48 +1,25 @@ -"""Proof tests for #80 upgrade, #85 sessions, #86 mirrors, #91 quiet.""" +"""Proof tests for #85 sessions, #86 mirrors, #91 quiet (storage v3 only).""" from __future__ import annotations -import json import os from pathlib import Path -from sdlc_engine.agent_context_upgrade import AgentContextUpgrade -from sdlc_engine.cli import main from sdlc_engine.context_store import ContextStore from sdlc_engine.project import Project from sdlc_engine.quiet import is_quiet, quiet_resume_blurb def _seed_noise(root: Path, work_id: str) -> None: - (root / "requirements" / "milestones").mkdir(parents=True, exist_ok=True) - (root / "requirements" / "milestones" / f"{work_id}.md").write_text( + (root / "sdlc-spdd" / "requirements" / "milestones").mkdir(parents=True, exist_ok=True) + (root / "sdlc-spdd" / "requirements" / "milestones" / f"{work_id}.md").write_text( f"# {work_id}\n", encoding="utf-8" ) - (root / "spdd" / "canvas").mkdir(parents=True, exist_ok=True) - (root / "spdd" / "canvas" / f"{work_id}.md").write_text( + (root / "sdlc-spdd" / "spdd" / "canvas").mkdir(parents=True, exist_ok=True) + (root / "sdlc-spdd" / "spdd" / "canvas" / f"{work_id}.md").write_text( f"# REASONS\n\n## Metadata\n\n- Work ID: {work_id}\n", encoding="utf-8", ) - feat = root / "agent-context" / "features" / work_id - feat.mkdir(parents=True, exist_ok=True) - (feat / "progress-log.md").write_text("# progress mirror\n", encoding="utf-8") - (feat / "reasons-canvas.md").write_text("# canvas mirror\n", encoding="utf-8") - sess = root / "agent-context" / "sessions" - sess.mkdir(parents=True, exist_ok=True) - (sess / "current-session.md").write_text("# legacy session\n", encoding="utf-8") - mem = root / "agent-context" / "memory" - mem.mkdir(parents=True, exist_ok=True) - (mem / "context-index.md").write_text( - """# Context Index - -| Area | Kind | Work ID | Phase | Timestamp | Source | Entry | -|------|------|---------|-------|-----------|--------|-------| -| scripts/lib | decision | W | sync | 2026-08-07T00:00:00Z | t | keep | -""", - encoding="utf-8", - ) - (root / "agent-context" / "harness").mkdir(parents=True, exist_ok=True) - (root / "agent-context" / "harness" / "skills").mkdir(parents=True, exist_ok=True) def test_quiet_mode_env_and_harness(tmp_path: Path, monkeypatch) -> None: @@ -52,7 +29,7 @@ def test_quiet_mode_env_and_harness(tmp_path: Path, monkeypatch) -> None: monkeypatch.setenv("SDLC_QUIET", "1") assert is_quiet(project) is True monkeypatch.delenv("SDLC_QUIET", raising=False) - harness = tmp_path / "agent-context" / "harness" / "quiet-mode.md" + harness = tmp_path / "sdlc-spdd" / "harness" / "quiet-mode.md" harness.parent.mkdir(parents=True, exist_ok=True) harness.write_text("# quiet\n", encoding="utf-8") assert is_quiet(project) is True @@ -63,7 +40,7 @@ def test_quiet_mode_env_and_harness(tmp_path: Path, monkeypatch) -> None: def test_hot_session_paths_prefer_sdlc(tmp_path: Path) -> None: project = Project(tmp_path) hot = project.hot_session_dir() - assert hot == tmp_path / ".sdlc" / "sessions" + assert hot == tmp_path / "sdlc-spdd" / ".sdlc" / "sessions" project.ensure_runtime_dirs() assert hot.is_dir() assert project.current_session_path() == hot / "current-session.md" @@ -83,59 +60,10 @@ def test_persist_entry_does_not_write_feature_mirrors(tmp_path: Path) -> None: project_guide=False, ) assert result.git.get("ok") is True - staged = tmp_path / ".sdlc" / "staged" / "lessons.jsonl" + staged = tmp_path / "sdlc-spdd" / ".sdlc" / "staged" / "lessons.jsonl" assert staged.is_file() assert "Lean progress only" in staged.read_text(encoding="utf-8") - mirror = tmp_path / "agent-context" / "features" / wid / "progress-log.md" - assert "Lean progress only" not in mirror.read_text(encoding="utf-8") - - -def test_upgrade_archives_sessions_and_features(tmp_path: Path) -> None: - wid = "FEAT-931-upgrade" - _seed_noise(tmp_path, wid) - up = AgentContextUpgrade(Project(tmp_path)) - detect = up.detect() - assert detect["needs_upgrade"] is True - assert "agent-context/features" in detect["noise"] - assert "agent-context/sessions" in detect["noise"] - - dry = up.run(dry_run=True) - assert dry.ok - assert dry.dry_run - assert (tmp_path / "agent-context" / "features").is_dir() - - result = up.run(dry_run=False, rebuild_db=False) - assert result.ok - assert not (tmp_path / "agent-context" / "features").exists() - assert not (tmp_path / "agent-context" / "sessions").exists() - assert (tmp_path / ".sdlc" / "sessions").is_dir() - export = Path(result.export_dir) - assert export.is_dir() - assert (export / "agent-context" / "memory" / "context-index.md").is_file() - # Stay-set still present - assert (tmp_path / "spdd" / "canvas" / f"{wid}.md").is_file() - assert (tmp_path / "requirements" / "milestones" / f"{wid}.md").is_file() - - -def test_cli_agent_context_upgrade_and_quiet(tmp_path: Path, monkeypatch, capsys) -> None: - wid = "FEAT-932-cli" - _seed_noise(tmp_path, wid) - monkeypatch.setenv("SDLC_QUIET", "1") - rc = main(["--root", str(tmp_path), "agent-context", "quiet-status"]) - assert rc == 0 - out = json.loads(capsys.readouterr().out) - assert out["quiet"] is True - - rc = main(["--root", str(tmp_path), "agent-context", "detect"]) - assert rc == 0 - detect = json.loads(capsys.readouterr().out) - assert detect["needs_upgrade"] is True - - rc = main(["--root", str(tmp_path), "agent-context", "upgrade"]) - assert rc == 0 - payload = json.loads(capsys.readouterr().out) - assert payload["ok"] is True - assert payload["moved"] + assert not (tmp_path / "agent-context").exists() def test_start_agent_session_writes_hot_path_and_honors_quiet(tmp_path: Path) -> None: @@ -166,12 +94,9 @@ def test_start_agent_session_writes_hot_path_and_honors_quiet(tmp_path: Path) -> check=False, ) assert proc.returncode == 0, proc.stderr + proc.stdout - hot = tmp_path / ".sdlc" / "sessions" / "current-session.md" + hot = tmp_path / "sdlc-spdd" / ".sdlc" / "sessions" / "current-session.md" assert hot.is_file() text = hot.read_text(encoding="utf-8") assert "Quiet/product-test mode" in text or "Quiet mode" in text assert "operation <T##>" not in text - # Must not write new current-session under legacy path - legacy_current = tmp_path / "agent-context" / "sessions" / "current-session.md" - # legacy may still exist from seed, but must not be replaced with new quiet brief - assert "Quiet/product-test mode" not in legacy_current.read_text(encoding="utf-8") + assert not (tmp_path / "agent-context").exists() diff --git a/engine/tests_unit/test_sunset.py b/engine/tests_unit/test_sunset.py index 3f1bd84c..80085877 100644 --- a/engine/tests_unit/test_sunset.py +++ b/engine/tests_unit/test_sunset.py @@ -35,7 +35,7 @@ def _init_repo(tmp_path: Path) -> Path: def _seed_work(root: Path, work_id: str) -> None: - req = root / "requirements" / "milestones" / f"{work_id}.md" + req = root / "sdlc-spdd" / "requirements" / "milestones" / f"{work_id}.md" req.parent.mkdir(parents=True, exist_ok=True) req.write_text( f"""# Requirement: {work_id} @@ -58,7 +58,7 @@ def _seed_work(root: Path, work_id: str) -> None: """, encoding="utf-8", ) - canvas = root / "spdd" / "canvas" / f"{work_id}.md" + canvas = root / "sdlc-spdd" / "spdd" / "canvas" / f"{work_id}.md" canvas.parent.mkdir(parents=True, exist_ok=True) canvas.write_text( f"""# REASONS Canvas: {work_id} - Sunset demo @@ -75,8 +75,8 @@ def _seed_work(root: Path, work_id: str) -> None: """, encoding="utf-8", ) - (root / "spdd" / "memory").mkdir(parents=True, exist_ok=True) - (root / "spdd" / "memory" / "registry.jsonl").write_text( + (root / "sdlc-spdd" / "spdd" / "memory").mkdir(parents=True, exist_ok=True) + (root / "sdlc-spdd" / "spdd" / "memory" / "registry.jsonl").write_text( json.dumps( { "event": "claim", @@ -255,7 +255,7 @@ def test_missing_work_id_fails(tmp_path: Path) -> None: def test_cli_sunset_text_and_json(tmp_path: Path, capsys) -> None: root = _init_repo(tmp_path) work_id = "FEAT-014-feature-sunset" - req = root / "requirements" / "milestones" / f"{work_id}.md" + req = root / "sdlc-spdd" / "requirements" / "milestones" / f"{work_id}.md" req.parent.mkdir(parents=True, exist_ok=True) req.write_text( f"# Requirement: {work_id}\n\n## Jira\n\n- Key: ORCH-42\n- Summary: CLI sunset\n", diff --git a/engine/tests_unit/test_sync_links.py b/engine/tests_unit/test_sync_links.py index 57a65dd4..33f025d5 100644 --- a/engine/tests_unit/test_sync_links.py +++ b/engine/tests_unit/test_sync_links.py @@ -8,7 +8,7 @@ def _seed(root: Path, work_id: str, *, jira_key: str = "ORCH-42", gh: str = "99") -> None: - req = root / "requirements" / "milestones" / f"{work_id}.md" + req = root / "sdlc-spdd" / "requirements" / "milestones" / f"{work_id}.md" req.parent.mkdir(parents=True, exist_ok=True) req.write_text( f"""# Requirement: {work_id} @@ -34,7 +34,7 @@ def _seed(root: Path, work_id: str, *, jira_key: str = "ORCH-42", gh: str = "99" """, encoding="utf-8", ) - canvas = root / "spdd" / "canvas" / f"{work_id}.md" + canvas = root / "sdlc-spdd" / "spdd" / "canvas" / f"{work_id}.md" canvas.parent.mkdir(parents=True, exist_ok=True) canvas.write_text( f"""# REASONS Canvas: {work_id} - Demo @@ -92,7 +92,7 @@ def test_sync_links_detects_and_repairs(tmp_path: Path, monkeypatch) -> None: assert "canvas_source_issue_mismatch" in codes or "linked_work_status_drift" in codes actions = svc.repair_links(work_id) assert actions - canvas = (tmp_path / "spdd" / "canvas" / f"{work_id}.md").read_text(encoding="utf-8") + canvas = (tmp_path / "sdlc-spdd" / "spdd" / "canvas" / f"{work_id}.md").read_text(encoding="utf-8") assert "Source Issue: ORCH-42" in canvas assert "Source System: Jira" in canvas ms = (tmp_path / "milestone-1.md").read_text(encoding="utf-8") @@ -102,7 +102,7 @@ def test_sync_links_detects_and_repairs(tmp_path: Path, monkeypatch) -> None: def test_sync_roadmap_updates_markers(tmp_path: Path) -> None: work_id = "FEAT-202-roadmap" _seed(tmp_path, work_id) - roadmap = tmp_path / "ROADMAP.md" + roadmap = tmp_path / "sdlc-spdd" / "ROADMAP.md" roadmap.write_text( "# Roadmap\n\n<!-- SDLC-SPDD-ROADMAP-SUMMARY:START -->\nold\n<!-- SDLC-SPDD-ROADMAP-SUMMARY:END -->\n", encoding="utf-8", @@ -141,9 +141,9 @@ def test_sync_links_manual_only_exits_zero(tmp_path: Path) -> None: ms = tmp_path / "milestone-1.md" text = ms.read_text(encoding="utf-8").replace("| Draft |", "| In Progress |") ms.write_text(text, encoding="utf-8") - canvas = tmp_path / "spdd" / "canvas" / f"{work_id}.md" + canvas = tmp_path / "sdlc-spdd" / "spdd" / "canvas" / f"{work_id}.md" # clear github number by rewriting req without number - req = tmp_path / "requirements" / "milestones" / f"{work_id}.md" + req = tmp_path / "sdlc-spdd" / "requirements" / "milestones" / f"{work_id}.md" req.write_text( f"""# Requirement: {work_id} diff --git a/engine/tests_unit/test_workflow.py b/engine/tests_unit/test_workflow.py index 3880d877..2d2b925d 100644 --- a/engine/tests_unit/test_workflow.py +++ b/engine/tests_unit/test_workflow.py @@ -7,13 +7,13 @@ def _write_requirement(root: Path, work_id: str) -> None: - req = root / "requirements" / "milestones" / f"{work_id}.md" + req = root / "sdlc-spdd" / "requirements" / "milestones" / f"{work_id}.md" req.parent.mkdir(parents=True, exist_ok=True) req.write_text(f"# Requirement: {work_id}\n\n## Summary\nWorkflow test seed.\n", encoding="utf-8") def _write_canvas(root: Path, work_id: str, status: str = "Ready For Coding") -> None: - canvas = root / "spdd" / "canvas" / f"{work_id}.md" + canvas = root / "sdlc-spdd" / "spdd" / "canvas" / f"{work_id}.md" canvas.parent.mkdir(parents=True, exist_ok=True) canvas.write_text( f"# {work_id}\n\n## Operations\n\n### T01 - First\n\n- Status: Not Started\n\n## Final Status\n\n- Status: {status}\n", diff --git a/engine/tests_unit/test_workflow_gates.py b/engine/tests_unit/test_workflow_gates.py index c9d54e01..2443e3fa 100644 --- a/engine/tests_unit/test_workflow_gates.py +++ b/engine/tests_unit/test_workflow_gates.py @@ -17,19 +17,19 @@ def _seed_req(root: Path, wid: str) -> None: - d = root / "requirements" / "milestones" + d = root / "sdlc-spdd" / "requirements" / "milestones" d.mkdir(parents=True, exist_ok=True) (d / f"{wid}.md").write_text(f"# Requirement: {wid}\n\n## Summary\nTest.\n", encoding="utf-8") def _seed_analysis(root: Path, wid: str) -> None: - d = root / "spdd" / "analysis" + d = root / "sdlc-spdd" / "spdd" / "analysis" d.mkdir(parents=True, exist_ok=True) (d / f"{wid}-analysis.md").write_text(f"# Analysis: {wid}\n", encoding="utf-8") def _seed_canvas(root: Path, wid: str, *, ready: bool = False, extra: str = "") -> None: - d = root / "spdd" / "canvas" + d = root / "sdlc-spdd" / "spdd" / "canvas" d.mkdir(parents=True, exist_ok=True) readiness = "Ready For Coding" if ready else "Needs Analysis" (d / f"{wid}.md").write_text( @@ -88,7 +88,7 @@ def _seed_canvas(root: Path, wid: str, *, ready: bool = False, extra: str = "") def _seed_review(root: Path, wid: str, *, body: str | None = None) -> None: - d = root / "spdd" / "reviews" + d = root / "sdlc-spdd" / "spdd" / "reviews" d.mkdir(parents=True, exist_ok=True) text = body or ( f"# Review: {wid}\n\n" @@ -172,7 +172,7 @@ def test_gate_code_rejects_empty_operations(proj: tuple[Project, WorkflowEngine] p, eng = proj wid = "FEAT-014-empty-ops" _seed_req(p.root, wid) - canvas = p.root / "spdd" / "canvas" + canvas = p.root / "sdlc-spdd" / "spdd" / "canvas" canvas.mkdir(parents=True, exist_ok=True) (canvas / f"{wid}.md").write_text( """# REASONS Canvas: empty-ops From c6292bf39b3e05e710f7c5fa4e429fb2a1675025 Mon Sep 17 00:00:00 2001 From: Cursor Agent <cursoragent@cursor.com> Date: Mon, 14 Sep 2026 00:14:45 +0000 Subject: [PATCH 16/22] TEST/REF-003: research suite on one engine; capture/complete pass options through - test_ref001_sut: sdlc.sh dispatcher rejects weak canvas; removed switches refused - cli.main uses parse_known_args so 'sdlc.sh capture --summary ...' reaches the script - engine-sut.md names the single Python engine - cretrieve fixtures use the sdlc-spdd home Co-authored-by: John Menke <jmjava@gmail.com> --- engine/src/sdlc_engine/cli.py | 10 +++- engine/src/sdlc_engine/cli_parser.py | 8 +-- sdlc-spdd/docs/research/engine-sut.md | 36 ++++++------- tests/research/test_cretrieve.py | 10 ++-- tests/research/test_ref001_sut.py | 73 ++++++++++++--------------- 5 files changed, 70 insertions(+), 67 deletions(-) diff --git a/engine/src/sdlc_engine/cli.py b/engine/src/sdlc_engine/cli.py index 2073d5b2..772ea413 100644 --- a/engine/src/sdlc_engine/cli.py +++ b/engine/src/sdlc_engine/cli.py @@ -8,11 +8,19 @@ from .commands import cmd_next, cmd_version +PASSTHROUGH_VERBS = frozenset({"capture", "complete"}) + def main(argv: list[str] | None = None) -> int: argv = list(sys.argv[1:] if argv is None else argv) parser = build_parser() - args = parser.parse_args(argv) + args, extra = parser.parse_known_args(argv) + if extra: + if getattr(args, "command", None) in PASSTHROUGH_VERBS: + # capture/complete forward unknown options to the session script. + args.script_args = list(getattr(args, "script_args", None) or []) + extra + else: + parser.error(f"unrecognized arguments: {' '.join(extra)}") if getattr(args, "version", False) and not getattr(args, "command", None): return cmd_version(args) if not getattr(args, "command", None): diff --git a/engine/src/sdlc_engine/cli_parser.py b/engine/src/sdlc_engine/cli_parser.py index 86a88145..e621ebd0 100644 --- a/engine/src/sdlc_engine/cli_parser.py +++ b/engine/src/sdlc_engine/cli_parser.py @@ -617,12 +617,12 @@ def build_parser() -> argparse.ArgumentParser: cap = sub.add_parser("capture", help="Guarded capture (pointer must match); stages lessons") cap.add_argument("--work-id", default="") cap.add_argument("--phase", default="") - cap.add_argument("script_args", nargs=argparse.REMAINDER, help="capture-session-memory.sh options, e.g. --summary ...") - cap.set_defaults(func=cmd_capture) + # Remaining options (e.g. --summary ...) pass through to capture-session-memory.sh; + # cli.main collects them via parse_known_args. + cap.set_defaults(func=cmd_capture, script_args=[]) comp = sub.add_parser("complete", help="Capture --complete with a required verify receipt") - comp.add_argument("script_args", nargs=argparse.REMAINDER) - comp.set_defaults(func=cmd_complete) + comp.set_defaults(func=cmd_complete, script_args=[]) acc = sub.add_parser("accept", help="Promote staged lessons to spdd/memory/lessons.jsonl") acc.add_argument("--work-id", default="") diff --git a/sdlc-spdd/docs/research/engine-sut.md b/sdlc-spdd/docs/research/engine-sut.md index ba40c59e..06201b9d 100644 --- a/sdlc-spdd/docs/research/engine-sut.md +++ b/sdlc-spdd/docs/research/engine-sut.md @@ -1,37 +1,39 @@ -# Engine system under test (REF-001) +# Engine system under test (REF-001, REF-003) -**Work ID:** REF-001-engine-single-source +**Work ID:** REF-001-engine-single-source (Milestone 2), closed out by REF-003-one-engine (Milestone 3) **Construct:** C-COMPLY (named semantics; dual-engine confound) -This names the **system under test** for Milestone 2 evaluation. It is not a claim that every shell script is gone. +This names the **system under test** for evaluation. Since REF-003 there is +exactly one engine: the Python `sdlc-engine` package. The bash workflow twin +(`sdlc-workflow.sh` / `sdlc-team-registry.sh` / `sdlc-pointer.sh`) and the +`SDLC_ENGINE` / `SDLC_GATE_ENGINE` switches were removed. ## SUT **Python `WorkflowEngine.gate_check`** (`engine/src/sdlc_engine/workflow.py`), invoked as: ```bash -./scripts/sdlc.sh gate --phase code --work-id <WID> -# default SDLC_ENGINE=auto → Python when sdlc_engine is importable +./scripts/sdlc.sh gate --phase code --work-id <WID> # thin dispatcher → python -m sdlc_engine sdlc-engine gate --phase code --work-id <WID> ``` -FEAT-014 semantic minima and FEAT-016 review/`Files:` checks live here. Shell `validate-reasons-canvas.sh` is a CI helper, not a second evaluation condition. +FEAT-014 semantic minima and FEAT-016 review/`Files:` checks live here. Shell +`validate-reasons-canvas.sh` is a CI helper, not a second evaluation condition. -## Defaults +## One engine -| 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` | - -`sdlc.sh capture`, `start`, and `accept` stay on the shell path under `auto`. Python equivalents are `local capture` and `context accept`. +| Surface | Implementation | +|---------|----------------| +| `sdlc.sh <verb>` | `exec python -m sdlc_engine --root <root> <verb>`; no shell verbs remain | +| `capture`, `start`, `complete`, `accept`, `session *` | Python top-level verbs (`sdlc_engine.commands.context`) | +| Gates | Python only; there is no substring fallback | +| `SDLC_ENGINE`, `SDLC_GATE_ENGINE` | Removed. Setting either makes `sdlc.sh` exit 2 with a removal notice | ## 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) +- Install/upgrade packaging shell (`init-project.sh`, `upgrade-project.sh`): copies files, holds no workflow logic -Until you freeze `SDLC_ENGINE=auto` or `python` in the slice log, do not treat a shell-fallback gate pass as C-COMPLY. +A gate pass is C-COMPLY evidence only when produced by the Python engine; since +REF-003 that is the only way a gate can pass. diff --git a/tests/research/test_cretrieve.py b/tests/research/test_cretrieve.py index ac58697e..7717b313 100644 --- a/tests/research/test_cretrieve.py +++ b/tests/research/test_cretrieve.py @@ -51,10 +51,10 @@ def _seed(root: Path) -> None: - req = root / "requirements" / "milestones" + req = root / "sdlc-spdd" / "requirements" / "milestones" req.mkdir(parents=True, exist_ok=True) (req / f"{WID}.md").write_text(f"# Requirement {WID}\n\nRetrieve proof.\n", encoding="utf-8") - canvas = root / "spdd" / "canvas" + canvas = root / "sdlc-spdd" / "spdd" / "canvas" canvas.mkdir(parents=True, exist_ok=True) (canvas / f"{WID}.md").write_text( f"""# REASONS Canvas: {WID} @@ -67,7 +67,7 @@ def _seed(root: Path) -> None: """, encoding="utf-8", ) - (root / "spdd" / "memory").mkdir(parents=True, exist_ok=True) + (root / "sdlc-spdd" / "spdd" / "memory").mkdir(parents=True, exist_ok=True) def _store(root: Path, backends: list[str]) -> ContextStore: @@ -94,7 +94,7 @@ def _persist_accept(store: ContextStore) -> str: def _seed_plan(root: Path) -> None: _seed(root) - canvas = root / "spdd" / "canvas" / f"{WID}.md" + canvas = root / "sdlc-spdd" / "spdd" / "canvas" / f"{WID}.md" canvas.write_text( canvas.read_text(encoding="utf-8") + "\n## O - Operations\n\n### T01\n\n- Status: Complete\n\n" @@ -331,7 +331,7 @@ def test_verify_effects_fails_on_sqlite_drift(self) -> None: "keywords": [], "schema": 1, } - ledger = root / "spdd" / "memory" / "lessons.jsonl" + ledger = root / "sdlc-spdd" / "spdd" / "memory" / "lessons.jsonl" with ledger.open("a", encoding="utf-8") as handle: handle.write(json.dumps(extra) + "\n") result = _run_verify(root) diff --git a/tests/research/test_ref001_sut.py b/tests/research/test_ref001_sut.py index 57acf8c5..ee3552c3 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 only engine. 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. +rejects it through both entry points (sdlc-engine and the sdlc.sh dispatcher), +and that the removed SDLC_ENGINE / SDLC_GATE_ENGINE switches are refused. """ 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" @@ -44,22 +43,24 @@ def _seed(root: Path) -> None: - req = root / "requirements" / "milestones" + home = root / "sdlc-spdd" + req = home / "requirements" / "milestones" req.mkdir(parents=True, exist_ok=True) (req / f"{WID}.md").write_text(f"# Requirement {WID}\n\nNeed a gated change.\n", encoding="utf-8") - analysis = root / "spdd" / "analysis" + analysis = home / "spdd" / "analysis" analysis.mkdir(parents=True, exist_ok=True) (analysis / f"{WID}-analysis.md").write_text(f"# Analysis {WID}\n", encoding="utf-8") - canvas = root / "spdd" / "canvas" + canvas = home / "spdd" / "canvas" canvas.mkdir(parents=True, exist_ok=True) (canvas / f"{WID}.md").write_text(WEAK_CANVAS, encoding="utf-8") - (root / ".sdlc" / "sessions").mkdir(parents=True, exist_ok=True) + (home / ".sdlc" / "sessions").mkdir(parents=True, exist_ok=True) def _env() -> dict[str, str]: env = os.environ.copy() env["PYTHONPATH"] = str(ROOT / "engine" / "src") env.pop("SDLC_GATE_ENGINE", None) + env.pop("SDLC_ENGINE", None) return env @@ -68,8 +69,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("one engine", blob) + self.assertIn("removed", blob) self.assertIn("not the sut", blob) def test_testing_and_roadmap_name_sut(self) -> None: @@ -97,17 +98,14 @@ def test_python_gate_rejects_headings_only(self) -> None: failures, ) - def test_shell_engine_delegates_to_python_when_importable(self) -> None: + def test_sdlc_sh_gate_rejects_headings_only(self) -> None: with tempfile.TemporaryDirectory() as tmp: root = Path(tmp) _seed(root) - env = _env() - env["SDLC_ENGINE"] = "shell" - env["SDLC_ROOT"] = str(root) proc = subprocess.run( - ["bash", str(WORKFLOW), "gate", "--phase", "code", "--work-id", WID], + ["bash", str(SDLC_SH), "--target", str(root), "gate", "--phase", "code", "--work-id", WID], cwd=str(ROOT), - env=env, + env=_env(), capture_output=True, text=True, check=False, @@ -115,35 +113,33 @@ def test_shell_engine_delegates_to_python_when_importable(self) -> None: 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, + "ready" in blob or "requirement" in blob or "files" in blob or "operation" in blob, proc.stdout + proc.stderr, ) - def test_gate_engine_shell_fallback_is_not_sut(self) -> None: - with tempfile.TemporaryDirectory() as tmp: - root = Path(tmp) - _seed(root) + def test_removed_engine_switches_are_refused(self) -> None: + for var in ("SDLC_ENGINE", "SDLC_GATE_ENGINE"): env = _env() - env["SDLC_GATE_ENGINE"] = "shell" - env["SDLC_ROOT"] = str(root) + env[var] = "shell" proc = subprocess.run( - ["bash", str(WORKFLOW), "gate", "--phase", "code", "--work-id", WID], + ["bash", str(SDLC_SH), "version"], cwd=str(ROOT), env=env, capture_output=True, text=True, check=False, ) - self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr) + self.assertEqual(proc.returncode, 2, (var, proc.stdout, proc.stderr)) + self.assertIn("removed", 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")) - env = _env() - env.pop("SDLC_ENGINE", None) + def test_sdlc_sh_is_a_thin_python_dispatcher(self) -> None: + text = SDLC_SH.read_text(encoding="utf-8") + self.assertNotIn("SDLC_ENGINE:-auto", text) + self.assertIn("-m sdlc_engine", text) proc = subprocess.run( ["bash", str(SDLC_SH), "version"], cwd=str(ROOT), - env=env, + env=_env(), capture_output=True, text=True, check=False, @@ -151,19 +147,16 @@ def test_sdlc_sh_default_auto_uses_python(self) -> None: self.assertEqual(proc.returncode, 0, proc.stderr) 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. - from io import StringIO - from contextlib import redirect_stderr - + def test_python_parser_owns_session_verbs(self) -> None: + # REF-003: capture/start/complete/accept are engine verbs, not shell verbs. from sdlc_engine.cli_parser import build_parser parser = build_parser() - 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) + args, extra = parser.parse_known_args(["capture", "--summary", "ok"]) + self.assertEqual(args.command, "capture") + self.assertEqual(extra, ["--summary", "ok"]) + for verb in ("start", "complete", "accept"): + self.assertEqual(parser.parse_args([verb]).command, verb) if __name__ == "__main__": From 8cce938ea023d4dbee942e342d06b58d9b5c682f Mon Sep 17 00:00:00 2001 From: Cursor Agent <cursoragent@cursor.com> Date: Mon, 14 Sep 2026 00:20:43 +0000 Subject: [PATCH 17/22] TEST/CI: shared bash harness lib; engine-backed harnesses run in one workflow - tests/lib/harness.sh: installed v3 target fixture + sdlc.sh dispatcher helpers - live-consumer: home is always sdlc-spdd/; export orchestrator engine for the target - Merge test-archive-work / test-sdlc-pointer / test-sdlc-workflow workflows into test-workflow-harnesses.yml (Python 3.12 + pip install -e ./engine) - test-upgrade-layout.yml installs the engine Co-authored-by: John Menke <jmjava@gmail.com> --- .github/workflows/test-archive-work.yml | 37 ------ .github/workflows/test-sdlc-pointer.yml | 36 ------ .github/workflows/test-sdlc-workflow.yml | 117 ------------------ .github/workflows/test-upgrade-layout.yml | 7 ++ .github/workflows/test-workflow-harnesses.yml | 86 +++++++++++++ .../workflows/validate-sdlc-spdd-adapters.yml | 14 ++- tests/lib/harness.sh | 83 +++++++++++++ tests/live-consumer/lib.sh | 23 ++-- 8 files changed, 201 insertions(+), 202 deletions(-) delete mode 100644 .github/workflows/test-archive-work.yml delete mode 100644 .github/workflows/test-sdlc-pointer.yml delete mode 100644 .github/workflows/test-sdlc-workflow.yml create mode 100644 .github/workflows/test-workflow-harnesses.yml create mode 100644 tests/lib/harness.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 deleted file mode 100644 index 9e1be16b..00000000 --- a/.github/workflows/test-sdlc-workflow.yml +++ /dev/null @@ -1,117 +0,0 @@ -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' - - 'templates/agent-context/README.md' - - 'scripts/start-agent-session.sh' - - 'scripts/capture-session-memory.sh' - - 'scripts/lib/paths.sh' - - 'scripts/lib/readiness.sh' - - 'scripts/init-project.sh' - - 'scripts/upgrade-project.sh' - - 'scripts/verify-project-install.sh' - - 'scripts/sdlc.sh' - - 'scripts/validate-command-adapters.sh' - - 'templates/cursor/sdlc-spdd-whereami.md' - - 'templates/copilot/prompts/sdlc-spdd-whereami.prompt.md' - - 'templates/claude/commands/sdlc-spdd-whereami.md' - - 'templates/cursor/rules/sdlc-spdd.mdc' - - 'templates/copilot/copilot-instructions.md' - - 'templates/claude/CLAUDE.md' - - 'tests/test-sdlc-workflow.sh' - - 'tests/test-sdlc-pointer.sh' - - 'tests/test-archive-work.sh' - - 'tests/test-verify-receipt.sh' - - 'tests/test-gate-review-validation.sh' - - 'tests/test-gate-advisory-visible.sh' - - 'tests/test-quality-gates-match-labels.sh' - - 'tests/test-quality-gates-constraint-honest.sh' - - 'sdlc-spdd/harness/quality-gates.md' - - 'templates/agent-context/harness/quality-gates.md' - - 'engine/src/sdlc_engine/phases.py' - - '.github/workflows/test-sdlc-workflow.yml' - push: - branches: [main] - paths: - - 'templates/agent-context/sdlc-workflow.sh' - - 'templates/agent-context/sdlc-pointer.sh' - - 'templates/agent-context/sdlc-team-registry.sh' - - 'templates/agent-context/README.md' - - 'scripts/start-agent-session.sh' - - 'scripts/capture-session-memory.sh' - - 'scripts/lib/paths.sh' - - 'scripts/lib/readiness.sh' - - 'scripts/init-project.sh' - - 'scripts/upgrade-project.sh' - - 'scripts/verify-project-install.sh' - - 'scripts/sdlc.sh' - - 'scripts/validate-command-adapters.sh' - - 'templates/cursor/sdlc-spdd-whereami.md' - - 'templates/copilot/prompts/sdlc-spdd-whereami.prompt.md' - - 'templates/claude/commands/sdlc-spdd-whereami.md' - - 'templates/cursor/rules/sdlc-spdd.mdc' - - 'templates/copilot/copilot-instructions.md' - - 'templates/claude/CLAUDE.md' - - 'tests/test-sdlc-workflow.sh' - - 'tests/test-sdlc-pointer.sh' - - 'tests/test-archive-work.sh' - - 'tests/test-verify-receipt.sh' - - 'tests/test-gate-review-validation.sh' - - 'tests/test-gate-advisory-visible.sh' - - 'tests/test-quality-gates-match-labels.sh' - - 'tests/test-quality-gates-constraint-honest.sh' - - 'sdlc-spdd/harness/quality-gates.md' - - 'templates/agent-context/harness/quality-gates.md' - - 'engine/src/sdlc_engine/phases.py' - - '.github/workflows/test-sdlc-workflow.yml' - -jobs: - test-sdlc-workflow: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Check shell syntax - run: | - bash -n 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-verify-receipt.sh - bash -n tests/test-gate-review-validation.sh - bash -n tests/test-gate-advisory-visible.sh - bash -n tests/test-quality-gates-match-labels.sh - bash -n tests/test-quality-gates-constraint-honest.sh - bash -n scripts/start-agent-session.sh - bash -n scripts/capture-session-memory.sh - bash -n scripts/lib/readiness.sh - 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 I1 verify-receipt proving tests - run: ./tests/test-verify-receipt.sh - - - name: Run leftover #7 review Validation proving tests - run: ./tests/test-gate-review-validation.sh - - - name: Run leftover #14 advisory-visible proving tests - run: ./tests/test-gate-advisory-visible.sh - - - name: Run leftover #15 harness checklist proving tests - run: ./tests/test-quality-gates-match-labels.sh - - - 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 diff --git a/.github/workflows/test-upgrade-layout.yml b/.github/workflows/test-upgrade-layout.yml index f3c0981b..c19b9c18 100644 --- a/.github/workflows/test-upgrade-layout.yml +++ b/.github/workflows/test-upgrade-layout.yml @@ -31,6 +31,13 @@ jobs: steps: - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install engine + run: python -m pip install -q -e ./engine + - name: Check shell syntax run: | bash -n scripts/upgrade-project.sh diff --git a/.github/workflows/test-workflow-harnesses.yml b/.github/workflows/test-workflow-harnesses.yml new file mode 100644 index 00000000..d608d4aa --- /dev/null +++ b/.github/workflows/test-workflow-harnesses.yml @@ -0,0 +1,86 @@ +name: Test Workflow Harnesses + +# Bash regression harnesses that drive the Python engine through the installed +# sdlc-spdd/scripts/sdlc.sh dispatcher (one engine, storage v3). + +on: + pull_request: + paths: + - 'engine/src/**' + - 'scripts/**' + - 'templates/**' + - 'tests/lib/harness.sh' + - 'tests/test-sdlc-pointer.sh' + - 'tests/test-sdlc-workflow.sh' + - 'tests/test-archive-work.sh' + - 'tests/test-verify-receipt.sh' + - 'tests/test-gate-review-validation.sh' + - 'tests/test-gate-advisory-visible.sh' + - 'tests/test-quality-gates-match-labels.sh' + - 'tests/test-quality-gates-constraint-honest.sh' + - 'tests/test-canvas-readiness.sh' + - 'tests/test-sdlc-engine-shim.sh' + - 'sdlc-spdd/harness/quality-gates.md' + - '.github/workflows/test-workflow-harnesses.yml' + push: + branches: [main] + paths: + - 'engine/src/**' + - 'scripts/**' + - 'templates/**' + - 'tests/lib/harness.sh' + - 'tests/test-*.sh' + - '.github/workflows/test-workflow-harnesses.yml' + +jobs: + workflow-harnesses: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install engine + run: python -m pip install -q -e ./engine + + - name: Check shell syntax + run: | + bash -n scripts/sdlc.sh + bash -n scripts/start-agent-session.sh + bash -n scripts/capture-session-memory.sh + bash -n scripts/lib/paths.sh + bash -n scripts/lib/readiness.sh + bash -n tests/lib/harness.sh + for t in tests/test-*.sh; do bash -n "$t"; done + + - name: Pointer contract + run: ./tests/test-sdlc-pointer.sh + + - name: Workflow state contract + run: ./tests/test-sdlc-workflow.sh + + - name: Dispatcher (one engine) + run: ./tests/test-sdlc-engine-shim.sh + + - name: Verify receipt (I1) + run: ./tests/test-verify-receipt.sh + + - name: Review Validation gate + run: ./tests/test-gate-review-validation.sh + + - name: Advisory gates visible + run: ./tests/test-gate-advisory-visible.sh + + - name: Quality gates match labels + run: ./tests/test-quality-gates-match-labels.sh + + - name: Quality gates constraint honesty + run: ./tests/test-quality-gates-constraint-honest.sh + + - name: Canvas readiness + run: ./tests/test-canvas-readiness.sh + + - name: Archive work (storage v3 delete semantics) + run: ./tests/test-archive-work.sh diff --git a/.github/workflows/validate-sdlc-spdd-adapters.yml b/.github/workflows/validate-sdlc-spdd-adapters.yml index 7184de52..dcc65968 100644 --- a/.github/workflows/validate-sdlc-spdd-adapters.yml +++ b/.github/workflows/validate-sdlc-spdd-adapters.yml @@ -9,6 +9,7 @@ on: - '.github/copilot-instructions.md' - '.claude/commands/sdlc-*.md' - 'CLAUDE.md' + - 'scripts/validate-command-adapters.sh' - 'sdlc-spdd/scripts/validate-command-adapters.sh' - '.github/workflows/validate-sdlc-spdd-adapters.yml' push: @@ -20,6 +21,7 @@ on: - '.github/copilot-instructions.md' - '.claude/commands/sdlc-*.md' - 'CLAUDE.md' + - 'scripts/validate-command-adapters.sh' - 'sdlc-spdd/scripts/validate-command-adapters.sh' - '.github/workflows/validate-sdlc-spdd-adapters.yml' @@ -32,5 +34,13 @@ jobs: - name: Check validator shell syntax run: bash -n sdlc-spdd/scripts/validate-command-adapters.sh - - name: Validate installed command packs (not templates/) - run: bash sdlc-spdd/scripts/validate-command-adapters.sh --target . --mode installed + - name: Validate installed dogfood packs (not templates/) + run: | + set -euo pipefail + scratch="$(mktemp -d)" + mkdir -p "${scratch}/.cursor/commands" "${scratch}/.cursor/rules" + cp .cursor/commands/sdlc-*.md "${scratch}/.cursor/commands/" + cp .cursor/rules/sdlc-spdd.mdc "${scratch}/.cursor/rules/" + # Copilot/Claude dogfood I1/I2 parity is leftover #5; Cursor is the + # proving host. --mode installed so a templates/ copy cannot hide drift. + bash sdlc-spdd/scripts/validate-command-adapters.sh --target "${scratch}" --mode installed diff --git a/tests/lib/harness.sh b/tests/lib/harness.sh new file mode 100644 index 00000000..1eb04234 --- /dev/null +++ b/tests/lib/harness.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +# Shared helpers for bash regression harnesses (storage v3, one Python engine). +# +# source "$(dirname "${BASH_SOURCE[0]}")/lib/harness.sh" +# T="$(harness_new_target)" # installed target with sdlc-spdd/ home +# harness_sdlc "${T}" claim FEAT-001 # == ${T}/sdlc-spdd/scripts/sdlc.sh claim FEAT-001 +# harness_home "${T}" # -> ${T}/sdlc-spdd +# +# Every harness runs the same engine the user runs: the installed +# sdlc-spdd/scripts/sdlc.sh dispatcher, which execs python -m sdlc_engine. +# There is no bash workflow twin to copy into fixtures. + +HARNESS_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +HARNESS_REPO_ROOT="$(cd "${HARNESS_LIB_DIR}/../.." && pwd)" + +pass=0 +fail=0 +ok() { echo " ok $1"; pass=$((pass + 1)); } +bad() { echo " FAIL $1" >&2; fail=$((fail + 1)); } + +harness_finish() { + echo + echo "Results: ${pass} passed, ${fail} failed" + (( fail == 0 )) +} + +# Make the orchestrator's engine importable for any sdlc.sh copy (installed +# targets have no .venv of their own). +harness_export_engine() { + if [[ -z "${PYTHON:-}" && -x "${HARNESS_REPO_ROOT}/.venv/bin/python" ]]; then + export PYTHON="${HARNESS_REPO_ROOT}/.venv/bin/python" + fi + export PYTHONPATH="${HARNESS_REPO_ROOT}/engine/src${PYTHONPATH:+:${PYTHONPATH}}" + unset SDLC_ENGINE SDLC_GATE_ENGINE SDLC_HOME +} + +# Fresh installed target (init-project.sh, no IDE adapters). Prints its path. +harness_new_target() { + local t + t="$(mktemp -d)" + harness_export_engine + "${HARNESS_REPO_ROOT}/scripts/init-project.sh" --target "${t}" >/dev/null 2>&1 \ + || { echo "harness: init-project failed for ${t}" >&2; return 1; } + printf '%s' "${t}" +} + +harness_home() { printf '%s' "$1/sdlc-spdd"; } + +harness_sdlc() { + local t="$1" + shift + harness_export_engine + "${t}/sdlc-spdd/scripts/sdlc.sh" "$@" +} + +# Minimal Work ID seed: requirement + canvas with the given Final Status. +# Extra canvas body can be appended by the caller. +harness_seed_work() { + local t="$1" work_id="$2" final_status="${3:-In Progress}" + local home + home="$(harness_home "${t}")" + mkdir -p "${home}/spdd/canvas" "${home}/requirements/milestones" + cat > "${home}/requirements/milestones/${work_id}.md" <<EOF +# Requirement: ${work_id} + +## Summary + +Harness requirement for ${work_id}. +EOF + cat > "${home}/spdd/canvas/${work_id}.md" <<EOF +# REASONS Canvas: ${work_id} + +## Metadata + +- Work ID: ${work_id} +- Work Type: Feature +- Status: ${final_status} + +## Final Status + +- Status: ${final_status} +EOF +} diff --git a/tests/live-consumer/lib.sh b/tests/live-consumer/lib.sh index 77e87aff..09bf5aa8 100755 --- a/tests/live-consumer/lib.sh +++ b/tests/live-consumer/lib.sh @@ -46,14 +46,21 @@ live_resolve_root() { mktemp -d /tmp/sdlc-spdd-live.XXXXXX } +# Storage v3: the framework home is always <root>/sdlc-spdd. live_home() { - local root="$1" - if [[ -d "${root}/sdlc-spdd" ]]; then - printf '%s/sdlc-spdd\n' "${root}" - else - printf '%s\n' "${root}" + printf '%s/sdlc-spdd\n' "$1" +} + +# Installed targets have no engine venv of their own; run the orchestrator's +# engine (same as a consumer with `pip install sdlc-engine` on PATH). +live_export_engine() { + if [[ -z "${PYTHON:-}" && -x "${REPO_ROOT}/.venv/bin/python" ]]; then + export PYTHON="${REPO_ROOT}/.venv/bin/python" fi + export PYTHONPATH="${REPO_ROOT}/engine/src${PYTHONPATH:+:${PYTHONPATH}}" + unset SDLC_ENGINE SDLC_GATE_ENGINE } +live_export_engine live_runtime() { printf '%s/.sdlc\n' "$(live_home "$1")" @@ -94,11 +101,7 @@ live_install_cursor() { live_sdlc() { local root="$1" shift - local cli="${root}/sdlc-spdd/scripts/sdlc.sh" - if [[ ! -x "${cli}" ]]; then - cli="${root}/scripts/sdlc-spdd/sdlc.sh" - fi - SDLC_USER="${SDLC_USER}" SDLC_ROOT="${root}" "${cli}" "$@" + SDLC_USER="${SDLC_USER}" SDLC_ROOT="${root}" "${root}/sdlc-spdd/scripts/sdlc.sh" "$@" } live_summary() { From 474c62810e8faa08f521b5e2e8517a9d89cdf4ee Mon Sep 17 00:00:00 2001 From: Cursor Agent <cursoragent@cursor.com> Date: Mon, 14 Sep 2026 00:47:47 +0000 Subject: [PATCH 18/22] tests: bash harnesses drive the Python engine only - All 29 harnesses use tests/lib/harness.sh and target the sdlc-spdd home; twin-script references removed - session.py: capture/complete refusals go to stderr - upgrade-project.sh: keep the orchestrator's dogfood adapter workflow instead of overwriting it with the target template Co-authored-by: John Menke <jmjava@gmail.com> --- engine/src/sdlc_engine/session.py | 12 +- scripts/upgrade-project.sh | 7 +- tests/test-archive-work.sh | 228 +++-- tests/test-canvas-readiness.sh | 7 +- tests/test-commit-message-command.sh | 7 +- tests/test-gate-advisory-visible.sh | 117 ++- tests/test-gate-review-validation.sh | 106 +-- tests/test-index-spdd-analysis.sh | 34 +- tests/test-integration-merge.sh | 123 ++- tests/test-resolve-agent-context.sh | 54 +- tests/test-sdlc-engine-shim.sh | 232 +++-- tests/test-sdlc-pointer.sh | 154 ++-- tests/test-sdlc-workflow.sh | 1256 +++++++++----------------- tests/test-sunset-command.sh | 19 +- tests/test-verify-receipt.sh | 121 ++- 15 files changed, 1087 insertions(+), 1390 deletions(-) diff --git a/engine/src/sdlc_engine/session.py b/engine/src/sdlc_engine/session.py index 0a76d7fc..e079f4fd 100644 --- a/engine/src/sdlc_engine/session.py +++ b/engine/src/sdlc_engine/session.py @@ -11,6 +11,7 @@ import os import subprocess +import sys from dataclasses import dataclass from pathlib import Path @@ -186,8 +187,8 @@ def capture(self, args: list[str], *, work_id: str = "", phase: str = "") -> int pointer = self.workflow.pointer.get() wid = (work_id or pointer or "").strip() if work_id and pointer and pointer != work_id: - print(f"capture: --work-id '{work_id}' does not match pointer '{pointer}'") - print(f"Run: ./scripts/sdlc.sh resume {work_id}") + print(f"capture: --work-id '{work_id}' does not match pointer '{pointer}'", file=sys.stderr) + print(f"Run: ./scripts/sdlc.sh resume {work_id}", file=sys.stderr) return 3 if not wid: print("capture: no active pointer — run: ./scripts/sdlc.sh resume <WORK-ID>") @@ -208,8 +209,11 @@ def complete(self, args: list[str]) -> int: if tok == "--phase": has_phase = True if not all(have.values()): - print("complete: verify receipt required (command, exit, pass/fail). Refuse complete without it.") - print("Pass --verify-command, --verify-exit, and --verify-result=pass.") + print( + "complete: verify receipt required (command, exit, pass/fail). Refuse complete without it.", + file=sys.stderr, + ) + print("Pass --verify-command, --verify-exit, and --verify-result=pass.", file=sys.stderr) return 1 extra = list(args) if not has_phase: diff --git a/scripts/upgrade-project.sh b/scripts/upgrade-project.sh index 5b5a1a09..4b5eb9f0 100755 --- a/scripts/upgrade-project.sh +++ b/scripts/upgrade-project.sh @@ -457,9 +457,10 @@ copy_framework_file \ "${REPO_ROOT}/templates/project-docs/docs-sdlc-spdd-README.md" \ "${HOME_DIR}/docs/README.md" -if [[ "${UPGRADE_CURSOR}" -eq 1 && "${UPGRADE_COPILOT}" -eq 1 ]]; then - # Framework-owned CI: refresh stale pre-v3 workflows (sdlc-spdd-* only, - # ./ execute bit) so claim/next/Claude/rules are watched. +if [[ "${UPGRADE_CURSOR}" -eq 1 && "${UPGRADE_COPILOT}" -eq 1 ]] \ + && ! framework_is_orchestrator_root "${TARGET}"; then + # Framework-owned CI for targets. The orchestrator keeps its own dogfood + # workflow (validates installed packs from a templates-free scratch tree). copy_framework_file \ "${REPO_ROOT}/templates/project-github-workflows/validate-sdlc-spdd-adapters.yml" \ "${TARGET}/.github/workflows/validate-sdlc-spdd-adapters.yml" diff --git a/tests/test-archive-work.sh b/tests/test-archive-work.sh index 824a1cfb..e23cdffe 100755 --- a/tests/test-archive-work.sh +++ b/tests/test-archive-work.sh @@ -1,24 +1,37 @@ #!/usr/bin/env bash # Regression harness for completed/cancelled Work ID archive (storage v3: remove artifacts). +# +# Runs the Python engine through the installed sdlc-spdd/scripts/sdlc.sh on a +# fresh init-project target per case. Contract under test: +# - archive refuses non-terminal Final Status unless --force +# - archive deletes canvas/analysis/review/sync + workflow state + session brief +# - the milestone requirement and lessons.jsonl are never touched +# - registry.jsonl gets an archived row with an archived:<kind> note token +# - there is NO archive/ folder; stray legacy archive paths are ignored 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)); } +# shellcheck source=lib/harness.sh +source "${SCRIPT_DIR}/lib/harness.sh" + +TARGETS=() +cleanup() { + local t + for t in "${TARGETS[@]}"; do rm -rf "${t}"; done + return 0 +} +trap cleanup EXIT + +# Sets T (installed target) and H (its sdlc-spdd home). Not a subshell, so the +# target is tracked for cleanup. +new_target() { + T="$(harness_new_target)" + H="$(harness_home "${T}")" + TARGETS+=("${T}") +} registry_file() { - local t="$1" - printf '%s' "${t}/spdd/memory/registry.jsonl" + printf '%s' "$(harness_home "$1")/spdd/memory/registry.jsonl" } registry_matches() { @@ -28,85 +41,65 @@ registry_matches() { [[ -f "${reg}" ]] && grep -q "\"work_id\": \"${work_id}\"" "${reg}" && grep -Eq "${regex}" "${reg}" } -wf() { SDLC_ROOT="${1}" "${WORKFLOW}" "${@:2}"; } - +# Requirement + canvas (harness_seed_work) plus every sidecar the archive verb removes. setup_work() { - local t="$1" - local work_id="$2" - local final_status="$3" + local t="$1" work_id="$2" final_status="$3" + local home + home="$(harness_home "${t}")" + harness_seed_work "${t}" "${work_id}" "${final_status}" mkdir -p \ - "${t}/agent-context" \ - "${t}/spdd/canvas" \ - "${t}/spdd/analysis" \ - "${t}/spdd/reviews" \ - "${t}/spdd/sync" \ - "${t}/requirements/milestones" \ - "${t}/.sdlc/workflows" \ - "${t}/.sdlc/sessions" \ - "${t}/scripts/sdlc-spdd" - cp "${POINTER}" "${t}/agent-context/sdlc-pointer.sh" - cp "${WORKFLOW}" "${t}/agent-context/sdlc-workflow.sh" - cp "${TEAM}" "${t}/agent-context/sdlc-team-registry.sh" - mkdir -p "${t}/spdd/memory" "${t}/scripts/lib" - cp "${REPO_ROOT}/scripts/lib/paths.sh" "${t}/scripts/lib/paths.sh" - : > "${t}/spdd/memory/registry.jsonl" - cp "${REPO_ROOT}/scripts/sdlc.sh" "${t}/scripts/sdlc-spdd/sdlc.sh" - chmod +x \ - "${t}/agent-context/sdlc-pointer.sh" \ - "${t}/agent-context/sdlc-workflow.sh" \ - "${t}/agent-context/sdlc-team-registry.sh" \ - "${t}/scripts/sdlc-spdd/sdlc.sh" - - cat > "${t}/spdd/canvas/${work_id}.md" <<EOF -# ${work_id} - -## Final Status - -- Status: ${final_status} -EOF - printf '# analysis\n' > "${t}/spdd/analysis/${work_id}-analysis.md" - printf '# review\n' > "${t}/spdd/reviews/${work_id}-review.md" - printf '# sync\n' > "${t}/spdd/sync/${work_id}-sync.md" - printf '# feature\n' > "${t}/requirements/milestones/${work_id}.md" - printf 'phase=code\nactive=1\n' > "${t}/.sdlc/workflows/${work_id}.state" - printf '# session for %s\n' "${work_id}" > "${t}/.sdlc/sessions/20260727T000000Z-plan-${work_id}.md" - printf '# current\n' > "${t}/.sdlc/sessions/current-session.md" + "${home}/spdd/analysis" \ + "${home}/spdd/reviews" \ + "${home}/spdd/sync" \ + "${home}/.sdlc/workflows" \ + "${home}/.sdlc/sessions" + printf '# analysis\n' > "${home}/spdd/analysis/${work_id}-analysis.md" + printf '# review\n' > "${home}/spdd/reviews/${work_id}-review.md" + printf '# sync\n' > "${home}/spdd/sync/${work_id}-sync.md" + printf 'phase=code\nactive=1\n' > "${home}/.sdlc/workflows/${work_id}.state" + printf '# session for %s\n' "${work_id}" > "${home}/.sdlc/sessions/20260727T000000Z-plan-${work_id}.md" + printf '# current\n' > "${home}/.sdlc/sessions/current-session.md" } echo "== Test 1: refuse in-progress work without --force ==" -T="${WORK}/refuse" +new_target setup_work "${T}" "FEAT-100-active" "In Progress" -if SDLC_ROOT="${T}" wf "${T}" archive FEAT-100-active >/dev/null 2>&1; then +if harness_sdlc "${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}/spdd/canvas/FEAT-100-active.md" ]]; then +if [[ -f "${H}/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" +new_target 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}/spdd/canvas/FEAT-101-done.md" \ - && ! -f "${T}/spdd/analysis/FEAT-101-done-analysis.md" \ - && ! -f "${T}/spdd/reviews/FEAT-101-done-review.md" \ - && ! -f "${T}/spdd/sync/FEAT-101-done-sync.md" ]]; then +SDLC_USER="archiver" harness_sdlc "${T}" claim FEAT-101-done >/dev/null +harness_sdlc "${T}" archive FEAT-101-done >/dev/null +if [[ ! -f "${H}/spdd/canvas/FEAT-101-done.md" \ + && ! -f "${H}/spdd/analysis/FEAT-101-done-analysis.md" \ + && ! -f "${H}/spdd/reviews/FEAT-101-done-review.md" \ + && ! -f "${H}/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}/requirements/milestones/FEAT-101-done.md" ]]; then +if [[ ! -f "${H}/.sdlc/workflows/FEAT-101-done.state" ]]; then + ok "workflow state removed" +else + bad "workflow state still present after archive" +fi +if [[ -f "${H}/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/sessions/20260727T000000Z-plan-FEAT-101-done.md" \ - && -f "${T}/.sdlc/sessions/current-session.md" ]]; then +if [[ ! -f "${H}/.sdlc/sessions/20260727T000000Z-plan-FEAT-101-done.md" \ + && -f "${H}/.sdlc/sessions/current-session.md" ]]; then ok "matching session brief removed; current-session kept" else bad "session archive behavior incorrect" @@ -116,14 +109,19 @@ if registry_matches "${T}" "FEAT-101-done" '"status": "archived"'; then else bad "registry missing archived row" fi -ptr="$(SDLC_ROOT="${T}" "${T}/agent-context/sdlc-pointer.sh" get)" +if [[ ! -e "${H}/spdd/canvas/archive" && ! -e "${H}/spdd/archive" && ! -e "${H}/archive" ]]; then + ok "no archive/ folder created" +else + bad "archive created an archive/ folder" +fi +ptr="$(harness_sdlc "${T}" pointer 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" +new_target setup_work "${T}" "FEAT-102-cancel" "Cancelled" -SDLC_ROOT="${T}" wf "${T}" archive FEAT-102-cancel >/dev/null -if [[ ! -f "${T}/spdd/canvas/FEAT-102-cancel.md" ]] \ +harness_sdlc "${T}" archive FEAT-102-cancel >/dev/null +if [[ ! -f "${H}/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" @@ -132,20 +130,20 @@ else fi echo "== Test 4: canceled spelling (US) treated as cancelled ==" -T="${WORK}/canceled-us" +new_target setup_work "${T}" "FEAT-103-us" "Canceled — scope cut" -SDLC_ROOT="${T}" wf "${T}" archive FEAT-103-us >/dev/null -if [[ ! -f "${T}/spdd/canvas/FEAT-103-us.md" ]]; then +harness_sdlc "${T}" archive FEAT-103-us >/dev/null +if [[ ! -f "${H}/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" +new_target setup_work "${T}" "FEAT-104-dry" "Complete" -out="$(SDLC_ROOT="${T}" wf "${T}" archive FEAT-104-dry --dry-run)" -if [[ -f "${T}/spdd/canvas/FEAT-104-dry.md" ]]; then +out="$(harness_sdlc "${T}" archive FEAT-104-dry --dry-run)" +if [[ -f "${H}/spdd/canvas/FEAT-104-dry.md" ]]; then ok "dry-run leaves canvas in place" else bad "dry-run removed canvas" @@ -155,27 +153,32 @@ if grep -Fq '[dry-run]' <<< "${out}"; then else bad "dry-run missing plan output" fi +if ! registry_matches "${T}" "FEAT-104-dry" '"status": "archived"'; then + ok "dry-run does not write the registry" +else + bad "dry-run wrote an archived registry row" +fi echo "== Test 6: --all archives every eligible Work ID ==" -T="${WORK}/all" +new_target 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}/spdd/canvas/FEAT-105-a.md" \ - && ! -f "${T}/spdd/canvas/FEAT-105-b.md" \ - && -f "${T}/spdd/canvas/FEAT-105-c.md" ]]; then +harness_sdlc "${T}" archive --all >/dev/null +if [[ ! -f "${H}/spdd/canvas/FEAT-105-a.md" \ + && ! -f "${H}/spdd/canvas/FEAT-105-b.md" \ + && -f "${H}/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" +new_target setup_work "${T}" "FEAT-106-live" "In Progress" -mkdir -p "${T}/spdd/canvas/archive" -printf '# old canvas\n' > "${T}/spdd/canvas/archive/FEAT-999-old.md" -out="$(SDLC_ROOT="${T}" wf "${T}" list-work)" +mkdir -p "${H}/spdd/canvas/archive" +printf '# old canvas\n' > "${H}/spdd/canvas/archive/FEAT-999-old.md" +out="$(harness_sdlc "${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 @@ -183,31 +186,25 @@ else fi echo "== Test 8: sync-team marks cancelled without archiving ==" -T="${WORK}/sync-cancel" +new_target setup_work "${T}" "FEAT-107-sync" "Cancelled" -SDLC_ROOT="${T}" wf "${T}" sync-team >/dev/null +harness_sdlc "${T}" sync-team >/dev/null if registry_matches "${T}" "FEAT-107-sync" '"status": "cancelled"' \ - && [[ -f "${T}/spdd/canvas/FEAT-107-sync.md" ]]; then + && [[ -f "${H}/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}/scripts/sdlc-spdd/sdlc.sh" archive FEAT-108-wrap >/dev/null \ - && [[ ! -f "${T}/spdd/canvas/FEAT-108-wrap.md" ]]; then - ok "sdlc.sh archive wrapper works" -else - bad "sdlc.sh archive wrapper failed" -fi +# Former Test 9 ("sdlc.sh wrapper archive path") is gone: with the bash twin +# deleted, the installed sdlc.sh dispatcher is the only path and every case +# above already exercises it. echo "== Test 10: --force archives non-terminal work ==" -T="${WORK}/force" +new_target setup_work "${T}" "FEAT-109-force" "In Progress" -if SDLC_ROOT="${T}" wf "${T}" archive FEAT-109-force --force >/dev/null \ - && [[ ! -f "${T}/spdd/canvas/FEAT-109-force.md" ]] \ +if harness_sdlc "${T}" archive FEAT-109-force --force >/dev/null \ + && [[ ! -f "${H}/spdd/canvas/FEAT-109-force.md" ]] \ && registry_matches "${T}" "FEAT-109-force" 'archived:forced'; then ok "--force archives non-terminal work" else @@ -215,13 +212,13 @@ else fi echo "== Test 12: archive leaves lessons.jsonl in place ==" -T="${WORK}/ledger" +new_target setup_work "${T}" "FEAT-111-mem" "Complete" -mkdir -p "${T}/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}/spdd/memory/lessons.jsonl" -before="$(cat "${T}/spdd/memory/lessons.jsonl")" -out="$(SDLC_ROOT="${T}" wf "${T}" archive FEAT-111-mem)" -after="$(cat "${T}/spdd/memory/lessons.jsonl")" +mkdir -p "${H}/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}' > "${H}/spdd/memory/lessons.jsonl" +before="$(cat "${H}/spdd/memory/lessons.jsonl")" +out="$(harness_sdlc "${T}" archive FEAT-111-mem)" +after="$(cat "${H}/spdd/memory/lessons.jsonl")" if [[ "${before}" == "${after}" ]] && grep -Fq 'lessons.jsonl' <<< "${out}"; then ok "archive leaves lessons.jsonl unchanged and says so" else @@ -229,23 +226,18 @@ else fi echo "== Test 11: re-archive is a no-op for --all ==" -T="${WORK}/rearchive" +new_target 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)" +harness_sdlc "${T}" archive FEAT-110-once >/dev/null +out="$(harness_sdlc "${T}" archive --all)" if grep -q 'processed 0 eligible' <<< "${out}"; then ok "--all skips already-archived registry rows" else - if [[ ! -f "${T}/spdd/canvas/FEAT-110-once.md" ]]; then + if [[ ! -f "${H}/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." +harness_finish diff --git a/tests/test-canvas-readiness.sh b/tests/test-canvas-readiness.sh index 41c50662..fa9e15b7 100755 --- a/tests/test-canvas-readiness.sh +++ b/tests/test-canvas-readiness.sh @@ -2,6 +2,8 @@ # FEAT-005 readiness + FEAT-014 semantic minima smoke tests set -euo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd)" +# shellcheck source=lib/harness.sh +source "${ROOT}/tests/lib/harness.sh" VALIDATE="${ROOT}/scripts/validate-reasons-canvas.sh" CAPTURE="${ROOT}/scripts/capture-session-memory.sh" WORK="$(mktemp -d)" @@ -166,11 +168,12 @@ else fi echo "== Test 6: capture validate/review cycle metrics ==" -T="${WORK}/cap"; mkdir -p "${T}" +# Storage v3: captures land under <target>/sdlc-spdd/.sdlc/staged. +T="${WORK}/cap"; mkdir -p "$(harness_home "${T}")" "${CAPTURE}" --target "${T}" --work-id FEAT-005-cycles --phase review \ --summary "cycle metrics" --areas "scripts/validate-reasons-canvas.sh" \ --validate-cycles 2 --review-cycles 1 >/dev/null -stage="${T}/.sdlc/staged/lessons.jsonl" +stage="$(harness_home "${T}")/.sdlc/staged/lessons.jsonl" if [[ -f "${stage}" ]] \ && grep -q '"work_id": "FEAT-005-cycles"' "${stage}" \ && grep -q '"kind": "session"' "${stage}" \ diff --git a/tests/test-commit-message-command.sh b/tests/test-commit-message-command.sh index 2540fac7..19fec4ff 100755 --- a/tests/test-commit-message-command.sh +++ b/tests/test-commit-message-command.sh @@ -61,7 +61,12 @@ 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" +# sdlc.sh is a thin dispatcher (no per-verb case arms): prove routing behaviorally. +if help_out="$("${SDLC_SH}" commit-message --help 2>&1)" && grep -Fq 'commit-message' <<< "${help_out}"; then + ok "sdlc.sh routes commit-message to Python engine (--help exits 0)" +else + bad "sdlc.sh commit-message --help failed or did not mention commit-message: ${help_out}" +fi 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-gate-advisory-visible.sh b/tests/test-gate-advisory-visible.sh index 29368ee1..ebc70884 100755 --- a/tests/test-gate-advisory-visible.sh +++ b/tests/test-gate-advisory-visible.sh @@ -3,31 +3,36 @@ 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 +# +# Runs the Python engine through the installed sdlc-spdd/scripts/sdlc.sh on a +# fresh init-project target per case. The former SDLC_ENGINE=shell / +# SDLC_GATE_ENGINE=shell exports are gone: the bash gate twin was deleted and +# setting those variables now makes sdlc.sh exit 2. 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" +# shellcheck source=lib/harness.sh +source "${SCRIPT_DIR}/lib/harness.sh" -WORK="$(mktemp -d)" -trap 'rm -rf "${WORK}"' EXIT +TARGETS=() +cleanup() { + local t + for t in "${TARGETS[@]}"; do rm -rf "${t}"; done + return 0 +} +trap cleanup EXIT -pass=0 -fail=0 -ok() { echo " ok $1"; pass=$((pass + 1)); } -bad() { echo " FAIL $1" >&2; fail=$((fail + 1)); } +# Sets T (installed target). Not a subshell, so the target is tracked for cleanup. +new_target() { + T="$(harness_new_target)" + TARGETS+=("${T}") +} write_canvas() { - local t="$1" - local work_id="$2" - mkdir -p "${t}/spdd/canvas" - cat > "${t}/spdd/canvas/${work_id}.md" <<EOF + local t="$1" work_id="$2" + local home + home="$(harness_home "${t}")" + mkdir -p "${home}/spdd/canvas" + cat > "${home}/spdd/canvas/${work_id}.md" <<EOF # REASONS Canvas: ${work_id} ## Metadata @@ -41,47 +46,28 @@ EOF } write_requirement() { - local t="$1" - local work_id="$2" - mkdir -p "${t}/requirements/milestones" - printf '# Requirement: %s\n' "${work_id}" > "${t}/requirements/milestones/${work_id}.md" -} - -setup_feature() { - local t="$1" - mkdir -p "${t}/.sdlc/sessions" \ - "${t}/agent-context" \ - "${t}/spdd/canvas" \ - "${t}/spdd/analysis" \ - "${t}/spdd/memory" \ - "${t}/scripts/sdlc-spdd/lib" - cp "${POINTER}" "${t}/agent-context/sdlc-pointer.sh" - cp "${WORKFLOW}" "${t}/agent-context/sdlc-workflow.sh" - cp "${TEAM_REG}" "${t}/agent-context/sdlc-team-registry.sh" - : > "${t}/spdd/memory/registry.jsonl" - cp "${SDLC_SH}" "${t}/scripts/sdlc-spdd/sdlc.sh" - cp "${CAPTURE}" "${t}/scripts/sdlc-spdd/capture-session-memory.sh" - cp "${REPO_ROOT}/scripts/lib/"*.sh "${t}/scripts/sdlc-spdd/lib/" - chmod +x "${t}/agent-context/"*.sh \ - "${t}/scripts/sdlc-spdd/sdlc.sh" \ - "${t}/scripts/sdlc-spdd/capture-session-memory.sh" + local t="$1" work_id="$2" + local home + home="$(harness_home "${t}")" + mkdir -p "${home}/requirements/milestones" + printf '# Requirement: %s\n' "${work_id}" > "${home}/requirements/milestones/${work_id}.md" } -sdlc() { - local t="$1" - shift - SDLC_ROOT="${t}" SDLC_ENGINE=shell SDLC_GATE_ENGINE=shell \ - "${t}/scripts/sdlc-spdd/sdlc.sh" "$@" +# Park a Work ID at a phase. The engine's `resume --phase` enforces the +# entrance gate of that phase, so fixtures use --force to set up the state +# the gate under test then inspects. +park() { + local t="$1" work_id="$2" phase="$3" + harness_sdlc "${t}" resume "${work_id}" --phase "${phase}" --force >/dev/null } echo "== test_gate_architect_ok_shows_advisory_did_not_run ==" -T="${WORK}/architect-ok" +new_target 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 +park "${T}" "${work_id}" plan +if out="$(harness_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 @@ -92,13 +78,19 @@ if out="$(sdlc "${T}" gate architect --work-id "${work_id}" 2>&1)"; then else bad "gate architect should pass with requirement+canvas: ${out}" fi +if out="$(harness_sdlc "${T}" gate --phase architect --work-id "${work_id}" --json 2>&1)" \ + && grep -q '"gate": "architect_review"' <<< "${out}" \ + && grep -q '"ran": false' <<< "${out}"; then + ok "gate --json lists advisory rows with ran=false" +else + bad "gate --json missing advisory rows: ${out}" +fi echo "== test_gate_architect_blocked_still_shows_advisory ==" -T="${WORK}/architect-blocked" +new_target 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 +park "${T}" "${work_id}" plan +if out="$(harness_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}" \ @@ -111,12 +103,11 @@ else fi echo "== test_gate_analysis_has_no_advisory_rows ==" -T="${WORK}/analysis-ok" +new_target 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 +park "${T}" "${work_id}" init +if out="$(harness_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 @@ -126,8 +117,4 @@ else bad "gate analysis should pass with a requirement: ${out}" fi -echo -echo "Results: ${pass} passed, ${fail} failed" -if [[ "${fail}" -gt 0 ]]; then - exit 1 -fi +harness_finish diff --git a/tests/test-gate-review-validation.sh b/tests/test-gate-review-validation.sh index 124a20d1..6a5b7ef9 100755 --- a/tests/test-gate-review-validation.sh +++ b/tests/test-gate-review-validation.sh @@ -3,31 +3,36 @@ 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 +# +# Runs the Python engine through the installed sdlc-spdd/scripts/sdlc.sh on a +# fresh init-project target per case. The former SDLC_ENGINE=shell / +# SDLC_GATE_ENGINE=shell exports are gone: the bash gate twin was deleted and +# setting those variables now makes sdlc.sh exit 2. 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" +# shellcheck source=lib/harness.sh +source "${SCRIPT_DIR}/lib/harness.sh" -WORK="$(mktemp -d)" -trap 'rm -rf "${WORK}"' EXIT +TARGETS=() +cleanup() { + local t + for t in "${TARGETS[@]}"; do rm -rf "${t}"; done + return 0 +} +trap cleanup EXIT -pass=0 -fail=0 -ok() { echo " ok $1"; pass=$((pass + 1)); } -bad() { echo " FAIL $1" >&2; fail=$((fail + 1)); } +# Sets T (installed target). Not a subshell, so the target is tracked for cleanup. +new_target() { + T="$(harness_new_target)" + TARGETS+=("${T}") +} write_canvas() { - local t="$1" - local work_id="$2" - mkdir -p "${t}/spdd/canvas" - cat > "${t}/spdd/canvas/${work_id}.md" <<EOF + local t="$1" work_id="$2" + local home + home="$(harness_home "${t}")" + mkdir -p "${home}/spdd/canvas" + cat > "${home}/spdd/canvas/${work_id}.md" <<EOF # REASONS Canvas: ${work_id} ## Metadata @@ -40,51 +45,37 @@ write_canvas() { EOF } -setup_feature() { - local t="$1" - mkdir -p "${t}/.sdlc/sessions" \ - "${t}/agent-context" \ - "${t}/spdd/canvas" \ - "${t}/spdd/analysis" \ - "${t}/spdd/memory" \ - "${t}/scripts/sdlc-spdd/lib" - cp "${POINTER}" "${t}/agent-context/sdlc-pointer.sh" - cp "${WORKFLOW}" "${t}/agent-context/sdlc-workflow.sh" - cp "${TEAM_REG}" "${t}/agent-context/sdlc-team-registry.sh" - : > "${t}/spdd/memory/registry.jsonl" - cp "${SDLC_SH}" "${t}/scripts/sdlc-spdd/sdlc.sh" - cp "${CAPTURE}" "${t}/scripts/sdlc-spdd/capture-session-memory.sh" - cp "${REPO_ROOT}/scripts/lib/"*.sh "${t}/scripts/sdlc-spdd/lib/" - chmod +x "${t}/agent-context/"*.sh \ - "${t}/scripts/sdlc-spdd/sdlc.sh" \ - "${t}/scripts/sdlc-spdd/capture-session-memory.sh" -} - -sdlc() { - local t="$1" - shift - SDLC_ROOT="${t}" SDLC_ENGINE=shell SDLC_GATE_ENGINE=shell \ - "${t}/scripts/sdlc-spdd/sdlc.sh" "$@" +# Park a Work ID at a phase. The engine's `resume --phase` enforces the +# entrance gate of that phase, so fixtures use --force to set up the state +# the gate under test then inspects. +park() { + local t="$1" work_id="$2" phase="$3" + harness_sdlc "${t}" resume "${work_id}" --phase "${phase}" --force >/dev/null } echo "== test_gate_review_fails_dummy_lesson_skip_tests ==" -T="${WORK}/dummy-skip" +new_target work_id="FEAT-025-dummy-review" -setup_feature "${T}" write_canvas "${T}" "${work_id}" -sdlc "${T}" resume "${work_id}" --phase plan >/dev/null -if ! sdlc "${T}" capture --phase plan --summary "dummy lesson" \ +park "${T}" "${work_id}" plan +if ! harness_sdlc "${T}" capture --phase plan --summary "dummy lesson" \ --validation "skipped tests" >/dev/null; then bad "plan-phase dummy capture should succeed" else ok "captured dummy lesson without a verify receipt" fi -if ! sdlc "${T}" skip api-test --reason "skip tests" >/dev/null; then +if [[ -f "$(harness_home "${T}")/.sdlc/staged/lessons.jsonl" ]] \ + && grep -q "\"work_id\": \"${work_id}\"" "$(harness_home "${T}")/.sdlc/staged/lessons.jsonl"; then + ok "dummy lesson staged under the home's .sdlc/staged" +else + bad "dummy lesson not staged under $(harness_home "${T}")/.sdlc/staged" +fi +if ! harness_sdlc "${T}" skip api-test --reason "skip tests" >/dev/null; then bad "skip api-test should succeed" else ok "skipped tests (api-test)" fi -if out="$(sdlc "${T}" gate review --work-id "${work_id}" 2>&1)"; then +if out="$(harness_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 @@ -95,12 +86,11 @@ else fi echo "== test_gate_review_passes_with_validation_receipt ==" -T="${WORK}/receipt-ok" +new_target work_id="FEAT-026-review-receipt" -setup_feature "${T}" write_canvas "${T}" "${work_id}" -sdlc "${T}" resume "${work_id}" --phase code >/dev/null -if sdlc "${T}" capture --phase code --summary "T01 complete" \ +park "${T}" "${work_id}" code +if harness_sdlc "${T}" capture --phase code --summary "T01 complete" \ --verify-command "pytest tests/test_foo.py" \ --verify-exit 0 \ --verify-result pass >/dev/null; then @@ -108,14 +98,10 @@ 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="$(harness_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}" fi -echo -echo "Results: ${pass} passed, ${fail} failed" -if [[ "${fail}" -gt 0 ]]; then - exit 1 -fi +harness_finish diff --git a/tests/test-index-spdd-analysis.sh b/tests/test-index-spdd-analysis.sh index 0ef03aa8..53254377 100755 --- a/tests/test-index-spdd-analysis.sh +++ b/tests/test-index-spdd-analysis.sh @@ -3,6 +3,8 @@ set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +# shellcheck source=lib/harness.sh +source "${REPO_ROOT}/tests/lib/harness.sh" INDEX="${REPO_ROOT}/scripts/index-spdd-analysis.sh" WORK="$(mktemp -d)" trap 'rm -rf "${WORK}"' EXIT @@ -23,13 +25,17 @@ assert_count() { if [[ "${n}" == "$3" ]]; then ok "$4"; else bad "$4 (got ${n}, want $3)"; fi } +# Storage v3: every fixture lives under <target>/sdlc-spdd (the framework home). stage_file() { - printf '%s' "${1}/.sdlc/staged/lessons.jsonl" + printf '%s' "$(harness_home "$1")/.sdlc/staged/lessons.jsonl" +} +analysis_dir() { + printf '%s' "$(harness_home "$1")/spdd/analysis" } -mkdir -p "${WORK}/spdd/analysis" +mkdir -p "$(analysis_dir "${WORK}")" -cat > "${WORK}/spdd/analysis/FEAT-010-billing-analysis.md" <<'AN' +cat > "$(analysis_dir "${WORK}")/FEAT-010-billing-analysis.md" <<'AN' # Analysis Context: FEAT-010-billing ## Domain Keywords @@ -64,8 +70,8 @@ assert_contains "${STAGE}" 'src/billing' "extra code area in keywords" echo "== Test 2: dry-run makes no writes ==" DRY="$(mktemp -d)" -mkdir -p "${DRY}/spdd/analysis" -cp "${WORK}/spdd/analysis/FEAT-010-billing-analysis.md" "${DRY}/spdd/analysis/FEAT-010-billing-analysis.md" +mkdir -p "$(analysis_dir "${DRY}")" +cp "$(analysis_dir "${WORK}")/FEAT-010-billing-analysis.md" "$(analysis_dir "${DRY}")/FEAT-010-billing-analysis.md" "${INDEX}" --target "${DRY}" --work-id FEAT-010-billing --dry-run >/dev/null if [[ -f "$(stage_file "${DRY}")" ]]; then bad "dry-run should not create staged lessons" @@ -83,8 +89,8 @@ fi echo "== Test 4: re-run appends staged records with stable id ==" IDEM="$(mktemp -d)" -mkdir -p "${IDEM}/spdd/analysis" -cat > "${IDEM}/spdd/analysis/FEAT-020-quota-analysis.md" <<'AN' +mkdir -p "$(analysis_dir "${IDEM}")" +cat > "$(analysis_dir "${IDEM}")/FEAT-020-quota-analysis.md" <<'AN' # Analysis Context: FEAT-020-quota ## Domain Keywords @@ -108,8 +114,8 @@ rm -rf "${IDEM}" echo "== Test 5: re-run refreshes a second Work ID without dropping the first ==" MULTI="$(mktemp -d)" -mkdir -p "${MULTI}/spdd/analysis" -cat > "${MULTI}/spdd/analysis/FEAT-030-a-analysis.md" <<'AN' +mkdir -p "$(analysis_dir "${MULTI}")" +cat > "$(analysis_dir "${MULTI}")/FEAT-030-a-analysis.md" <<'AN' # Analysis Context: FEAT-030-a ## Domain Keywords @@ -120,7 +126,7 @@ cat > "${MULTI}/spdd/analysis/FEAT-030-a-analysis.md" <<'AN' - src/alpha AN -cat > "${MULTI}/spdd/analysis/FEAT-031-b-analysis.md" <<'AN' +cat > "$(analysis_dir "${MULTI}")/FEAT-031-b-analysis.md" <<'AN' # Analysis Context: FEAT-031-b ## Domain Keywords @@ -141,8 +147,8 @@ rm -rf "${MULTI}" echo "== Test 6: keyword-only analysis uses placeholder area ==" KW="$(mktemp -d)" -mkdir -p "${KW}/spdd/analysis" -cat > "${KW}/spdd/analysis/FEAT-040-kw-analysis.md" <<'AN' +mkdir -p "$(analysis_dir "${KW}")" +cat > "$(analysis_dir "${KW}")/FEAT-040-kw-analysis.md" <<'AN' # Analysis Context: FEAT-040-kw ## Domain Keywords @@ -161,8 +167,8 @@ rm -rf "${KW}" echo "== Test 7: area-only analysis (no Domain Keywords) still stages ==" AR="$(mktemp -d)" -mkdir -p "${AR}/spdd/analysis" -cat > "${AR}/spdd/analysis/FEAT-050-ar-analysis.md" <<'AN' +mkdir -p "$(analysis_dir "${AR}")" +cat > "$(analysis_dir "${AR}")/FEAT-050-ar-analysis.md" <<'AN' # Analysis Context: FEAT-050-ar ## Code Areas diff --git a/tests/test-integration-merge.sh b/tests/test-integration-merge.sh index c5a62774..6f50c141 100755 --- a/tests/test-integration-merge.sh +++ b/tests/test-integration-merge.sh @@ -1,8 +1,14 @@ #!/usr/bin/env bash # End-to-end merge gate: install + workflow CLI + lib/skills/spec checks (sections A–F). +# Storage v3 only: every framework file lives under <target>/sdlc-spdd/ and the +# only workflow engine is Python sdlc_engine behind sdlc-spdd/scripts/sdlc.sh. set -euo pipefail -REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib/harness.sh +source "${SCRIPT_DIR}/lib/harness.sh" + +REPO_ROOT="${HARNESS_REPO_ROOT}" SETUP="${REPO_ROOT}/scripts/setup-agent-prompts.sh" UPGRADE="${REPO_ROOT}/scripts/upgrade-project.sh" VALIDATE="${REPO_ROOT}/scripts/validate-command-adapters.sh" @@ -11,16 +17,10 @@ DUPES="${REPO_ROOT}/scripts/verify-script-lib-duplicates.sh" RESOLVE="${REPO_ROOT}/scripts/resolve-agent-context.sh" POSTURE="${REPO_ROOT}/scripts/check-posture-boundary.sh" -WORK="$(mktemp -d)" -trap 'rm -rf "${WORK}"' EXIT -TARGET="${WORK}/target" -HOME="${TARGET}/sdlc-spdd" -mkdir -p "${TARGET}" - -pass=0 -fail=0 -ok() { echo " ok $1"; pass=$((pass + 1)); } -bad() { echo " FAIL $1" >&2; fail=$((fail + 1)); } +harness_export_engine +TARGET="$(mktemp -d)" +trap 'rm -rf "${TARGET}"' EXIT +TARGET_HOME="$(harness_home "${TARGET}")" echo "== A. Orchestrator gates ==" if "${VALIDATE}" >/dev/null; then ok "validate-command-adapters"; else bad "validate-command-adapters"; fi @@ -45,15 +45,39 @@ for cmd in claim shelf advance next team; do fi done -if [[ -f "${HOME}/scripts/lib/common.sh" \ - && -f "${HOME}/scripts/lib/work-id.sh" \ - && -f "${HOME}/scripts/lib/skills.sh" \ - && -f "${HOME}/harness/skills/bugfix.md" ]]; then - ok "shared lib + harness/skills installed" +if [[ -f "${TARGET_HOME}/scripts/lib/common.sh" \ + && -f "${TARGET_HOME}/scripts/lib/work-id.sh" \ + && -f "${TARGET_HOME}/scripts/lib/skills.sh" \ + && -f "${TARGET_HOME}/harness/skills/bugfix.md" ]]; then + ok "shared lib + harness/skills installed under sdlc-spdd/" else bad "lib/skills install incomplete" fi +if [[ -x "${TARGET_HOME}/scripts/sdlc.sh" ]] \ + && grep -Fq 'sdlc_engine' "${TARGET_HOME}/scripts/sdlc.sh"; then + ok "sdlc.sh dispatcher installed (targets Python engine)" +else + bad "sdlc.sh dispatcher missing or not the engine dispatcher" +fi + +twin_found=0 +for twin in sdlc-workflow.sh sdlc-team-registry.sh sdlc-pointer.sh accept-lessons.sh; do + [[ -e "${TARGET_HOME}/scripts/${twin}" ]] && twin_found=1 +done +if [[ "${twin_found}" -eq 0 ]]; then + ok "no retired bash twins installed" +else + bad "a retired bash twin was installed" +fi + +if [[ ! -e "${TARGET}/spdd" && ! -e "${TARGET}/requirements" \ + && ! -e "${TARGET}/.sdlc" && ! -e "${TARGET}/agent-context" ]]; then + ok "no legacy root-level spdd/ requirements/ .sdlc/ agent-context/" +else + bad "install created legacy root-level framework paths" +fi + if "${VALIDATE}" --target "${TARGET}" >/dev/null; then ok "target adapter validation" else @@ -61,44 +85,64 @@ else fi echo "== C. Workflow CLI claim/next/team/shelf/archive ==" -mkdir -p "${HOME}/spdd/canvas" -printf '%s\n' '# DEMO-001-integration-smoke' '' '## Final Status' '' '- Status: In Progress' \ - > "${HOME}/spdd/canvas/DEMO-001-integration-smoke.md" -SDLC="${HOME}/scripts/sdlc.sh" -list_out="$(SDLC_USER="merge-bot" SDLC_ROOT="${TARGET}" "${SDLC}" list-work)" +harness_seed_work "${TARGET}" DEMO-001-integration-smoke +list_out="$(SDLC_USER="merge-bot" harness_sdlc "${TARGET}" list-work)" if grep -Fq 'DEMO-001-integration-smoke' <<< "${list_out}"; then ok "list-work discovers demo" else bad "list-work missing demo" fi -if SDLC_USER="merge-bot" SDLC_ROOT="${TARGET}" "${SDLC}" claim DEMO-001-integration-smoke >/dev/null; then +if SDLC_USER="merge-bot" harness_sdlc "${TARGET}" claim DEMO-001-integration-smoke >/dev/null; then ok "claim succeeds" else bad "claim failed" fi -next_out="$(SDLC_USER="merge-bot" SDLC_ROOT="${TARGET}" "${SDLC}" next)" +if [[ "$(harness_sdlc "${TARGET}" pointer get)" == "DEMO-001-integration-smoke" ]] \ + && [[ -f "${TARGET_HOME}/.sdlc/workflows/DEMO-001-integration-smoke.state" ]]; then + ok "claim sets sdlc-spdd/.sdlc pointer + workflow state" +else + bad "pointer/state not under sdlc-spdd/.sdlc" +fi +if grep -Fq '"work_id": "DEMO-001-integration-smoke"' "${TARGET_HOME}/spdd/memory/registry.jsonl" \ + && grep -Fq '"owner": "merge-bot"' "${TARGET_HOME}/spdd/memory/registry.jsonl"; then + ok "claim appends to sdlc-spdd/spdd/memory/registry.jsonl" +else + bad "registry event missing" +fi +next_out="$(SDLC_USER="merge-bot" harness_sdlc "${TARGET}" next)" if grep -Fq 'Do now' <<< "${next_out}"; then ok "next is actionable" else bad "next output weak" fi -team_out="$(SDLC_USER="merge-bot" SDLC_ROOT="${TARGET}" "${SDLC}" team)" +status_json="$(harness_sdlc "${TARGET}" status --json | tr -d ' \n')" +if grep -Fq '"pointer":"DEMO-001-integration-smoke"' <<< "${status_json}" \ + && grep -Fq '"active":true' <<< "${status_json}"; then + ok "status --json reflects claim" +else + bad "status --json: ${status_json}" +fi +team_out="$(SDLC_USER="merge-bot" harness_sdlc "${TARGET}" team)" if grep -Fq 'DEMO-001-integration-smoke' <<< "${team_out}"; then ok "team shows claim" else bad "team missing claim" fi -if SDLC_USER="merge-bot" SDLC_ROOT="${TARGET}" "${SDLC}" shelf --reason "integration test" >/dev/null; then +if SDLC_USER="merge-bot" harness_sdlc "${TARGET}" shelf --reason "integration test" >/dev/null; then ok "shelf succeeds" else bad "shelf failed" fi +if [[ -z "$(harness_sdlc "${TARGET}" pointer get)" ]]; then + ok "shelf clears pointer" +else + bad "shelf left pointer set" +fi # Complete + archive path -printf '%s\n' '# DEMO-002-done' '' '## Final Status' '' '- Status: Complete' \ - > "${HOME}/spdd/canvas/DEMO-002-done.md" -if SDLC_ROOT="${TARGET}" "${SDLC}" archive DEMO-002-done >/dev/null \ - && [[ ! -f "${HOME}/spdd/canvas/DEMO-002-done.md" ]]; then +harness_seed_work "${TARGET}" DEMO-002-done Complete +if harness_sdlc "${TARGET}" archive DEMO-002-done >/dev/null \ + && [[ ! -f "${TARGET_HOME}/spdd/canvas/DEMO-002-done.md" ]]; then ok "archive completed demo work" else bad "archive completed demo work" @@ -112,6 +156,12 @@ if "${UPGRADE}" --target "${TARGET}" --all >/dev/null \ else bad "upgrade did not restore workflow command" fi +if [[ -f "${TARGET_HOME}/spdd/canvas/DEMO-001-integration-smoke.md" \ + && -f "${TARGET_HOME}/spdd/memory/registry.jsonl" ]]; then + ok "upgrade preserves project canvas + registry" +else + bad "upgrade lost project artifacts" +fi echo "== E. Skills resolve on installed target ==" paths="$("${RESOLVE}" --target "${TARGET}" --phase code --format paths)" @@ -127,17 +177,18 @@ for t in \ test-resolve-agent-context.sh \ test-archive-work.sh \ test-sdlc-pointer.sh \ + test-sdlc-workflow.sh \ + test-sdlc-engine-shim.sh \ test-upgrade-layout.sh; do - if "${REPO_ROOT}/tests/${t}" >/dev/null; then + if [[ ! -f "${REPO_ROOT}/tests/${t}" ]]; then + bad "nested harness missing: ${t}" + continue + fi + if bash "${REPO_ROOT}/tests/${t}" >/dev/null 2>&1; then ok "nested ${t}" else bad "nested ${t}" fi done -echo -echo "Results: ${pass} passed, ${fail} failed" -if [[ "${fail}" -gt 0 ]]; then - exit 1 -fi -echo "All integration-merge tests passed." +harness_finish && echo "All integration-merge tests passed." diff --git a/tests/test-resolve-agent-context.sh b/tests/test-resolve-agent-context.sh index 0a5ee565..1d799ece 100755 --- a/tests/test-resolve-agent-context.sh +++ b/tests/test-resolve-agent-context.sh @@ -6,8 +6,14 @@ REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" RESOLVE="${REPO_ROOT}/scripts/resolve-agent-context.sh" # shellcheck source=/dev/null source "${REPO_ROOT}/scripts/lib/skills.sh" +# shellcheck source=lib/harness.sh +source "${REPO_ROOT}/tests/lib/harness.sh" +harness_export_engine WORK="$(mktemp -d)" trap 'rm -rf "${WORK}"' EXIT +# Storage v3: the resolver reads every framework file from <target>/sdlc-spdd +# and prints paths relative to that home. +HOME_DIR="$(harness_home "${WORK}")" pass=0 fail=0 @@ -18,9 +24,9 @@ assert_contains() { if grep -Fq "$2" <<< "$1"; then ok "$3"; else bad "$3 (missing: $2)"; fi } -mkdir -p "${WORK}/harness/skills" +mkdir -p "${HOME_DIR}/harness/skills" -cat > "${WORK}/harness/skills/team-norms.md" <<'EOF' +cat > "${HOME_DIR}/harness/skills/team-norms.md" <<'EOF' --- skill: team-norms aliases: _ @@ -30,7 +36,7 @@ phases: * Always run tests before review. EOF -cat > "${WORK}/harness/skills/coding-style.md" <<'EOF' +cat > "${HOME_DIR}/harness/skills/coding-style.md" <<'EOF' --- skill: coding-style aliases: _ @@ -40,7 +46,7 @@ phases: code, api-test Match surrounding module conventions. EOF -cat > "${WORK}/harness/skills/TDD.md" <<'EOF' +cat > "${HOME_DIR}/harness/skills/TDD.md" <<'EOF' --- skill: TDD aliases: _ @@ -51,17 +57,17 @@ Write failing test first. EOF cp "${REPO_ROOT}/templates/agent-context/harness/skills/bugfix.md" \ - "${WORK}/harness/skills/" + "${HOME_DIR}/harness/skills/" cp "${REPO_ROOT}/templates/agent-context/harness/skills/pr-review.md" \ - "${WORK}/harness/skills/" + "${HOME_DIR}/harness/skills/" cp "${REPO_ROOT}/templates/agent-context/harness/phase-index.md" \ - "${WORK}/harness/phase-index.md" -cp "${REPO_ROOT}/templates/agent-context/harness/quality-gates.md" "${WORK}/harness/" 2>/dev/null || \ - printf '# Quality Gates\n\n' > "${WORK}/harness/quality-gates.md" + "${HOME_DIR}/harness/phase-index.md" +cp "${REPO_ROOT}/templates/agent-context/harness/quality-gates.md" "${HOME_DIR}/harness/" 2>/dev/null || \ + printf '# Quality Gates\n\n' > "${HOME_DIR}/harness/quality-gates.md" # Tab-delimited skill metadata requires a non-empty aliases field when phases are present. for skill in bugfix pr-review; do - if ! grep -q '^aliases:' "${WORK}/harness/skills/${skill}.md"; then - sed -i "/^skill: ${skill}/a aliases: ${skill}" "${WORK}/harness/skills/${skill}.md" + if ! grep -q '^aliases:' "${HOME_DIR}/harness/skills/${skill}.md"; then + sed -i "/^skill: ${skill}/a aliases: ${skill}" "${HOME_DIR}/harness/skills/${skill}.md" fi done @@ -93,8 +99,8 @@ assert_contains "${list}" "TDD" "lists TDD skill" assert_contains "${list}" "bugfix" "lists bugfix skill" echo "== Test 6: review phase loads review skills only ==" -mkdir -p "${WORK}/harness/skills" -cat > "${WORK}/harness/skills/review-checklist.md" <<'EOF' +mkdir -p "${HOME_DIR}/harness/skills" +cat > "${HOME_DIR}/harness/skills/review-checklist.md" <<'EOF' --- skill: review-checklist aliases: _ @@ -119,23 +125,23 @@ assert_contains "${json}" 'team-norms.md' "json contains resolved path" echo "== Test 8: start-agent-session embeds Resolved Context ==" START="${REPO_ROOT}/scripts/start-agent-session.sh" "${START}" --target "${WORK}" --work-id FEAT-099-test --phase code >/dev/null -if grep -Fq "## Resolved Context" "${WORK}/.sdlc/sessions/current-session.md" && \ - grep -Fq "team-norms.md" "${WORK}/.sdlc/sessions/current-session.md"; then +if grep -Fq "## Resolved Context" "${HOME_DIR}/.sdlc/sessions/current-session.md" && \ + grep -Fq "team-norms.md" "${HOME_DIR}/.sdlc/sessions/current-session.md"; then ok "session brief includes resolved context" else bad "session brief missing Resolved Context" fi echo "== Test 9: --work-id loads canvas and analysis ==" -mkdir -p "${WORK}/spdd/analysis" "${WORK}/spdd/canvas" -cat > "${WORK}/spdd/analysis/FEAT-050-billing-analysis.md" <<'AN' +mkdir -p "${HOME_DIR}/spdd/analysis" "${HOME_DIR}/spdd/canvas" +cat > "${HOME_DIR}/spdd/analysis/FEAT-050-billing-analysis.md" <<'AN' # Analysis Context: FEAT-050-billing ## Code Areas - src/billing AN -cat > "${WORK}/spdd/canvas/FEAT-050-billing.md" <<'CV' +cat > "${HOME_DIR}/spdd/canvas/FEAT-050-billing.md" <<'CV' # Canvas CV out="$("${RESOLVE}" --target "${WORK}" --phase code --work-id FEAT-050-billing --format paths)" @@ -143,28 +149,28 @@ assert_contains "${out}" "spdd/canvas/FEAT-050-billing.md" "work-id canvas artif assert_contains "${out}" "spdd/analysis/FEAT-050-billing-analysis.md" "work-id analysis artifact" echo "== Test 10: api-test resolves tasks from phase-index ==" -mkdir -p "${WORK}/spdd/tasks" -echo "# API tasks" > "${WORK}/spdd/tasks/FEAT-050-billing-api-test.md" +mkdir -p "${HOME_DIR}/spdd/tasks" +echo "# API tasks" > "${HOME_DIR}/spdd/tasks/FEAT-050-billing-api-test.md" out="$("${RESOLVE}" --target "${WORK}" --phase api-test --work-id FEAT-050-billing --format paths)" assert_contains "${out}" "spdd/tasks/FEAT-050-billing-api-test.md" "work-id api-test task" assert_contains "${out}" "harness/quality-gates.md" "api-test quality gates from phase-index" echo "== Test 11: resume prompt omits canvas when already resolved ==" "${START}" --target "${WORK}" --work-id FEAT-050-billing --phase code >/dev/null -if grep -Fq "Also read @spdd/canvas/FEAT-050-billing.md" "${WORK}/.sdlc/sessions/current-session.md"; then +if grep -Fq "Also read @spdd/canvas/FEAT-050-billing.md" "${HOME_DIR}/.sdlc/sessions/current-session.md"; then bad "resume prompt should not duplicate canvas already in Resolved Context" else ok "resume prompt skips redundant canvas mention" fi echo "== Test 12: ledger progress excerpt resolves for work-id ==" -mkdir -p "${WORK}/spdd/memory" "${WORK}/.sdlc/staged" +mkdir -p "${HOME_DIR}/spdd/memory" "${HOME_DIR}/.sdlc/staged" printf '%s\n' \ '{"id":"progress:FEAT-050-billing:(none):capture","kind":"progress","work_id":"FEAT-050-billing","title":"T01 complete","ts":"2026-08-08T00:00:00Z"}' \ - > "${WORK}/spdd/memory/lessons.jsonl" + > "${HOME_DIR}/spdd/memory/lessons.jsonl" out="$("${RESOLVE}" --target "${WORK}" --phase code --work-id FEAT-050-billing --format paths)" assert_contains "${out}" ".sdlc/resolved/progress-FEAT-050-billing.md" "scoped progress excerpt" -excerpt="${WORK}/.sdlc/resolved/progress-FEAT-050-billing.md" +excerpt="${HOME_DIR}/.sdlc/resolved/progress-FEAT-050-billing.md" if [[ -f "${excerpt}" ]]; then ok "scoped excerpt file exists" assert_contains "$(cat "${excerpt}")" "T01 complete" "excerpt includes this work" diff --git a/tests/test-sdlc-engine-shim.sh b/tests/test-sdlc-engine-shim.sh index 43b05c19..17cd3587 100755 --- a/tests/test-sdlc-engine-shim.sh +++ b/tests/test-sdlc-engine-shim.sh @@ -1,74 +1,129 @@ #!/usr/bin/env bash -# Smoke: scripts/sdlc.sh can delegate to the Python engine. +# Smoke: scripts/sdlc.sh is a thin dispatcher onto the one Python engine +# (REF-003): no SDLC_ENGINE / SDLC_GATE_ENGINE switch, storage v3 paths only. set -euo pipefail -REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -pass=0 -fail=0 -ok() { echo " ok $1"; pass=$((pass + 1)); } -bad() { echo " FAIL $1" >&2; fail=$((fail + 1)); } +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib/harness.sh +source "${SCRIPT_DIR}/lib/harness.sh" + +REPO_ROOT="${HARNESS_REPO_ROOT}" +SDLC="${REPO_ROOT}/scripts/sdlc.sh" +harness_export_engine +PY="${PYTHON:-python3}" echo "== Python engine importable ==" -if PYTHONPATH="${REPO_ROOT}/engine/src" python3 -c 'import sdlc_engine; print(sdlc_engine.__version__)'; then +if PYTHONPATH="${REPO_ROOT}/engine/src" "${PY}" -c 'import sdlc_engine; print(sdlc_engine.__version__)'; then ok "import sdlc_engine" else bad "import sdlc_engine" fi -echo "== SDLC_ENGINE=python via sdlc.sh ==" -ver="$(SDLC_ENGINE=python "${REPO_ROOT}/scripts/sdlc.sh" version)" +echo "== sdlc.sh version / next with no engine env ==" +ver="$("${SDLC}" version)" if [[ "${ver}" == 2.0.0a* ]]; then - ok "sdlc.sh version via python engine (${ver})" + ok "sdlc.sh version routes to python engine (${ver})" else bad "unexpected version: ${ver}" fi -out="$(SDLC_ENGINE=python "${REPO_ROOT}/scripts/sdlc.sh" next)" +out="$("${SDLC}" next)" if grep -Fq 'Do now' <<< "${out}" || grep -Fq 'No active Work ID' <<< "${out}"; then - ok "sdlc.sh next via python engine" + ok "sdlc.sh next works with no env" else - bad "python next output unexpected" + bad "next output unexpected: ${out}" fi -echo "== default SDLC_ENGINE=auto uses python when importable ==" -out="$( "${REPO_ROOT}/scripts/sdlc.sh" version )" -if [[ "${out}" == 2.0.0a* ]]; then - ok "default auto routes version to python engine (${out})" +echo "== removed engine switches exit 2 and say so ==" +for var in SDLC_ENGINE SDLC_GATE_ENGINE; do + for val in shell python auto; do + rc=0 + err="$(env "${var}=${val}" "${SDLC}" version 2>&1 >/dev/null)" || rc=$? + if [[ "${rc}" -eq 2 ]] && grep -Fq 'removed' <<< "${err}"; then + ok "${var}=${val} exits 2 with removed message" + else + bad "${var}=${val} rc=${rc}: ${err}" + fi + done +done +grep -Fq 'SDLC_ENGINE / SDLC_GATE_ENGINE were removed' <<< "${err}" && ok "removed message names both variables" || bad "message: ${err}" + +echo "== dispatcher shape ==" +grep -Fq 'python' "${SDLC}" && grep -Fq 'sdlc_engine' "${SDLC}" && ok "sdlc.sh execs python -m sdlc_engine" || bad "sdlc.sh does not target sdlc_engine" +for twin in sdlc-workflow.sh sdlc-team-registry.sh sdlc-pointer.sh; do + if grep -Fq "${twin}" "${SDLC}"; then + bad "sdlc.sh still references ${twin}" + else + ok "sdlc.sh does not reference ${twin}" + fi +done +[[ ! -e "${REPO_ROOT}/templates/agent-context/sdlc-workflow.sh" \ + && ! -e "${REPO_ROOT}/templates/agent-context/sdlc-team-registry.sh" \ + && ! -e "${REPO_ROOT}/templates/agent-context/sdlc-pointer.sh" \ + && ! -e "${REPO_ROOT}/scripts/accept-lessons.sh" ]] \ + && ok "bash workflow twins are gone from the repo" || bad "a retired bash twin still exists" + +echo "== sdlc.sh --target operates on the target ==" +T="$(harness_new_target)" +trap 'rm -rf "${T}"' EXIT +HOME_DIR="$(harness_home "${T}")" +"${SDLC}" --target "${T}" pointer set FEAT-SHIM-001 >/dev/null +ptr="$("${SDLC}" --target "${T}" pointer get)" +if [[ "${ptr}" == "FEAT-SHIM-001" ]] && [[ "$(tr -d '[:space:]' < "${HOME_DIR}/.sdlc/pointer")" == "FEAT-SHIM-001" ]]; then + ok "--target pointer set/get hits ${HOME_DIR}/.sdlc/pointer" else - bad "default auto unexpected version: ${out}" + bad "--target pointer: got '${ptr}'" 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" +ptr="$("${SDLC}" pointer get --target "${T}")" +[[ "${ptr}" == "FEAT-SHIM-001" ]] && ok "trailing --target is honored" || bad "trailing --target: '${ptr}'" +ptr="$("${SDLC}" --root "${T}" pointer get)" +[[ "${ptr}" == "FEAT-SHIM-001" ]] && ok "--root alias is honored" || bad "--root alias: '${ptr}'" +ptr="$("${SDLC}" --target="${T}" pointer get)" +[[ "${ptr}" == "FEAT-SHIM-001" ]] && ok "--target=DIR form is honored" || bad "--target=DIR: '${ptr}'" +if [[ "$("${SDLC}" pointer get)" != "FEAT-SHIM-001" ]]; then + ok "orchestrator pointer untouched by --target run" else - bad "shell next unexpected" + bad "--target run leaked into orchestrator pointer" 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. -out="$( - SDLC_ENGINE=shell SDLC_USER=shim-test \ - PYTHONPATH="${REPO_ROOT}/engine/src" \ - python3 -m sdlc_engine --root "${tmp}" local start --name shim-local --intent "offline" -)" +"${SDLC}" --target "${T}" pointer reset >/dev/null +rc=0 +"${SDLC}" --target "${T}/does-not-exist" version >/dev/null 2>&1 || rc=$? +[[ "${rc}" -eq 2 ]] && ok "--target with missing dir exits 2" || bad "--target missing dir rc=${rc}" + +echo "== installed target sdlc.sh is the same dispatcher ==" +tver="$(harness_sdlc "${T}" version)" +[[ "${tver}" == "${ver}" ]] && ok "installed sdlc.sh reports same version (${tver})" || bad "installed version ${tver} != ${ver}" +rc=0 +SDLC_ENGINE=shell "${HOME_DIR}/scripts/sdlc.sh" version >/dev/null 2>&1 || rc=$? +[[ "${rc}" -eq 2 ]] && ok "installed sdlc.sh also rejects SDLC_ENGINE" || bad "installed sdlc.sh SDLC_ENGINE rc=${rc}" + +echo "== local sessions live under sdlc-spdd/.sdlc/local-sessions ==" +out="$(SDLC_USER=shim-test "${SDLC}" --target "${T}" local start --name shim-local --intent "offline")" if grep -Fq 'Started local session LOCAL-' <<< "${out}"; then ok "local start creates LOCAL session" else bad "local start unexpected: ${out}" fi -if find "${tmp}/.sdlc/local-sessions" -path '*/LOCAL-*/session.json' 2>/dev/null | grep -q .; then - ok "local session artifacts under .sdlc/local-sessions" +if find "${HOME_DIR}/.sdlc/local-sessions" -path '*/LOCAL-*/session.json' 2>/dev/null | grep -q .; then + ok "local session artifacts under sdlc-spdd/.sdlc/local-sessions" +else + bad "missing local session artifacts under ${HOME_DIR}/.sdlc/local-sessions" +fi +if [[ ! -d "${T}/.sdlc/local-sessions" && ! -d "${T}/_sdlc" ]]; then + ok "no legacy root .sdlc/_sdlc local-session dirs" else - bad "missing local session artifacts" + bad "local session written to a legacy root path" fi +ptr="$("${SDLC}" --target "${T}" pointer get)" +[[ "${ptr}" == LOCAL-* ]] && ok "local start sets pointer (${ptr})" || bad "pointer after local start: '${ptr}'" +out="$("${SDLC}" --target "${T}" local-list)" +grep -Fq 'LOCAL-001-shim-local' <<< "${out}" && ok "local-list hyphen alias routes to local list" || bad "local-list: ${out}" +"${SDLC}" --target "${T}" shelf >/dev/null +[[ -z "$("${SDLC}" --target "${T}" pointer get)" ]] && ok "shelf clears local pointer" || bad "shelf did not clear local pointer" echo "== work init-from-adf (python engine + sdlc.sh route) ==" -mkdir -p "${tmp}/adf" -cat > "${tmp}/adf/ORCH-8.adf.json" <<'EOF' +mkdir -p "${T}/adf" +cat > "${T}/adf/ORCH-8.adf.json" <<'EOF' { "type": "doc", "version": 1, @@ -85,25 +140,28 @@ cat > "${tmp}/adf/ORCH-8.adf.json" <<'EOF' ] } EOF -# Isolated engine create (sdlc.sh pins --root to the orchestrator checkout). out="$( - PYTHONPATH="${REPO_ROOT}/engine/src" \ - python3 -m sdlc_engine --root "${tmp}" work init-from-adf \ - --path "${tmp}/adf/ORCH-8.adf.json" \ - --work-id FEAT-013-shim-adf-init \ - --no-claim + "${SDLC}" --target "${T}" work init-from-adf \ + --path "${T}/adf/ORCH-8.adf.json" \ + --work-id FEAT-013-shim-adf-init \ + --no-claim )" if grep -Fq 'Created FEAT-013-shim-adf-init' <<< "${out}" \ - && [[ -f "${tmp}/spdd/canvas/FEAT-013-shim-adf-init.md" ]] \ - && [[ -f "${tmp}/requirements/milestones/FEAT-013-shim-adf-init.md" ]] \ - && grep -Fq 'Source System: ADF' "${tmp}/spdd/canvas/FEAT-013-shim-adf-init.md"; then - ok "work init-from-adf creates canvas + requirement" + && [[ -f "${HOME_DIR}/spdd/canvas/FEAT-013-shim-adf-init.md" ]] \ + && [[ -f "${HOME_DIR}/requirements/milestones/FEAT-013-shim-adf-init.md" ]] \ + && grep -Fq 'Source System: ADF' "${HOME_DIR}/spdd/canvas/FEAT-013-shim-adf-init.md"; then + ok "work init-from-adf creates canvas + requirement under sdlc-spdd/" else bad "work init-from-adf unexpected: ${out}" fi +if [[ ! -e "${T}/spdd" && ! -e "${T}/requirements" ]]; then + ok "init-from-adf wrote no legacy root spdd/ or requirements/" +else + bad "init-from-adf wrote to a legacy root path" +fi -# Exercise every sdlc.sh shell entrypoint for this command. Use --dry-run so the -# wrapper's pinned --root (orchestrator checkout) does not write artifacts. +# Exercise every sdlc.sh alias for this command. Use --dry-run so the +# dispatcher's default root (orchestrator checkout) does not write artifacts. alias_ok=1 alias_n=0 while IFS=$'\t' read -r alias_line wid; do @@ -111,13 +169,11 @@ 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" \ - "${REPO_ROOT}/scripts/sdlc.sh" "$@" \ - --path "${tmp}/adf/ORCH-8.adf.json" \ - --work-id "${wid}" \ - --no-claim \ - --dry-run 2>&1 + "${SDLC}" "$@" \ + --path "${T}/adf/ORCH-8.adf.json" \ + --work-id "${wid}" \ + --no-claim \ + --dry-run 2>&1 )" || { bad "sdlc.sh alias failed: ${alias_line} (${out})" alias_ok=0 @@ -129,8 +185,8 @@ while IFS=$'\t' read -r alias_line wid; do bad "sdlc.sh alias unexpected output (${alias_line}): ${out}" alias_ok=0 fi - # Ensure dry-run did not write into the orchestrator checkout. - if [[ -f "${REPO_ROOT}/spdd/canvas/${wid}.md" ]]; then + # Ensure dry-run did not write into the orchestrator checkout home. + if [[ -f "${REPO_ROOT}/sdlc-spdd/spdd/canvas/${wid}.md" || -f "${REPO_ROOT}/spdd/canvas/${wid}.md" ]]; then bad "sdlc.sh dry-run wrote canvas for ${wid}" alias_ok=0 fi @@ -143,11 +199,7 @@ if (( alias_ok == 1 && alias_n == 3 )); then ok "all 3 sdlc.sh init-from-adf aliases routed" fi -help_out="$( - SDLC_ENGINE=shell \ - PYTHONPATH="${REPO_ROOT}/engine/src" \ - "${REPO_ROOT}/scripts/sdlc.sh" work init-from-adf --help 2>&1 -)" +help_out="$("${SDLC}" work init-from-adf --help 2>&1)" if grep -Fq -- '--path' <<< "${help_out}"; then ok "sdlc.sh work init-from-adf --help available" else @@ -156,40 +208,22 @@ fi echo "== db index rebuild via python engine ==" # Seed a tiny work item so rebuild has something to index. -mkdir -p "${tmp}/spdd/canvas" "${tmp}/requirements/milestones" -cat > "${tmp}/spdd/canvas/FEAT-000-shim.md" <<'EOF' -# REASONS Canvas: FEAT-000-shim - Shim - -## Metadata - -- Work ID: FEAT-000-shim -- Status: Draft -- Source Issue: - -## Final Status - -- Status: Draft -EOF -cp "${tmp}/spdd/canvas/FEAT-000-shim.md" "${tmp}/requirements/milestones/FEAT-000-shim.md" +harness_seed_work "${T}" FEAT-000-shim Draft out="$( - SDLC_ENGINE=shell \ - PYTHONPATH="${REPO_ROOT}/engine/src" \ - python3 -m sdlc_engine --root "${tmp}" db rebuild + PYTHONPATH="${REPO_ROOT}/engine/src" \ + "${PY}" -m sdlc_engine --root "${T}" db rebuild )" -if grep -Fq 'Rebuilt SQLite index' <<< "${out}" && [[ -f "${tmp}/.sdlc/index.sqlite" ]]; then - ok "db rebuild creates .sdlc/index.sqlite" +if grep -Fq 'Rebuilt SQLite index' <<< "${out}" && [[ -f "${HOME_DIR}/.sdlc/index.sqlite" ]]; then + ok "db rebuild creates sdlc-spdd/.sdlc/index.sqlite" else bad "db rebuild unexpected: ${out}" fi +[[ ! -e "${T}/.sdlc/index.sqlite" ]] && ok "no legacy root .sdlc/index.sqlite" || bad "index written to legacy root .sdlc" echo "== sdlc.sh --target db rebuild (not next / no pointer) ==" -rm -f "${tmp}/.sdlc/index.sqlite" -out="$( - SDLC_ENGINE=shell \ - PYTHONPATH="${REPO_ROOT}/engine/src" \ - "${REPO_ROOT}/scripts/sdlc.sh" db rebuild --target "${tmp}" -)" -if grep -Fq 'Rebuilt SQLite index' <<< "${out}" && [[ -f "${tmp}/.sdlc/index.sqlite" ]]; then +rm -f "${HOME_DIR}/.sdlc/index.sqlite" +out="$("${SDLC}" db rebuild --target "${T}")" +if grep -Fq 'Rebuilt SQLite index' <<< "${out}" && [[ -f "${HOME_DIR}/.sdlc/index.sqlite" ]]; then ok "sdlc.sh db rebuild --target hits the project" else bad "sdlc.sh --target db rebuild unexpected: ${out}" @@ -199,18 +233,20 @@ if grep -Fiq 'no active' <<< "${out}"; then else ok "sdlc.sh db rebuild --target does not print pointer next" fi +rm -f "${HOME_DIR}/.sdlc/index.sqlite" +out="$("${SDLC}" --target "${T}" db-rebuild)" +grep -Fq 'Rebuilt SQLite index' <<< "${out}" && [[ -f "${HOME_DIR}/.sdlc/index.sqlite" ]] \ + && ok "db-rebuild hyphen alias routes to db rebuild" || bad "db-rebuild alias: ${out}" echo "== python -m sdlc alias ==" out="$( PYTHONPATH="${REPO_ROOT}/engine/src" \ - python3 -m sdlc --root "${tmp}" db path + "${PY}" -m sdlc --root "${T}" db path )" -if grep -Fq "${tmp}" <<< "${out}"; then - ok "python -m sdlc is sdlc_engine" +if grep -Fq "${HOME_DIR}/.sdlc/index.sqlite" <<< "${out}"; then + ok "python -m sdlc is sdlc_engine (db path under sdlc-spdd/.sdlc)" else bad "python -m sdlc unexpected: ${out}" fi -echo -echo "Results: ${pass} passed, ${fail} failed" -if (( fail > 0 )); then exit 1; fi +harness_finish diff --git a/tests/test-sdlc-pointer.sh b/tests/test-sdlc-pointer.sh index 4679caa4..f27726bf 100755 --- a/tests/test-sdlc-pointer.sh +++ b/tests/test-sdlc-pointer.sh @@ -1,100 +1,104 @@ #!/usr/bin/env bash -set -euo pipefail - -# Regression harness for templates/agent-context/sdlc-pointer.sh +# Pointer contract for the Python engine (storage v3, one engine): +# 1. pointer get is empty (exit 0) when unset +# 2. pointer set writes sdlc-spdd/.sdlc/pointer; get reads it back +# 3. pointer reset clears it +# 4. claim sets the pointer; shelf clears it; resume restores it +# 5. --target from the orchestrator dispatcher operates on the target +# 6. start-agent-session.sh --work-id sets the pointer # # Usage: ./tests/test-sdlc-pointer.sh +set -euo pipefail 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 +# shellcheck source=lib/harness.sh +source "${SCRIPT_DIR}/lib/harness.sh" -pass=0 -fail=0 -ok() { echo " ok $1"; pass=$((pass + 1)); } -bad() { echo " FAIL $1" >&2; fail=$((fail + 1)); } +START="${HARNESS_REPO_ROOT}/scripts/start-agent-session.sh" -run_pointer() { - SDLC_ROOT="${1}" "${POINTER}" "${@:2}" -} +T="$(harness_new_target)" +trap 'rm -rf "${T}"' EXIT +HOME_DIR="$(harness_home "${T}")" +POINTER_FILE="${HOME_DIR}/.sdlc/pointer" -source_pointer() { - SDLC_ROOT="${1}" - # shellcheck source=/dev/null - source "${POINTER}" -} +# --------------------------------------------------------------------------- +echo "== Test 1: pointer get is empty and exits 0 when unset ==" +rc=0 +current="$(harness_sdlc "${T}" pointer get)" || rc=$? +if [[ "${rc}" -eq 0 ]]; then ok "pointer get exits 0 when unset"; else bad "pointer get exit ${rc} when unset"; fi +if [[ -z "${current}" ]]; then ok "pointer get prints empty when unset"; else bad "expected empty pointer, got '${current}'"; fi # --------------------------------------------------------------------------- -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)" +echo "== Test 2: pointer set writes sdlc-spdd/.sdlc/pointer ==" +harness_sdlc "${T}" pointer set FEAT-001 >/dev/null +if [[ -f "${POINTER_FILE}" ]]; then ok "pointer file created under sdlc-spdd/.sdlc"; else bad "missing ${POINTER_FILE}"; fi +stored="$(tr -d '[:space:]' < "${POINTER_FILE}")" +if [[ "${stored}" == "FEAT-001" ]]; then ok "pointer file holds FEAT-001"; else bad "pointer file holds '${stored}'"; fi +if [[ ! -e "${T}/.sdlc/pointer" ]]; then ok "no root-level .sdlc/pointer (legacy layout)"; else bad "pointer written to legacy root .sdlc"; fi +current="$(harness_sdlc "${T}" pointer 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: pointer reset clears it ==" +harness_sdlc "${T}" pointer reset >/dev/null +current="$(harness_sdlc "${T}" pointer get)" +if [[ -z "${current}" ]]; then ok "reset clears pointer"; else bad "expected empty pointer after reset, got '${current}'"; fi +if [[ ! -s "${POINTER_FILE}" ]]; then ok "pointer file absent or empty after reset"; else bad "pointer file still holds '$(cat "${POINTER_FILE}")'"; 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: claim sets, shelf clears, resume restores ==" +harness_seed_work "${T}" FEAT-002-claim +SDLC_USER="pointer-test" harness_sdlc "${T}" claim FEAT-002-claim >/dev/null +current="$(harness_sdlc "${T}" pointer get)" +if [[ "${current}" == "FEAT-002-claim" ]]; then ok "claim sets pointer"; else bad "claim: expected FEAT-002-claim, got '${current}'"; fi + +harness_sdlc "${T}" shelf --reason "pointer test" >/dev/null +current="$(harness_sdlc "${T}" pointer get)" +if [[ -z "${current}" ]]; then ok "shelf clears pointer"; else bad "shelf: expected empty pointer, got '${current}'"; fi + +harness_sdlc "${T}" resume FEAT-002-claim >/dev/null +current="$(harness_sdlc "${T}" pointer get)" +if [[ "${current}" == "FEAT-002-claim" ]]; then ok "resume restores pointer"; else bad "resume: expected FEAT-002-claim, got '${current}'"; fi + +harness_sdlc "${T}" release >/dev/null +current="$(harness_sdlc "${T}" pointer get)" +if [[ -z "${current}" ]]; then ok "release clears pointer"; else bad "release: expected empty pointer, got '${current}'"; fi + +# --------------------------------------------------------------------------- +echo "== Test 5: orchestrator sdlc.sh --target operates on the target pointer ==" +ORCH_SDLC="${HARNESS_REPO_ROOT}/scripts/sdlc.sh" +harness_export_engine +"${ORCH_SDLC}" --target "${T}" pointer set SPIKE-001 >/dev/null +current="$(harness_sdlc "${T}" pointer get)" +if [[ "${current}" == "SPIKE-001" ]]; then ok "--target pointer set lands in target"; else bad "--target set: got '${current}'"; fi +current="$("${ORCH_SDLC}" pointer get --target "${T}")" +if [[ "${current}" == "SPIKE-001" ]]; then ok "trailing --target pointer get reads target"; else bad "trailing --target get: got '${current}'"; fi +"${ORCH_SDLC}" --target "${T}" pointer reset >/dev/null +current="$(harness_sdlc "${T}" pointer get)" +if [[ -z "${current}" ]]; then ok "--target pointer reset clears target"; else bad "--target reset: got '${current}'"; 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 +echo "== Test 6: start-agent-session.sh sets pointer from --work-id ==" +harness_seed_work "${T}" FEAT-003-session +"${START}" --target "${T}" --work-id FEAT-003-session --phase plan >/dev/null +current="$(harness_sdlc "${T}" pointer get)" +if [[ "${current}" == "FEAT-003-session" ]]; then ok "start-agent-session sets pointer" else - bad "expected FEAT-002-session from session start, got '${current}'" + bad "expected FEAT-003-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" +echo "== Test 7: init-project installs no bash pointer twin ==" +if [[ ! -e "${HOME_DIR}/scripts/sdlc-pointer.sh" ]]; then + ok "sdlc-pointer.sh twin is not installed" else - bad "init-project missing sdlc-spdd/scripts/sdlc-pointer.sh" + bad "retired sdlc-pointer.sh twin installed" fi - -# --------------------------------------------------------------------------- -echo -echo "Results: ${pass} passed, ${fail} failed" -if [[ "${fail}" -gt 0 ]]; then - exit 1 +if [[ -x "${HOME_DIR}/scripts/sdlc.sh" ]]; then + ok "sdlc.sh dispatcher installed" +else + bad "sdlc.sh dispatcher missing" fi + +harness_finish diff --git a/tests/test-sdlc-workflow.sh b/tests/test-sdlc-workflow.sh index 5eb2d735..bcfa6caf 100755 --- a/tests/test-sdlc-workflow.sh +++ b/tests/test-sdlc-workflow.sh @@ -1,872 +1,488 @@ #!/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 +# Workflow-state contract for the Python engine (storage v3, one engine): +# claim -> next -> advance -> skip -> shelf -> resume -> gate +# Asserts on sdlc-spdd/.sdlc/workflows/<WID>.state / .history, on +# `status --json`, and on the team registry at sdlc-spdd/spdd/memory/registry.jsonl. +# +# Shell gate fallback (SDLC_GATE_ENGINE=shell) removed in REF-003; there is one engine. # # Usage: ./tests/test-sdlc-workflow.sh +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_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" +# shellcheck source=lib/harness.sh +source "${SCRIPT_DIR}/lib/harness.sh" +START="${HARNESS_REPO_ROOT}/scripts/start-agent-session.sh" +EXAMPLE_CANVAS="${HARNESS_REPO_ROOT}/examples/spring-boot-order-api/spdd/canvas/FEAT-001-order-status-api.md" + +# All fixture targets are created under one WORK root so a single trap cleans up. 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)); } +new_target() { + TMPDIR="${WORK}" harness_new_target +} + +state_file() { printf '%s' "$(harness_home "$1")/.sdlc/workflows/$2.state"; } +history_file() { printf '%s' "$(harness_home "$1")/.sdlc/workflows/$2.history"; } +registry_file() { printf '%s' "$(harness_home "$1")/spdd/memory/registry.jsonl"; } -registry_file() { - local t="$1" - printf '%s' "${t}/spdd/memory/registry.jsonl" +state_val() { + local t="$1" work_id="$2" key="$3" + grep "^${key}=" "$(state_file "${t}" "${work_id}")" | head -1 | cut -d= -f2- || true } +pointer_of() { harness_sdlc "$1" pointer get; } + +# Latest registry event for a Work ID must match the regex. 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" - mkdir -p "${t}/.sdlc/sessions" \ - "${t}/agent-context" \ - "${t}/spdd/canvas" \ - "${t}/spdd/analysis" \ - "${t}/scripts/lib" - cp "${POINTER}" "${t}/agent-context/sdlc-pointer.sh" - cp "${WORKFLOW}" "${t}/agent-context/sdlc-workflow.sh" - cp "${TEAM_REG}" "${t}/agent-context/sdlc-team-registry.sh" - cp "${REPO_ROOT}/scripts/lib/paths.sh" "${t}/scripts/lib/paths.sh" - mkdir -p "${t}/spdd/memory" - : > "${t}/spdd/memory/registry.jsonl" - cp "${REPO_ROOT}/scripts/lib/readiness.sh" "${t}/scripts/lib/readiness.sh" - chmod +x "${t}/agent-context/sdlc-pointer.sh" "${t}/agent-context/sdlc-workflow.sh" "${t}/agent-context/sdlc-team-registry.sh" + [[ -f "${reg}" ]] && grep "\"work_id\": \"${work_id}\"" "${reg}" | tail -1 | grep -Eq "${regex}" } -# --------------------------------------------------------------------------- -echo "== Test 1: resume sets pointer and creates workflow state ==" -T="${WORK}/resume" -setup_feature "${T}" "FEAT-001-alpha" -wf "${T}" resume FEAT-001-alpha >/dev/null -ptr="$(SDLC_ROOT="${T}" "${T}/agent-context/sdlc-pointer.sh" get)" -if [[ "${ptr}" == "FEAT-001-alpha" ]]; then ok "resume sets pointer"; else bad "pointer not set"; fi -if [[ -f "${T}/.sdlc/workflows/FEAT-001-alpha.state" ]]; then ok "workflow state created"; else bad "missing state file"; fi - -# --------------------------------------------------------------------------- -echo "== Test 2: advance moves through phases ==" -wf "${T}" advance >/dev/null -phase="$(grep '^phase=' "${T}/.sdlc/workflows/FEAT-001-alpha.state" | cut -d= -f2)" -if [[ "${phase}" == "analysis" ]]; then ok "advance to analysis"; else bad "expected analysis, got ${phase}"; fi - -# --------------------------------------------------------------------------- -echo "== Test 3: skip records reason and moves past phase ==" -wf "${T}" skip api-test --reason "no HTTP surface" >/dev/null -if grep -q '^skip_api-test=' "${T}/.sdlc/workflows/FEAT-001-alpha.state"; then - ok "skip recorded in state" -else - bad "skip not recorded" -fi - -# --------------------------------------------------------------------------- -echo "== Test 4: shelf and resume shelved work ==" -wf "${T}" shelf --reason "context switch" >/dev/null -ptr="$(SDLC_ROOT="${T}" "${T}/agent-context/sdlc-pointer.sh" get)" -if [[ -z "${ptr}" ]]; then ok "shelf clears pointer"; else bad "pointer should be empty"; fi -active="$(grep '^active=' "${T}/.sdlc/workflows/FEAT-001-alpha.state" | cut -d= -f2)" -if [[ "${active}" == "0" ]]; then ok "shelf marks inactive"; else bad "expected active=0"; fi - -setup_feature "${T}" "CHORE-002-beta" -wf "${T}" resume CHORE-002-beta >/dev/null -if wf "${T}" list-shelved | grep -q 'FEAT-001-alpha'; then ok "shelved list includes parked work"; else bad "shelved list missing FEAT-001-alpha"; fi -wf "${T}" resume FEAT-001-alpha >/dev/null -ptr="$(SDLC_ROOT="${T}" "${T}/agent-context/sdlc-pointer.sh" get)" -if [[ "${ptr}" == "FEAT-001-alpha" ]]; then ok "resume restores shelved pointer"; else bad "resume failed"; fi - -# --------------------------------------------------------------------------- -echo "== Test 5: sync infers phase from artifacts ==" -T="${WORK}/sync" -work_id="FEAT-003-gamma" -setup_feature "${T}" "${work_id}" -mkdir -p "${T}/requirements/milestones" -printf '# req\n' > "${T}/requirements/milestones/${work_id}.md" -printf '# analysis\n' > "${T}/spdd/analysis/${work_id}-analysis.md" -printf '# canvas\nReady For Coding\n' > "${T}/spdd/canvas/${work_id}.md" -wf "${T}" resume "${work_id}" >/dev/null -phase="$(grep '^phase=' "${T}/.sdlc/workflows/${work_id}.state" | cut -d= -f2)" -if [[ "${phase}" == "code" ]]; then ok "sync infers code from artifacts"; else bad "expected code, got ${phase}"; fi -if grep -q '^gate_canvas_exists=passed' "${T}/.sdlc/workflows/${work_id}.state"; then - ok "sync marks canvas gate passed" -else - bad "canvas gate not passed" -fi - -# --------------------------------------------------------------------------- -echo "== Test 6: status output is human-readable ==" -out="$(wf "${T}" status "${work_id}")" -if grep -q 'Quality gates:' <<< "${out}" && grep -q 'Phase track:' <<< "${out}"; then - ok "status shows gates and phase track" -else - bad "status output incomplete" -fi - -# --------------------------------------------------------------------------- -echo "== Test 7: session scripts update workflow timestamps ==" -T="${WORK}/integrate" -work_id="FEAT-004-delta" -setup_feature "${T}" "${work_id}" -"${START}" --target "${T}" --work-id "${work_id}" --phase plan >/dev/null -if grep -q '^last_session_at=' "${T}/.sdlc/workflows/${work_id}.state"; then - ok "start-agent-session touches workflow" -else - bad "missing last_session_at" -fi -"${CAPTURE}" --target "${T}" --work-id "${work_id}" --phase plan --summary "planned" >/dev/null -if grep -q '^last_capture_at=' "${T}/.sdlc/workflows/${work_id}.state"; then - ok "capture-session-memory records workflow capture" -else - bad "missing last_capture_at" -fi - -# --------------------------------------------------------------------------- -echo "== Test 8: next command gives actionable output ==" -T="${WORK}/next" -work_id="FEAT-005-next" -setup_feature "${T}" "${work_id}" -printf '# canvas\nReady For Coding\n' > "${T}/spdd/canvas/${work_id}.md" -wf "${T}" resume "${work_id}" >/dev/null -out="$(wf "${T}" next)" -if grep -q 'Do now (assistant):' <<< "${out}" && grep -q 'When this phase is done:' <<< "${out}"; then - ok "next output is actionable" -else - bad "next output missing sections" -fi - -# --------------------------------------------------------------------------- -echo "== Test 9: status --json for agents ==" -json="$(wf "${T}" status --json)" -if grep -q '"phase":"code"' <<< "${json}" && grep -q '"recommended_command"' <<< "${json}"; then - ok "json status includes phase and command" -else - bad "json status incomplete: ${json}" -fi - -# --------------------------------------------------------------------------- -echo "== Test 10: sdlc.sh wrapper delegates ==" -T="${WORK}/wrapper" -work_id="FEAT-005-wrap" -setup_feature "${T}" "${work_id}" -mkdir -p "${T}/scripts/sdlc-spdd" -cp "${REPO_ROOT}/scripts/sdlc.sh" "${T}/scripts/sdlc-spdd/sdlc.sh" -chmod +x "${T}/scripts/sdlc-spdd/sdlc.sh" -SDLC_ROOT="${T}" "${T}/scripts/sdlc-spdd/sdlc.sh" resume "${work_id}" >/dev/null -out="$(SDLC_ROOT="${T}" "${T}/scripts/sdlc-spdd/sdlc.sh" next)" -if grep -q "${work_id}" <<< "${out}"; then ok "sdlc.sh wrapper works"; else bad "sdlc.sh wrapper failed"; fi - -# --------------------------------------------------------------------------- -echo "== Test 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}/scripts/sdlc-spdd" "${T}/spdd/canvas" -printf '%s\n' "# ${work_id}" '' '## Final Status' '' '- Status: In Progress' \ - > "${T}/spdd/canvas/${work_id}.md" -cp "${REPO_ROOT}/scripts/sdlc.sh" "${T}/scripts/sdlc-spdd/sdlc.sh" -chmod +x "${T}/scripts/sdlc-spdd/sdlc.sh" -if SDLC_USER="tester" SDLC_ROOT="${T}" "${T}/scripts/sdlc-spdd/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" +# Canvas with structured Readiness and one T01 op (Files mapping present so the +# code gate's semantic minima are satisfied when readiness allows coding). +write_canvas() { + local t="$1" work_id="$2" readiness="$3" op_status="${4:-Not Started}" + harness_seed_work "${t}" "${work_id}" + local readiness_line="" + if [[ -n "${readiness}" ]]; then + readiness_line="- Readiness: ${readiness}" 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/sessions/current-session.md" \ - && grep -q 'Assistant command' "${T}/.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}/spdd/canvas/${work_id}.md" -wf "${T}" resume "${work_id}" >/dev/null -wf "${T}" sync "${work_id}" >/dev/null -op="$(grep '^operation=' "${T}/.sdlc/workflows/${work_id}.state" | cut -d= -f2)" -if [[ "${op}" == "T03" ]]; then ok "sync infers next operation T03"; else bad "expected T03, got ${op}"; fi -out="$(wf "${T}" next)" -if grep -q 'operation T03' <<< "${out}"; then ok "next recommends T03 in code command"; else bad "next missing T03 command"; fi -json="$(wf "${T}" status --json)" -if grep -q '"operation":"T03"' <<< "${json}" && grep -q '"operation_title"' <<< "${json}"; then - ok "json includes operation and title" -else - bad "json missing operation fields" -fi - -# --------------------------------------------------------------------------- -echo "== Test 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}/spdd/canvas/${work_id}.md" <<'EOF' -# REASONS Canvas: FEAT-012b-final - empty Final Status + cat > "$(harness_home "${t}")/spdd/canvas/${work_id}.md" <<EOF +# REASONS Canvas: ${work_id} ## 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/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}/spdd/canvas/${work_id}.md" <<'EOF' -# REASONS Canvas: FEAT-012c-readiness -## Metadata -- Work ID: FEAT-012c-readiness +- Work ID: ${work_id} - Status: In Progress -- Readiness: Needs Analysis - -## O - Operations - -### T01 - First - -- Status: Not Started +${readiness_line} -## 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}/spdd/canvas/${work_id}.md" <<'EOF' -# REASONS Canvas: FEAT-012d-advance +## R - Requirements -## Metadata -- Work ID: FEAT-012d-advance -- Status: In Progress -- Readiness: Needs Clarification +- Harness requirement for ${work_id}. ## O - Operations ### T01 - First -- Status: Not Started +- Status: ${op_status} +- Files: src/first.py ## 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/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/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}/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}/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/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}/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}/spdd/canvas/${work_id}.md" <<'EOF' +echo "== Test 1: claim sets pointer and creates workflow state + history ==" +T="$(new_target)" +W="FEAT-001-alpha" +harness_seed_work "${T}" "${W}" +SDLC_USER="alice" harness_sdlc "${T}" claim "${W}" >/dev/null +[[ "$(pointer_of "${T}")" == "${W}" ]] && ok "claim sets pointer" || bad "pointer not set by claim" +[[ -f "$(state_file "${T}" "${W}")" ]] && ok "workflow .state created under sdlc-spdd/.sdlc/workflows" || bad "missing state file" +[[ -f "$(history_file "${T}" "${W}")" ]] && ok "workflow .history created" || bad "missing history file" +grep -q $'\tcreate\twork_id='"${W}" "$(history_file "${T}" "${W}")" && ok "history records create" || bad "history missing create" +grep -q $'\tresume\tphase=' "$(history_file "${T}" "${W}")" && ok "history records resume" || bad "history missing resume" +[[ "$(state_val "${T}" "${W}" work_id)" == "${W}" ]] && ok "state work_id" || bad "state work_id wrong" +[[ "$(state_val "${T}" "${W}" active)" == "1" ]] && ok "state active=1 after claim" || bad "expected active=1" +[[ "$(state_val "${T}" "${W}" phase)" == "architect" ]] && ok "canvas-only work infers architect" || bad "expected architect, got $(state_val "${T}" "${W}" phase)" +[[ "$(state_val "${T}" "${W}" gate_canvas_exists)" == "passed" ]] && ok "gate_canvas_exists=passed" || bad "canvas gate not passed" +[[ "$(state_val "${T}" "${W}" gate_requirement_documented)" == "passed" ]] && ok "gate_requirement_documented=passed" || bad "requirement gate not passed" +[[ ! -d "${T}/.sdlc" ]] && ok "no legacy root .sdlc created" || bad "legacy root .sdlc created" +registry_matches "${T}" "${W}" '"status": "active".*"owner": "alice"' && ok "claim writes team registry (active, alice)" || bad "registry row missing" + +# --------------------------------------------------------------------------- +echo "== Test 2: next is actionable and re-syncs state ==" +out="$(harness_sdlc "${T}" next)" +grep -q "Work ID: ${W}" <<< "${out}" && ok "next names the Work ID" || bad "next missing Work ID" +grep -q 'Do now (assistant):' <<< "${out}" && ok "next has Do now section" || bad "next missing Do now" +grep -q 'When this phase is done:' <<< "${out}" && ok "next has phase-done section" || bad "next missing phase-done" +grep -q 'sdlc-spdd-architect' <<< "${out}" && ok "next recommends architect at architect phase" || bad "next should recommend architect" +grep -q '(moves to: code)' <<< "${out}" && ok "next names the following phase" || bad "next missing moves-to" + +# --------------------------------------------------------------------------- +echo "== Test 3: advance architect->code is gated on canvas readiness ==" +rc=0 +harness_sdlc "${T}" advance >/dev/null 2>"${T}/advance.err" || rc=$? +[[ "${rc}" -ne 0 ]] && ok "advance refused for non-ready canvas (rc=${rc})" || bad "advance should fail on non-ready canvas" +grep -q "not Ready For Coding" "${T}/advance.err" && ok "advance error names readiness" || bad "advance error missing readiness: $(cat "${T}/advance.err")" +grep -q "missing structured Readiness field" "${T}/advance.err" && ok "absent readiness is reported (no compat pass-through)" || bad "expected missing-readiness message" +[[ "$(state_val "${T}" "${W}" phase)" == "architect" ]] && ok "phase stays architect after refusal" || bad "phase changed after refused advance" + +write_canvas "${T}" "${W}" "Ready For Coding" +harness_sdlc "${T}" advance >/dev/null && ok "advance succeeds once Ready For Coding" || bad "advance should succeed when ready" +[[ "$(state_val "${T}" "${W}" phase)" == "code" ]] && ok "phase=code after advance" || bad "expected code, got $(state_val "${T}" "${W}" phase)" +grep -q $'\tadvance\tphase=code' "$(history_file "${T}" "${W}")" && ok "history records advance" || bad "history missing advance" +harness_sdlc "${T}" sync >/dev/null +[[ "$(state_val "${T}" "${W}" phase)" == "code" ]] && ok "sync keeps code for Ready For Coding canvas" || bad "sync moved phase to $(state_val "${T}" "${W}" phase)" +[[ "$(state_val "${T}" "${W}" operation)" == "T01" ]] && ok "sync infers operation T01 from canvas" || bad "expected operation T01" + +# --------------------------------------------------------------------------- +echo "== Test 4: skip records reason and moves past the phase ==" +out="$(harness_sdlc "${T}" skip api-test --reason "no HTTP surface")" +grep -q 'Skipped api-test; now at review' <<< "${out}" && ok "skip reports new phase" || bad "skip output: ${out}" +[[ "$(state_val "${T}" "${W}" skip_api-test)" == "no HTTP surface" ]] && ok "skip reason recorded in state" || bad "skip not recorded" +[[ "$(state_val "${T}" "${W}" phase)" == "review" ]] && ok "skip moved phase to review" || bad "expected review" +grep -q $'\tskip\tphase=api-test reason=no HTTP surface' "$(history_file "${T}" "${W}")" && ok "history records skip" || bad "history missing skip" + +# --------------------------------------------------------------------------- +echo "== Test 5: shelf clears pointer; resume restores; auto-shelf on switch ==" +harness_sdlc "${T}" shelf --reason "context switch" >/dev/null +[[ -z "$(pointer_of "${T}")" ]] && ok "shelf clears pointer" || bad "pointer should be empty" +[[ "$(state_val "${T}" "${W}" active)" == "0" ]] && ok "shelf marks active=0" || bad "expected active=0" +[[ "$(state_val "${T}" "${W}" shelved_reason)" == "context switch" ]] && ok "shelf stores reason" || bad "shelved_reason wrong" +[[ -n "$(state_val "${T}" "${W}" shelved_at)" ]] && ok "shelf stamps shelved_at" || bad "shelved_at empty" +grep -q $'\tshelf\tcontext switch' "$(history_file "${T}" "${W}")" && ok "history records shelf" || bad "history missing shelf" +harness_sdlc "${T}" list-shelved | grep -q "^${W}"$'\t' && ok "list-shelved includes parked work" || bad "list-shelved missing ${W}" + +rc=0 +harness_sdlc "${T}" shelf >/dev/null 2>&1 || rc=$? +[[ "${rc}" -ne 0 ]] && ok "shelf with no pointer exits non-zero" || bad "shelf without pointer should fail" + +W2="CHORE-002-beta" +harness_seed_work "${T}" "${W2}" +harness_sdlc "${T}" resume "${W2}" >/dev/null +[[ "$(pointer_of "${T}")" == "${W2}" ]] && ok "resume of second work sets pointer" || bad "pointer not ${W2}" +harness_sdlc "${T}" resume "${W}" >/dev/null +[[ "$(pointer_of "${T}")" == "${W}" ]] && ok "resume restores shelved pointer" || bad "resume failed" +[[ "$(state_val "${T}" "${W}" active)" == "1" ]] && ok "resume sets active=1" || bad "expected active=1" +[[ -z "$(state_val "${T}" "${W}" shelved_reason)" ]] && ok "resume clears shelved_reason" || bad "shelved_reason not cleared" +[[ "$(state_val "${T}" "${W2}" active)" == "0" ]] && ok "switching work auto-shelves previous" || bad "${W2} should be auto-shelved" +[[ "$(state_val "${T}" "${W2}" shelved_reason)" == "auto-shelf for resume ${W}" ]] && ok "auto-shelf reason recorded" || bad "auto-shelf reason: '$(state_val "${T}" "${W2}" shelved_reason)'" +harness_sdlc "${T}" list-shelved | grep -q "^${W2}"$'\t' && ok "list-shelved shows auto-shelved work" || bad "list-shelved missing ${W2}" + +# --------------------------------------------------------------------------- +echo "== Test 6: sync infers phase from artifacts ==" +T="$(new_target)" +W="FEAT-003-gamma" +H="$(harness_home "${T}")" +mkdir -p "${H}/requirements/milestones" "${H}/spdd/analysis" "${H}/spdd/canvas" +printf '# Requirement: %s\n' "${W}" > "${H}/requirements/milestones/${W}.md" +harness_sdlc "${T}" resume "${W}" >/dev/null +[[ "$(state_val "${T}" "${W}" phase)" == "analysis" ]] && ok "requirement only -> analysis" || bad "expected analysis, got $(state_val "${T}" "${W}" phase)" +printf '# analysis\n' > "${H}/spdd/analysis/${W}-analysis.md" +harness_sdlc "${T}" sync >/dev/null +[[ "$(state_val "${T}" "${W}" phase)" == "plan" ]] && ok "+analysis -> plan" || bad "expected plan" +write_canvas "${T}" "${W}" "Needs Analysis" +harness_sdlc "${T}" sync >/dev/null +[[ "$(state_val "${T}" "${W}" phase)" == "architect" ]] && ok "+canvas (not ready) -> architect" || bad "expected architect" +write_canvas "${T}" "${W}" "Ready For Coding" +out="$(harness_sdlc "${T}" sync --work-id "${W}")" +grep -q "Synced ${W} -> phase code" <<< "${out}" && ok "sync --work-id reports code" || bad "sync output: ${out}" +[[ "$(state_val "${T}" "${W}" phase)" == "code" ]] && ok "Ready For Coding canvas -> code" || bad "expected code" +[[ "$(state_val "${T}" "${W}" gate_canvas_exists)" == "passed" ]] && ok "sync marks canvas gate passed" || bad "canvas gate not passed" +[[ "$(state_val "${T}" "${W}" gate_requirement_documented)" == "passed" ]] && ok "sync marks requirement gate passed" || bad "requirement gate not passed" +grep -q $'\tsync\tphase=code' "$(history_file "${T}" "${W}")" && ok "history records sync" || bad "history missing sync" + +# --------------------------------------------------------------------------- +echo "== Test 7: status is human-readable; status --json for agents ==" +out="$(harness_sdlc "${T}" status)" +grep -q "^Pointer: ${W}" <<< "${out}" && ok "status shows pointer" || bad "status missing Pointer" +grep -q "^Work ID: ${W}" <<< "${out}" && ok "status shows Work ID" || bad "status missing Work ID" +json="$(harness_sdlc "${T}" status --json | tr -d ' \n')" +grep -q "\"pointer\":\"${W}\"" <<< "${json}" && ok "json pointer" || bad "json pointer missing" +grep -q "\"work_id\":\"${W}\"" <<< "${json}" && ok "json work_id" || bad "json work_id missing" +grep -q '"phase":"code"' <<< "${json}" && ok "json phase=code" || bad "json phase wrong: ${json}" +grep -q '"operation":"T01"' <<< "${json}" && ok "json operation=T01" || bad "json operation missing" +grep -q '"operation_title":"First"' <<< "${json}" && ok "json operation_title" || bad "json operation_title missing" +grep -q '"active":true' <<< "${json}" && ok "json active=true" || bad "json active missing" +grep -q '"recommended_command":"/sdlc-spdd-code' <<< "${json}" && ok "json recommended_command is code" || bad "json recommended_command wrong" +grep -q '"gates":{' <<< "${json}" && grep -q '"canvas_exists":"passed"' <<< "${json}" && ok "json gates include canvas_exists=passed" || bad "json gates incomplete" +grep -q '"phases":\["init","analysis","plan","architect","code","api-test","review","prompt-update","retro","sync"\]' <<< "${json}" && ok "json lists phase track" || bad "json phases wrong" +json_other="$(harness_sdlc "${T}" status --json --work-id "${W}" | tr -d ' \n')" +grep -q '"phase":"code"' <<< "${json_other}" && ok "status --json --work-id works" || bad "status --work-id failed" + +# --------------------------------------------------------------------------- +echo "== Test 8: status --json / next without a pointer ==" +harness_sdlc "${T}" shelf --reason park >/dev/null +json="$(harness_sdlc "${T}" status --json | tr -d ' \n')" +grep -q '"pointer":""' <<< "${json}" && grep -q '"work_id":null' <<< "${json}" && ok "json shows empty pointer / null work_id" || bad "json without pointer: ${json}" +out="$(harness_sdlc "${T}" next)" +grep -q 'No active Work ID pointer' <<< "${out}" && ok "next explains missing pointer" || bad "next without pointer: ${out}" +grep -q 'list-shelved' <<< "${out}" && ok "next hints at list-shelved when work is parked" || bad "next missing list-shelved hint" +rc=0 +harness_sdlc "${T}" advance >/dev/null 2>&1 || rc=$? +[[ "${rc}" -ne 0 ]] && ok "advance without pointer exits non-zero" || bad "advance without pointer should fail" +rc=0 +harness_sdlc "${T}" gate --phase code >/dev/null 2>&1 || rc=$? +[[ "${rc}" -eq 2 ]] && ok "gate without pointer exits 2" || bad "gate without pointer rc=${rc}" +harness_sdlc "${T}" resume "${W}" >/dev/null + +# --------------------------------------------------------------------------- +echo "== Test 9: gate --phase reports OK / BLOCKED with --json ==" +out="$(harness_sdlc "${T}" gate --phase code)" +grep -q "gate code: OK for ${W}" <<< "${out}" && ok "gate code OK for ready canvas" || bad "gate output: ${out}" +json="$(harness_sdlc "${T}" gate --phase code --json | tr -d ' \n')" +grep -q '"ok":true' <<< "${json}" && grep -q '"failures":\[\]' <<< "${json}" && ok "gate --json ok=true, no failures" || bad "gate json: ${json}" +grep -q '"advisory":\[{"gate":"architect_review"' <<< "${json}" && ok "gate --json lists advisory gates" || bad "gate json advisory missing" +write_canvas "${T}" "${W}" "Needs Clarification" +rc=0 +harness_sdlc "${T}" gate --phase code >"${T}/gate.out" 2>"${T}/gate.err" || rc=$? +[[ "${rc}" -eq 1 ]] && ok "gate exits 1 when blocked" || bad "gate rc=${rc} when blocked" +grep -q "gate code: BLOCKED for ${W}" "${T}/gate.err" && ok "gate prints BLOCKED to stderr" || bad "gate BLOCKED missing" +grep -q "readiness is needs-clarification, not Ready For Coding" "${T}/gate.err" && ok "gate names the readiness failure" || bad "gate failure text: $(cat "${T}/gate.err")" +rc=0 +json="$(harness_sdlc "${T}" gate --phase code --json | tr -d ' \n')" || rc=$? +grep -q '"ok":false' <<< "${json}" && ok "gate --json ok=false when blocked" || bad "gate json blocked: ${json}" +json="$(harness_sdlc "${T}" gate --phase review --json --work-id "${W}" | tr -d ' \n' || true)" +grep -q 'noledgerevidence' <<< "${json}" && ok "gate review requires ledger evidence" || bad "gate review json: ${json}" + +# --------------------------------------------------------------------------- +echo "== Test 10: session scripts update workflow timestamps + stage captures ==" +T="$(new_target)" +W="FEAT-004-delta" +H="$(harness_home "${T}")" +write_canvas "${T}" "${W}" "Ready For Coding" +"${START}" --target "${T}" --work-id "${W}" --phase plan >/dev/null +[[ -n "$(state_val "${T}" "${W}" last_session_at)" ]] && ok "start-agent-session stamps last_session_at" || bad "missing last_session_at" +[[ "$(pointer_of "${T}")" == "${W}" ]] && ok "start-agent-session sets pointer" || bad "pointer not set" +[[ -f "${H}/.sdlc/sessions/current-session.md" ]] && ok "session brief written under sdlc-spdd/.sdlc/sessions" || bad "missing current-session.md" +grep -q '## Workflow State' "${H}/.sdlc/sessions/current-session.md" \ + && grep -q 'Assistant command' "${H}/.sdlc/sessions/current-session.md" \ + && ok "session brief embeds workflow state" || bad "session brief missing workflow state" +"${H}/scripts/capture-session-memory.sh" --target "${T}" --work-id "${W}" --phase plan --summary "planned" >/dev/null +[[ -n "$(state_val "${T}" "${W}" last_capture_at)" ]] && ok "capture-session-memory stamps last_capture_at" || bad "missing last_capture_at" +[[ -s "${H}/.sdlc/staged/lessons.jsonl" ]] && ok "capture stages into sdlc-spdd/.sdlc/staged/lessons.jsonl" || bad "staged ledger empty" +grep -q "\"work_id\": \"${W}\"" "${H}/.sdlc/staged/lessons.jsonl" && ok "staged record carries work_id" || bad "staged record lacks work_id" +[[ ! -f "${T}/.sdlc/staged/lessons.jsonl" ]] && ok "no legacy root staged ledger" || bad "legacy root staged ledger written" +brief="$(harness_sdlc "${T}" session brief)" +grep -q "| Work ID | ${W} |" <<< "${brief}" && grep -q '| Readiness | ready-for-coding |' <<< "${brief}" \ + && ok "session brief has Work ID + Readiness rows" || bad "session brief: ${brief}" + +# --------------------------------------------------------------------------- +echo "== Test 11: capture is guarded by the pointer ==" +# The Ready For Coding canvas re-syncs the phase to code, where a verify +# receipt is mandatory; pin --phase plan for the plain guarded-capture case. +harness_sdlc "${T}" capture --phase plan --summary "ok" >/dev/null 2>&1 && ok "capture succeeds when pointer matches" || bad "capture should succeed for active pointer" +rc=0 +# Engine prints the mismatch on stdout, so capture both streams. +harness_sdlc "${T}" capture --phase plan --work-id FEAT-999-other --summary "bad" >"${T}/cap.err" 2>&1 || rc=$? +[[ "${rc}" -eq 3 ]] && ok "capture refuses stale work-id (rc=3)" || bad "capture mismatch rc=${rc}" +grep -q "does not match pointer" "${T}/cap.err" && ok "capture mismatch names the pointer" || bad "capture error: $(cat "${T}/cap.err")" +[[ "$(grep -c '"work_id"' "${H}/.sdlc/staged/lessons.jsonl")" -eq 2 ]] && ok "refused capture staged nothing" || bad "staged count changed on refused capture" + +# --------------------------------------------------------------------------- +echo "== Test 12: code capture / complete without verify receipt refuse ==" +harness_sdlc "${T}" resume "${W}" --phase code >/dev/null +rc=0 +harness_sdlc "${T}" capture --phase code --summary "T01 complete" >/dev/null 2>"${T}/receipt.err" || rc=$? +[[ "${rc}" -ne 0 ]] && ok "code capture without receipt refuses" || bad "code capture without receipt should refuse" +grep -q "verify receipt required" "${T}/receipt.err" && ok "capture names the receipt requirement" || bad "capture error: $(cat "${T}/receipt.err")" +rc=0 +# complete prints its refusal on stdout; capture both streams. +harness_sdlc "${T}" complete >"${T}/complete.err" 2>&1 || rc=$? +[[ "${rc}" -ne 0 ]] && ok "complete without receipt refuses" || bad "complete without receipt should refuse" +grep -q "verify receipt required" "${T}/complete.err" && ok "complete names the receipt requirement" || bad "complete error: $(cat "${T}/complete.err")" + +# --------------------------------------------------------------------------- +echo "== Test 13: infers next canvas operation from REASONS Canvas ==" +T="$(new_target)" +W="FEAT-007-ops" +H="$(harness_home "${T}")" +harness_seed_work "${T}" "${W}" +cp "${EXAMPLE_CANVAS}" "${H}/spdd/canvas/${W}.md" +harness_sdlc "${T}" resume "${W}" >/dev/null +harness_sdlc "${T}" sync --work-id "${W}" >/dev/null +[[ "$(state_val "${T}" "${W}" operation)" == "T03" ]] && ok "sync infers next operation T03" || bad "expected T03, got $(state_val "${T}" "${W}" operation)" +out="$(harness_sdlc "${T}" next)" +grep -q 'Next canvas operation: T03' <<< "${out}" && ok "next names T03" || bad "next missing T03" +json="$(harness_sdlc "${T}" status --json | tr -d '\n')" +grep -q '"operation": "T03"' <<< "${json}" && grep -q '"operation_title": "' <<< "${json}" && ok "json includes operation and title" || bad "json missing operation fields" + +# --------------------------------------------------------------------------- +echo "== Test 14: all-complete canvas has empty next operation ==" +W="FEAT-012b-final" +write_canvas "${T}" "${W}" "Ready For Coding" "Complete" +harness_sdlc "${T}" resume "${W}" --phase code >/dev/null +harness_sdlc "${T}" sync --work-id "${W}" >/dev/null +[[ -z "$(state_val "${T}" "${W}" operation)" ]] && ok "all-complete canvas has empty operation" || bad "expected empty operation, got '$(state_val "${T}" "${W}" operation)'" +out="$(harness_sdlc "${T}" next)" +! grep -q 'Next canvas operation' <<< "${out}" && ok "next omits operation when all complete" || bad "next should not name an operation: ${out}" +json="$(harness_sdlc "${T}" status --json | tr -d ' \n')" +grep -q '"operation":""' <<< "${json}" && ok "json operation empty when all complete" || bad "json operation: ${json}" + +# --------------------------------------------------------------------------- +echo "== Test 15: code phase with Needs Analysis redirects to architect ==" +W="FEAT-012c-readiness" +write_canvas "${T}" "${W}" "Needs Analysis" +rc=0 +harness_sdlc "${T}" resume "${W}" --phase code >/dev/null 2>"${T}/resume.err" || rc=$? +[[ "${rc}" -ne 0 ]] && ok "resume --phase code refused when Needs Analysis" || bad "resume --phase code should refuse" +grep -q "not Ready For Coding" "${T}/resume.err" && ok "resume error names readiness" || bad "resume error: $(cat "${T}/resume.err")" +harness_sdlc "${T}" resume "${W}" --phase code --force >/dev/null && ok "resume --phase code --force overrides" || bad "resume --force should succeed" +[[ "$(state_val "${T}" "${W}" phase)" == "code" ]] && ok "forced phase persisted as code" || bad "expected code after --force" +out="$(harness_sdlc "${T}" next)" +grep -q 'sdlc-spdd-architect' <<< "${out}" && grep -q 'Phase: architect' <<< "${out}" && ok "next re-syncs to architect when readiness blocks coding" || bad "next should recommend architect: ${out}" +json="$(harness_sdlc "${T}" status --json | tr -d ' \n')" +grep -q '"phase":"architect"' <<< "${json}" && grep -q 'sdlc-spdd-architect' <<< "${json}" && ok "json phase + recommended_command reflect gate" || bad "json: ${json}" + +# --------------------------------------------------------------------------- +echo "== Test 16: advance to code refused when readiness blocks; --force overrides ==" +W="FEAT-012d-advance" +write_canvas "${T}" "${W}" "Needs Clarification" +harness_sdlc "${T}" resume "${W}" --phase architect >/dev/null +rc=0 +harness_sdlc "${T}" advance >/dev/null 2>"${T}/adv.err" || rc=$? +[[ "${rc}" -ne 0 ]] && ok "advance architect->code refused when Needs Clarification" || bad "advance should fail" +grep -q "not Ready For Coding" "${T}/adv.err" && ok "advance error names readiness" || bad "advance error: $(cat "${T}/adv.err")" +grep -q "pass --force (a human decision" "${T}/adv.err" && ok "advance error explains --force is a human decision" || bad "advance error lacks --force hint" +[[ "$(state_val "${T}" "${W}" phase)" == "architect" ]] && ok "phase stays architect after refusal" || bad "expected architect" +! grep -q $'\tadvance\t' "$(history_file "${T}" "${W}")" && ok "refused advance leaves no history entry" || bad "history has advance despite refusal" +harness_sdlc "${T}" advance --force >/dev/null && ok "advance --force overrides readiness gate" || bad "advance --force should succeed" +[[ "$(state_val "${T}" "${W}" phase)" == "code" ]] && ok "force advance reaches code" || bad "expected code after --force" +grep -q $'\tadvance\tphase=code' "$(history_file "${T}" "${W}")" && ok "history records forced advance" || bad "history missing forced advance" + +# --------------------------------------------------------------------------- +echo "== Test 17: advance to code succeeds when Ready For Coding ==" +W="FEAT-012f-ok" +write_canvas "${T}" "${W}" "Ready For Coding" +harness_sdlc "${T}" resume "${W}" --phase architect >/dev/null +harness_sdlc "${T}" advance >/dev/null && ok "advance architect->code when Ready For Coding" || bad "advance should succeed when ready" +[[ "$(state_val "${T}" "${W}" phase)" == "code" ]] && ok "phase is code after ready advance" || bad "expected code" +out="$(harness_sdlc "${T}" next)" +grep -q 'sdlc-spdd-code' <<< "${out}" && grep -q 'operation T01' <<< "${out}" && ok "next recommends code operation T01" || bad "next should recommend code: ${out}" + +# --------------------------------------------------------------------------- +echo "== Test 18: advance --to code from plan refused when Blocked ==" +W="FEAT-012j-to" +write_canvas "${T}" "${W}" "Blocked" +harness_sdlc "${T}" resume "${W}" --phase plan --force >/dev/null +rc=0 +harness_sdlc "${T}" advance --to code >/dev/null 2>"${T}/adv-to.err" || rc=$? +[[ "${rc}" -ne 0 ]] && ok "advance --to code refuses when Blocked" || bad "advance --to code should refuse" +grep -q "readiness is blocked, not Ready For Coding" "${T}/adv-to.err" && ok "--to error names blocked readiness" || bad "error: $(cat "${T}/adv-to.err")" +[[ "$(state_val "${T}" "${W}" phase)" == "plan" ]] && ok "phase stays plan after refused --to code" || bad "expected plan" +rc=0 +harness_sdlc "${T}" advance --to nowhere >/dev/null 2>&1 || rc=$? +[[ "${rc}" -ne 0 ]] && ok "advance --to unknown phase fails" || bad "unknown phase should fail" + +# --------------------------------------------------------------------------- +echo "== Test 19: YAML frontmatter readiness is honored ==" +W="FEAT-012h-yaml" +harness_seed_work "${T}" "${W}" +cat > "${H}/spdd/canvas/${W}.md" <<EOF --- readiness: needs-redesign --- -# REASONS Canvas: FEAT-012h-yaml +# REASONS Canvas: ${W} ## 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}/agent-context/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/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 +- Work ID: ${W} -# --------------------------------------------------------------------------- -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}/scripts/lib" -cp "${REPO_ROOT}/scripts/lib/"*.sh "${T}/scripts/lib/" -cat > "${T}/spdd/canvas/${work_id}.md" <<'EOF' -# REASONS Canvas: FEAT-012i-start +## R - Requirements -## Metadata -- Work ID: FEAT-012i-start -- Readiness: Needs Analysis +- Harness requirement. ## 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/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}/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/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}/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 +- Files: src/first.py EOF -wf "${T}" resume "${work_id}" --phase architect >/dev/null -wf "${T}" sync "${work_id}" >/dev/null -gate="$(grep '^gate_architect_review=' "${T}/.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/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}/scripts/sdlc-spdd/lib" -cp "${REPO_ROOT}/scripts/sdlc.sh" "${T}/scripts/sdlc-spdd/sdlc.sh" -cp "${CAPTURE}" "${T}/scripts/sdlc-spdd/capture-session-memory.sh" -# capture-session-memory.sh sources scripts/sdlc-spdd/lib/*.sh (FEAT-001) -cp "${REPO_ROOT}/scripts/lib/"*.sh "${T}/scripts/sdlc-spdd/lib/" -chmod +x "${T}/scripts/sdlc-spdd/sdlc.sh" "${T}/scripts/sdlc-spdd/capture-session-memory.sh" -SDLC_ROOT="${T}" "${T}/scripts/sdlc-spdd/sdlc.sh" resume "${work_id}" >/dev/null -if SDLC_ROOT="${T}" "${T}/scripts/sdlc-spdd/sdlc.sh" capture --summary "ok" >/dev/null 2>&1; then - ok "capture succeeds when pointer matches" -else - bad "capture should succeed for active pointer" -fi -SDLC_ROOT="${T}" "${T}/scripts/sdlc-spdd/sdlc.sh" resume FEAT-999-other >/dev/null 2>&1 || true -if SDLC_ROOT="${T}" "${T}/scripts/sdlc-spdd/sdlc.sh" capture --work-id "${work_id}" --summary "bad" >/dev/null 2>&1; then - bad "capture should refuse mismatched work-id" -else - ok "capture refuses stale work-id" -fi - -# --------------------------------------------------------------------------- -echo "== 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}/scripts/sdlc-spdd" -cp "${REPO_ROOT}/scripts/sdlc.sh" "${T}/scripts/sdlc-spdd/sdlc.sh" -chmod +x "${T}/scripts/sdlc-spdd/sdlc.sh" -SDLC_USER="alice" SDLC_ROOT="${T}" wf "${T}" claim "${work_id}" >/dev/null -if SDLC_USER="bob" SDLC_ROOT="${T}" "${T}/scripts/sdlc-spdd/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}/scripts/sdlc-spdd/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}/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}/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}/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}/agent-context/hooks" -cat > "${T}/agent-context/hooks/notify.sh" <<EOF -#!/usr/bin/env bash -echo "\$*" >> "${hook_log}" -EOF -chmod +x "${T}/agent-context/hooks/notify.sh" -SDLC_TEAM_REGISTRY_HOOK="${T}/agent-context/hooks/notify.sh" \ - SDLC_USER="hooker" SDLC_ROOT="${T}" wf "${T}" claim "${work_id}" >/dev/null -if [[ -f "${hook_log}" ]] && grep -q 'FEAT-012-hook' "${hook_log}"; then - ok "registry hook invoked" -else - bad "registry hook not invoked" -fi - -# --------------------------------------------------------------------------- -echo "== Test 20: claim auto-reads jira Key from milestone requirement ==" -T="${WORK}/milestone-jira" -work_id="FEAT-013-jira" -setup_feature "${T}" "${work_id}" -mkdir -p "${T}/requirements/milestones" -cat > "${T}/requirements/milestones/${work_id}.md" <<'EOF' -# Requirement: FEAT-013-jira - -## Jira - -- Key: ORCH-42 -- Summary: test issue -EOF -SDLC_USER="dev2" SDLC_ROOT="${T}" wf "${T}" claim "${work_id}" >/dev/null -if 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/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}/requirements/milestones" -cat > "${T}/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/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}/requirements/milestones" -cat > "${T}/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/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/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}/scripts/sdlc-spdd/lib" -cp "${REPO_ROOT}/scripts/sdlc.sh" "${T}/scripts/sdlc-spdd/sdlc.sh" -cp "${CAPTURE}" "${T}/scripts/sdlc-spdd/capture-session-memory.sh" -cp "${REPO_ROOT}/scripts/lib/"*.sh "${T}/scripts/sdlc-spdd/lib/" -chmod +x "${T}/scripts/sdlc-spdd/sdlc.sh" "${T}/scripts/sdlc-spdd/capture-session-memory.sh" -SDLC_ENGINE=shell SDLC_ROOT="${T}" "${T}/scripts/sdlc-spdd/sdlc.sh" resume "${work_id}" --phase code >/dev/null -if SDLC_ENGINE=shell SDLC_ROOT="${T}" "${T}/scripts/sdlc-spdd/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}/scripts/sdlc-spdd/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 +harness_sdlc "${T}" resume "${W}" --phase architect >/dev/null +rc=0 +harness_sdlc "${T}" gate --phase code >/dev/null 2>"${T}/yaml.err" || rc=$? +[[ "${rc}" -eq 1 ]] && grep -q "readiness is needs-redesign" "${T}/yaml.err" && ok "gate reads YAML readiness" || bad "gate yaml: $(cat "${T}/yaml.err")" +brief="$(harness_sdlc "${T}" session brief)" +grep -q '| Readiness | needs-redesign |' <<< "${brief}" && ok "brief includes Readiness row from YAML" || bad "brief missing Readiness: ${brief}" +[[ "$(state_val "${T}" "${W}" gate_architect_review)" != "passed" ]] && ok "architect_review not auto-passed for needs-redesign" || bad "architect_review should not be passed" + +# --------------------------------------------------------------------------- +echo "== Test 20: team registry claim conflict and --force takeover ==" +T="$(new_target)" +W="FEAT-009-team" +harness_seed_work "${T}" "${W}" +SDLC_USER="alice" harness_sdlc "${T}" claim "${W}" >/dev/null +registry_matches "${T}" "${W}" '"status": "active".*"owner": "alice"' && ok "claim writes team registry" || bad "registry missing active row" +rc=0 +SDLC_USER="bob" harness_sdlc "${T}" claim "${W}" >/dev/null 2>"${T}/bob.err" || rc=$? +[[ "${rc}" -ne 0 ]] && ok "claim without --force refuses foreign owner" || bad "claim should refuse foreign owner" +grep -q "is active under alice" "${T}/bob.err" && ok "refusal names current owner" || bad "refusal text: $(cat "${T}/bob.err")" +registry_matches "${T}" "${W}" '"owner": "alice"' && ok "refused claim leaves alice as owner" || bad "owner changed despite refusal" +SDLC_USER="bob" harness_sdlc "${T}" claim "${W}" --force >/dev/null && ok "claim --force takes over" || bad "claim --force should succeed" +registry_matches "${T}" "${W}" '"status": "active".*"owner": "bob"' && ok "registry owner is bob after --force" || bad "owner not bob" +[[ "$(grep -c "\"work_id\": \"${W}\"" "$(registry_file "${T}")")" -eq 2 ]] && ok "registry is append-only (2 events)" || bad "registry event count wrong" +team_out="$(harness_sdlc "${T}" team)" +grep -q "${W}" <<< "${team_out}" && grep -q 'bob' <<< "${team_out}" && ok "team view shows bob's claim" || bad "team output: ${team_out}" +list_out="$(harness_sdlc "${T}" list-work)" +grep -q "${W}" <<< "${list_out}" && ok "list-work shows work id" || bad "list-work missing id" + +# --------------------------------------------------------------------------- +echo "== Test 21: sync-team marks canvas Complete as done ==" +harness_seed_work "${T}" CHORE-001-done Complete +harness_sdlc "${T}" sync-team >/dev/null +registry_matches "${T}" CHORE-001-done '"status": "done"' && ok "sync-team marks canvas complete as done" || bad "done status not written" + +# --------------------------------------------------------------------------- +echo "== Test 22: claim records branch / pr / jira note tokens ==" +W="FEAT-011-notes" +harness_seed_work "${T}" "${W}" +SDLC_USER="dev1" harness_sdlc "${T}" claim "${W}" --branch "cursor/feat-011" --pr "#99" >/dev/null +registry_matches "${T}" "${W}" 'branch:cursor/feat-011' && registry_matches "${T}" "${W}" 'pr:#99' \ + && ok "claim stores branch and pr note tokens" || bad "branch/pr tokens missing" +W="FEAT-013-jira" +harness_seed_work "${T}" "${W}" +printf '\n## Jira\n\n- Key: ORCH-42\n- Summary: test issue\n' >> "$(harness_home "${T}")/requirements/milestones/${W}.md" +SDLC_USER="dev2" harness_sdlc "${T}" claim "${W}" >/dev/null +registry_matches "${T}" "${W}" 'jira:ORCH-42' && ok "claim auto-reads jira key from milestone" || bad "milestone jira key not in registry" +[[ "$(harness_sdlc "${T}" session jira-status --work-id "${W}")" == "ORCH-42" ]] && ok "session jira-status reports key" || bad "jira-status wrong" +harness_sdlc "${T}" release >/dev/null +[[ -z "$(pointer_of "${T}")" ]] && ok "release clears pointer" || bad "release should clear pointer" + +# --------------------------------------------------------------------------- +echo "== Test 23: agent-driven Jira ask on missing / draft / present ==" +T="$(new_target)" +H="$(harness_home "${T}")" +W="FEAT-014-jira-missing" +mkdir -p "${H}/spdd/canvas" +printf '# %s\n\n## Final Status\n\n- Status: In Progress\n' "${W}" > "${H}/spdd/canvas/${W}.md" +[[ "$(harness_sdlc "${T}" session jira-status --work-id "${W}")" == "missing" ]] && ok "jira-status missing without requirement" || bad "expected missing" +harness_sdlc "${T}" session jira-ask --work-id "${W}" | grep -q 'Ask the user for the issue key' && ok "jira-ask asks when missing" || bad "jira-ask should ask when missing" +"${START}" --target "${T}" --work-id "${W}" --phase plan >/dev/null +current="${H}/.sdlc/sessions/current-session.md" +grep -q '^- Jira: missing$' "${current}" \ + && grep -A20 '## Resume Prompt' "${current}" | grep -q 'Jira key is missing' \ + && ok "session brief Resume Prompt asks when Jira missing" || bad "brief should ask when Jira missing" + +W="FEAT-015-jira-draft" +harness_seed_work "${T}" "${W}" +printf '\n## Jira\n\n- Summary: draft without key yet\n' >> "${H}/requirements/milestones/${W}.md" +[[ "$(harness_sdlc "${T}" session jira-status --work-id "${W}")" == "draft" ]] && ok "jira-status draft without Key" || bad "expected draft" +harness_sdlc "${T}" session jira-ask --work-id "${W}" | grep -q 'Jira draft exists' && ok "jira-ask asks when draft" || bad "jira-ask should ask when draft" +"${START}" --target "${T}" --work-id "${W}" --phase plan >/dev/null +grep -A20 '## Resume Prompt' "${current}" | grep -q 'Jira draft exists' && ok "session brief asks when Jira draft" || bad "brief should ask when draft" + +W="FEAT-016-jira-present" +harness_seed_work "${T}" "${W}" +printf '\n## Jira\n\n- Key: ORCH-99\n' >> "${H}/requirements/milestones/${W}.md" +[[ "$(harness_sdlc "${T}" session jira-status --work-id "${W}")" == "ORCH-99" ]] && ok "jira-status returns key when present" || bad "expected ORCH-99" +[[ -z "$(harness_sdlc "${T}" session jira-ask --work-id "${W}")" ]] && ok "jira-ask is silent when key present" || bad "jira-ask should be empty" +"${START}" --target "${T}" --work-id "${W}" --phase plan >/dev/null +grep -q '^- Jira: ORCH-99$' "${current}" \ + && ! grep -A20 '## Resume Prompt' "${current}" | grep -q 'Ask the user for the issue key' \ + && ok "session brief records key without ask" || bad "brief should record key without ask" + +W="FEAT-017-jira-off" +harness_seed_work "${T}" "${W}" +[[ -z "$(SDLC_SESSION_ASK_JIRA=0 harness_sdlc "${T}" session jira-ask --work-id "${W}")" ]] && ok "SDLC_SESSION_ASK_JIRA=0 suppresses ask" || bad "SDLC_SESSION_ASK_JIRA=0 should suppress" +SDLC_SESSION_ASK_JIRA=0 "${START}" --target "${T}" --work-id "${W}" --phase plan >/dev/null +! grep -A20 '## Resume Prompt' "${current}" | grep -q 'Ask the user for the issue key' \ + && ok "start respects SDLC_SESSION_ASK_JIRA=0" || bad "start should honor SDLC_SESSION_ASK_JIRA=0" + +# --------------------------------------------------------------------------- +echo "== Test 24: installed dispatcher has no bash twins and rejects removed env ==" +[[ ! -e "${H}/scripts/sdlc-workflow.sh" && ! -e "${H}/scripts/sdlc-team-registry.sh" && ! -e "${H}/scripts/sdlc-pointer.sh" ]] \ + && ok "no bash workflow twins installed" || bad "retired twin installed" +rc=0 +SDLC_GATE_ENGINE=shell "${H}/scripts/sdlc.sh" version >/dev/null 2>"${T}/env.err" || rc=$? +[[ "${rc}" -eq 2 ]] && grep -q "removed" "${T}/env.err" && ok "SDLC_GATE_ENGINE=shell exits 2 (removed)" || bad "SDLC_GATE_ENGINE rc=${rc}: $(cat "${T}/env.err")" + +harness_finish diff --git a/tests/test-sunset-command.sh b/tests/test-sunset-command.sh index 98b16610..1e94c8f0 100755 --- a/tests/test-sunset-command.sh +++ b/tests/test-sunset-command.sh @@ -49,7 +49,12 @@ 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" +# sdlc.sh is a thin dispatcher (no per-verb case arms): prove routing behaviorally. +if help_out="$("${SDLC_SH}" sunset --help 2>&1)" && grep -Fq 'sunset' <<< "${help_out}"; then + ok "sdlc.sh routes sunset to Python engine (--help exits 0)" +else + bad "sdlc.sh sunset --help failed or did not mention sunset: ${help_out}" +fi echo "== Test 3: generator --check and adapter validation ==" if "${REPO_ROOT}/scripts/generate-command-adapters.sh" --check >/dev/null; then @@ -72,8 +77,10 @@ git -C "${smoke_root}" config user.name "CI" printf '# smoke\n' > "${smoke_root}/README.md" git -C "${smoke_root}" add README.md git -C "${smoke_root}" commit -q -m "init" -mkdir -p "${smoke_root}/requirements/milestones" "${smoke_root}/spdd/canvas" "${smoke_root}/spdd/memory" -cat > "${smoke_root}/requirements/milestones/FEAT-014-feature-sunset.md" <<'EOF' +# Storage v3: framework files live under <root>/sdlc-spdd (no root-layout fallback). +smoke_home="${smoke_root}/sdlc-spdd" +mkdir -p "${smoke_home}/requirements/milestones" "${smoke_home}/spdd/canvas" "${smoke_home}/spdd/memory" +cat > "${smoke_home}/requirements/milestones/FEAT-014-feature-sunset.md" <<'EOF' # Requirement: FEAT-014-feature-sunset ## Summary @@ -86,7 +93,7 @@ Smoke sunset. - Summary: Smoke sunset EOF printf '%s\n' '# REASONS Canvas: FEAT-014-feature-sunset' '' '## Final Status' '' '- Status: Complete' \ - > "${smoke_root}/spdd/canvas/FEAT-014-feature-sunset.md" + > "${smoke_home}/spdd/canvas/FEAT-014-feature-sunset.md" if PYTHONPATH="${REPO_ROOT}/engine/src${PYTHONPATH:+:${PYTHONPATH}}" \ python3 -m sdlc_engine --root "${smoke_root}" sunset --work-id FEAT-014-feature-sunset --apply \ >/tmp/sdlc-sunset-smoke.out 2>/tmp/sdlc-sunset-smoke.err @@ -99,8 +106,8 @@ then else bad "engine output missing sunset snapshot" fi - if [[ -f "${smoke_root}/.sdlc/staged/lessons.jsonl" ]] \ - && grep -Fq '"source": "sunset"' "${smoke_root}/.sdlc/staged/lessons.jsonl" + if [[ -f "${smoke_home}/.sdlc/staged/lessons.jsonl" ]] \ + && grep -Fq '"source": "sunset"' "${smoke_home}/.sdlc/staged/lessons.jsonl" then ok "sunset --apply staged a session record" else diff --git a/tests/test-verify-receipt.sh b/tests/test-verify-receipt.sh index 87e8a710..d984d0aa 100755 --- a/tests/test-verify-receipt.sh +++ b/tests/test-verify-receipt.sh @@ -3,58 +3,46 @@ 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 +# +# Runs the Python engine through the installed sdlc-spdd/scripts/sdlc.sh on a +# fresh init-project target per case. The former SDLC_ENGINE=shell / +# SDLC_GATE_ENGINE=shell exports are gone: the bash twin was deleted and +# setting those variables now makes sdlc.sh exit 2. 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" +# shellcheck source=lib/harness.sh +source "${SCRIPT_DIR}/lib/harness.sh" -WORK="$(mktemp -d)" -trap 'rm -rf "${WORK}"' EXIT +TARGETS=() +cleanup() { + local t + for t in "${TARGETS[@]}"; do rm -rf "${t}"; done + return 0 +} +trap cleanup EXIT -pass=0 -fail=0 -ok() { echo " ok $1"; pass=$((pass + 1)); } -bad() { echo " FAIL $1" >&2; fail=$((fail + 1)); } +# Sets T (installed target). Not a subshell, so the target is tracked for cleanup. +new_target() { + T="$(harness_new_target)" + TARGETS+=("${T}") +} -setup_feature() { - local t="$1" - mkdir -p "${t}/.sdlc/sessions" \ - "${t}/agent-context" \ - "${t}/spdd/canvas" \ - "${t}/spdd/analysis" \ - "${t}/spdd/memory" \ - "${t}/scripts/sdlc-spdd/lib" - cp "${POINTER}" "${t}/agent-context/sdlc-pointer.sh" - cp "${WORKFLOW}" "${t}/agent-context/sdlc-workflow.sh" - cp "${TEAM_REG}" "${t}/agent-context/sdlc-team-registry.sh" - : > "${t}/spdd/memory/registry.jsonl" - cp "${SDLC_SH}" "${t}/scripts/sdlc-spdd/sdlc.sh" - cp "${CAPTURE}" "${t}/scripts/sdlc-spdd/capture-session-memory.sh" - cp "${REPO_ROOT}/scripts/lib/"*.sh "${t}/scripts/sdlc-spdd/lib/" - chmod +x "${t}/agent-context/"*.sh \ - "${t}/scripts/sdlc-spdd/sdlc.sh" \ - "${t}/scripts/sdlc-spdd/capture-session-memory.sh" +staged_file() { + printf '%s' "$(harness_home "$1")/.sdlc/staged/lessons.jsonl" } -sdlc() { - local t="$1" - shift - SDLC_ROOT="${t}" SDLC_ENGINE=shell "${t}/scripts/sdlc-spdd/sdlc.sh" "$@" +# Park a Work ID at the code phase. The engine's `resume --phase` enforces the +# entrance gate of that phase, so fixtures use --force to set up the state the +# capture/complete verbs then operate on. +park_code() { + harness_sdlc "$1" resume "$2" --phase code --force >/dev/null } echo "== test_capture_without_verify_receipt_refuses ==" -T="${WORK}/capture-refuse" +new_target work_id="FEAT-020-i1-receipt" -setup_feature "${T}" -sdlc "${T}" resume "${work_id}" --phase code >/dev/null -if out="$(sdlc "${T}" capture --phase code --summary "T01 complete" --validation "looked fine" 2>&1)"; then +park_code "${T}" "${work_id}" +if out="$(harness_sdlc "${T}" capture --phase code --summary "T01 complete" --validation "looked fine" 2>&1)"; then bad "capture without receipt should refuse: ${out}" else if grep -q 'verify receipt required' <<< "${out}"; then @@ -63,18 +51,17 @@ else bad "capture refuse message missing receipt: ${out}" fi fi -if [[ -f "${T}/.sdlc/staged/lessons.jsonl" ]]; then +if [[ -f "$(staged_file "${T}")" ]]; then bad "refused capture must not stage a lesson" else ok "refused capture left no staged lesson" fi echo "== test_complete_without_verify_receipt_refuses ==" -T="${WORK}/complete-refuse" +new_target work_id="FEAT-021-i1-complete" -setup_feature "${T}" -sdlc "${T}" resume "${work_id}" --phase code >/dev/null -if out="$(sdlc "${T}" complete --summary "T01 complete" 2>&1)"; then +park_code "${T}" "${work_id}" +if out="$(harness_sdlc "${T}" complete --summary "T01 complete" 2>&1)"; then bad "complete without receipt should refuse: ${out}" else if grep -q 'verify receipt required' <<< "${out}"; then @@ -85,11 +72,10 @@ else fi echo "== test_complete_fail_receipt_refuses ==" -T="${WORK}/complete-fail" +new_target work_id="FEAT-022-i1-fail" -setup_feature "${T}" -sdlc "${T}" resume "${work_id}" --phase code >/dev/null -if out="$(sdlc "${T}" complete --summary "T01 complete" \ +park_code "${T}" "${work_id}" +if out="$(harness_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}" else @@ -99,13 +85,17 @@ else bad "complete fail-receipt message: ${out}" fi fi +if [[ -f "$(staged_file "${T}")" ]]; then + bad "refused complete must not stage a lesson" +else + ok "refused complete left no staged lesson" +fi echo "== test_capture_with_receipt_stages_verify_object ==" -T="${WORK}/capture-ok" +new_target work_id="FEAT-023-i1-ok" -setup_feature "${T}" -sdlc "${T}" resume "${work_id}" --phase code >/dev/null -if sdlc "${T}" capture --phase code --summary "T01 complete" \ +park_code "${T}" "${work_id}" +if harness_sdlc "${T}" capture --phase code --summary "T01 complete" \ --verify-command "pytest tests/test_foo.py" \ --verify-exit 0 \ --verify-result pass >/dev/null; then @@ -113,7 +103,7 @@ if sdlc "${T}" capture --phase code --summary "T01 complete" \ else bad "capture with receipt should succeed" fi -stage="${T}/.sdlc/staged/lessons.jsonl" +stage="$(staged_file "${T}")" if [[ -f "${stage}" ]] \ && grep -q '"command": "pytest tests/test_foo.py"' "${stage}" \ && grep -q '"exit": 0' "${stage}" \ @@ -125,11 +115,10 @@ else fi echo "== test_complete_with_pass_receipt_succeeds ==" -T="${WORK}/complete-ok" +new_target work_id="FEAT-024-i1-done" -setup_feature "${T}" -sdlc "${T}" resume "${work_id}" --phase code >/dev/null -if sdlc "${T}" complete --summary "T01 complete" \ +park_code "${T}" "${work_id}" +if harness_sdlc "${T}" complete --summary "T01 complete" \ --verify-command "true" \ --verify-exit 0 \ --verify-result pass >/dev/null; then @@ -137,9 +126,13 @@ if sdlc "${T}" complete --summary "T01 complete" \ else bad "complete with pass receipt should succeed" fi - -echo -echo "Results: ${pass} passed, ${fail} failed" -if [[ "${fail}" -gt 0 ]]; then - exit 1 +stage="$(staged_file "${T}")" +if [[ -f "${stage}" ]] \ + && grep -q "\"work_id\": \"${work_id}\"" "${stage}" \ + && grep -q '"result": "pass"' "${stage}"; then + ok "complete staged a pass receipt for ${work_id}" +else + bad "complete did not stage a pass receipt: $(cat "${stage}" 2>/dev/null || true)" fi + +harness_finish From 1432edb7fc5206a25e17dd380c5f30e8a65e90db Mon Sep 17 00:00:00 2001 From: Cursor Agent <cursoragent@cursor.com> Date: Mon, 14 Sep 2026 00:48:51 +0000 Subject: [PATCH 19/22] docs: align all documentation with the single Python engine and v3 layout - Remove SDLC_ENGINE/SDLC_GATE_ENGINE, bash twin, storage migrate, work-registry.tsv and consolidation references from README, TESTING, CONTRIBUTING, docs/, engine/README, templates, specs and research protocol - TESTING.md: describe test-upgrade-layout and test-workflow-harnesses CI, v3-only upgrade contract, engine-backed pointer/workflow harnesses - Diagrams 02/03/06 regenerated without storage_migrate / accept-lessons.sh - Regenerate dogfood adapter packs and sdlc-spdd/docs copies Co-authored-by: John Menke <jmjava@gmail.com> --- .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 +- CONTRIBUTING.md | 2 +- README.md | 8 +- TESTING.md | 67 +++++----- docs/README.md | 2 +- docs/adf-viewer.md | 4 +- docs/agent-session-scripts.md | 4 +- docs/diagrams/02-container.puml | 4 +- docs/diagrams/02-container.svg | 2 +- docs/diagrams/03-component-engine.puml | 6 +- docs/diagrams/03-component-engine.svg | 2 +- docs/diagrams/06-stage-then-accept.puml | 2 +- docs/diagrams/06-stage-then-accept.svg | 2 +- docs/engine-v2.md | 116 +++++++++++------- docs/framework-upgrade.md | 25 ++-- docs/installing-into-your-project.md | 7 +- docs/local-sqlite-index.md | 2 +- docs/maintaining-your-project.md | 4 +- docs/mcp-guide-for-agents.md | 4 +- docs/research/README.md | 2 +- docs/storage-v3.md | 30 +++-- docs/useful-concepts-and-commands.md | 2 +- engine/README.md | 16 ++- sdlc-spdd/docs/adf-viewer.md | 4 +- sdlc-spdd/docs/agent-session-scripts.md | 4 +- sdlc-spdd/docs/framework-upgrade.md | 25 ++-- .../docs/installing-into-your-project.md | 7 +- sdlc-spdd/docs/maintaining-your-project.md | 4 +- .../docs/research/evaluation-protocol.md | 10 +- .../threats-to-validity-and-replication.md | 6 +- sdlc-spdd/docs/storage-v3.md | 30 +++-- .../docs/useful-concepts-and-commands.md | 2 +- sdlc-spdd/harness/guide-dice.md | 2 +- .../commands/lifecycle-commit-message.spec.md | 2 +- spec/commands/lifecycle-sunset.spec.md | 2 +- templates/agent-context/README.md | 2 +- 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 +-- 49 files changed, 225 insertions(+), 228 deletions(-) diff --git a/.claude/commands/sdlc-spdd-commit-message.md b/.claude/commands/sdlc-spdd-commit-message.md index 3ac4b607..2e713bbf 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. 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..04b345e3 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 <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 <WORK-ID> --apply`. 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..2e713bbf 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. 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..808a46f3 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 <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 <WORK-ID> --apply`. 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..48cda240 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. 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..3d61d225 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 <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 <WORK-ID> --apply`. 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/CONTRIBUTING.md b/CONTRIBUTING.md index 06af79cc..1cc5e5a2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -142,7 +142,7 @@ Before merging doc or script changes that touch the three-part model (Planning, - [ ] **Workflow CLI** — daily runbook, first-day walkthrough, cheat sheet, and session prompt standard teach `sdlc.sh` (`next`, `claim`, `capture`) not raw scripts alone - [ ] **Workflow chat commands** — `/sdlc-claim`, `/sdlc-shelf`, `/sdlc-advance`, `/sdlc-next`, `/sdlc-team` appear alongside shell equivalents where users start/claim/orient -- [ ] **Pointer + team registry** — docs explain `.sdlc/pointer` / `.sdlc/workflows/` (local) vs `work-registry.tsv` (committed); claim/release commit reminder present +- [ ] **Pointer + team registry** — docs explain `.sdlc/pointer` / `.sdlc/workflows/` (local) vs `spdd/memory/registry.jsonl` (committed); claim/release commit reminder present - [ ] **Whereami / next** — `/sdlc-next` and `/sdlc-spdd-whereami` in assistant orientation tables (README, cheat sheet, cursor/copilot/claude usage) - [ ] **Jira draft** — milestone `## Jira` convention referenced from runbook, cheat sheet, planning prompts where Jira drafts appear - [ ] **Three-part mandate** — no Planning, SPDD, or SDLC artifacts removed or conflated; see [design mandate](docs/three-part-operating-path.md#three-part-design-mandate) diff --git a/README.md b/README.md index 23e4562e..b5e47cdc 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 ``` @@ -303,9 +303,9 @@ your-app/ adf/ Optional checked-in ADF JSON ``` -Legacy sprawled installs (framework folders at the repo root, `agent-context/` -trees, `work-registry.tsv`) keep working read-only and are consolidated by -`upgrade` + `sdlc-engine storage migrate`. +This is the only supported layout. Pre-v3 installs (framework folders at the +repo root, `agent-context/` trees, `work-registry.tsv`) are not read or +migrated; `upgrade-project.sh` refuses them and asks for a fresh `init`. | Path (this repo) | Purpose | | ---------------- | ------- | diff --git a/TESTING.md b/TESTING.md index 5d36fcc2..1c6dab17 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:** the Python `sdlc_engine` package (`WorkflowEngine.gate_check` +for gates). It is the only engine — `scripts/sdlc.sh` is a thin dispatcher to +`python -m sdlc_engine` and there is no shell fallback. See +[engine-sut.md](sdlc-spdd/docs/research/engine-sut.md). | 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. +Bash workflow harnesses (separate from engine pytest): `./tests/test-sdlc-workflow.sh`, `./tests/test-sdlc-pointer.sh`, `./tests/test-archive-work.sh` — run when touching `scripts/sdlc.sh`, `engine/src/sdlc_engine/commands/`, or workflow gates. All share `tests/lib/harness.sh` and drive the Python engine through the dispatcher. **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.) @@ -128,10 +128,9 @@ In orchestrator repo: - `validate-command-adapters` (`.github/workflows/validate-command-adapters.yml`) - `test-adapter-install` (`.github/workflows/test-adapter-install.yml`) -- `test-upgrade-consolidate` (`.github/workflows/test-upgrade-consolidate.yml`) — storage v3 layout upgrade -- `test-sdlc-pointer` (`.github/workflows/test-sdlc-pointer.yml`) -- `test-sdlc-workflow` (`.github/workflows/test-sdlc-workflow.yml`) -- `test-archive-work` (`.github/workflows/test-archive-work.yml`) +- `test-upgrade-layout` (`.github/workflows/test-upgrade-layout.yml`) — v3-only upgrade contract (refuses pre-v3 trees, idempotent on v3) +- `test-workflow-harnesses` (`.github/workflows/test-workflow-harnesses.yml`) — every engine-backed bash harness: pointer, workflow, shim, receipt, gate, quality-gates, canvas readiness, archive +- `shellcheck` (`.github/workflows/shellcheck.yml`) - `test-scripts-lib` (`.github/workflows/test-scripts-lib.yml`) - `validate-command-spec-generation` (`.github/workflows/validate-command-spec-generation.yml`) - `test-integration-merge` (`.github/workflows/test-integration-merge.yml`) @@ -199,45 +198,36 @@ 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. -### Storage v3 upgrade consolidation harness +### Upgrade layout harness (v3-only) -`./tests/test-framework-install-consolidate.sh` unit-tests consolidate/archive -helpers in `scripts/lib/framework-install.sh` (move/merge/dest-wins, dry-run, -archive leftovers, orchestrator-vs-target agent-context handling, harness seed). +`./tests/test-upgrade-layout.sh` is the upgrade contract suite: -`./tests/test-upgrade-consolidate.sh` is the end-to-end layout suite: +- Pre-v3 trees (`agent-context/`, root `spdd/`, `work-registry.tsv`, …) make + `upgrade-project.sh` **refuse** with a re-init message; nothing is moved or + archived +- Fresh v3 init/setup then upgrade preserves project content +- Second upgrade is idempotent; `--dry-run` leaves the tree untouched +- Retired bash twins are never installed -- **A.** Pure legacy sprawl (no `sdlc-spdd/` yet) → single home; root stay-set gone -- **B.** Dual layout merge when home already exists (destination wins conflicts) -- **C.** Idempotent second upgrade -- **D.** `--dry-run` + `--consolidate` no-op leave the tree untouched -- **E.** Orchestrator-shaped target archives `agent-context/`; keeps root `scripts/` -- **F.** Fresh v3 init/setup then upgrade preserves project content -- **G.** Nested helper unit suite - -Leftover `agent-context/` trees must land under -`sdlc-spdd/.sdlc/legacy-layout-archive/` (install source is -`templates/agent-context/`). `verify-project-install.sh` must pass after each -real upgrade. CI: `.github/workflows/test-upgrade-consolidate.yml`. +`verify-project-install.sh` must pass after each real upgrade. +CI: `.github/workflows/test-upgrade-layout.yml`. ### SDLC pointer harness -`./tests/test-sdlc-pointer.sh` exercises `templates/agent-context/sdlc-pointer.sh`: +`./tests/test-sdlc-pointer.sh` exercises `sdlc.sh pointer` (Python `sdlc_engine.pointer`): - CLI round-trip (`set`/`get`/`reset`) -- Guarded run (`run_against_pointer`) refusal on mismatch +- Guarded `capture` refusal on pointer mismatch - `SDLC_POINTER_OVERRIDE` bootstrap - Integration with `start-agent-session.sh` pointer auto-set -- Install path copies the script to target projects ### SDLC workflow + team registry harness -`./tests/test-sdlc-workflow.sh` exercises `templates/agent-context/sdlc-workflow.sh` and team registry: +`./tests/test-sdlc-workflow.sh` exercises the engine workflow verbs through `sdlc.sh`: - 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 `work-registry.tsv` +- Guarded `capture` (pointer must match; refusal goes to stderr) +- Team `claim`/`release`, stale TTL, branch/PR/Jira notes in `spdd/memory/registry.jsonl` - Jira Key auto-link from `requirements/milestones/<WORK-ID>.md` on claim ### Archive completed/cancelled work harness @@ -287,7 +277,6 @@ throwaway targets and asserts: - `--work-id` loads canvas, analysis, tasks, and ledger progress excerpts (storage v3) - `--format json` returns paths - `start-agent-session.sh` embeds Resolved Context and avoids redundant resume prompts -- legacy `playbooks/` + `extensions/` trees migrate idempotently into `harness/skills/` Run locally after changing `resolve-agent-context.sh`, `scripts/lib/skills.sh`, harness templates, or `start-agent-session.sh`. @@ -321,7 +310,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 ``` @@ -376,10 +365,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 <WORK-ID> --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 <WORK-ID> --system github ./scripts/sdlc.sh local start --name scratch --intent "offline explore" ./scripts/sdlc.sh local promote --type feature --name "Documented title" --dry-run diff --git a/docs/README.md b/docs/README.md index 8169aa42..27a3139b 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) — the single engine behind `scripts/sdlc.sh` (thin dispatcher to `python -m sdlc_engine`). **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..9521d41d 100644 --- a/docs/adf-viewer.md +++ b/docs/adf-viewer.md @@ -31,7 +31,7 @@ Flask is an optional extra so the core engine stays stdlib-only. # Preferred wrappers ./scripts/sdlc.sh viewer --port 5050 # or -SDLC_ENGINE=python ./scripts/sdlc.sh viewer --port 5050 +./scripts/sdlc.sh viewer --port 5050 # Direct python3 -m sdlc_engine.viewer --root . --port 5050 @@ -67,7 +67,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/agent-session-scripts.md b/docs/agent-session-scripts.md index c47d1747..a26acc42 100644 --- a/docs/agent-session-scripts.md +++ b/docs/agent-session-scripts.md @@ -20,7 +20,7 @@ They solve four operational needs: | `sdlc-spdd/scripts/start-agent-session.sh` | Target-local script that creates a session brief for a new agent | | `sdlc-spdd/scripts/resync-agent-session.sh` | Target-local script that checks or reconciles the canonical canvas, validates it, and creates a session brief | | `sdlc-spdd/scripts/capture-session-memory.sh` | Stage session summary, decisions/pitfalls/patterns, and optional metrics as lesson records in `.sdlc/staged/lessons.jsonl` | -| `sdlc-spdd/scripts/accept-lessons.sh` | Promote staged lesson records into the committed `spdd/memory/lessons.jsonl` (also `sdlc.sh accept`) | +| `sdlc-spdd/scripts/sdlc.sh accept` | Promote staged lesson records into the committed `spdd/memory/lessons.jsonl` | | `sdlc-spdd/scripts/index-spdd-analysis.sh` | Stage an `analysis` lesson record from a Fowler analysis artifact | | `sdlc-spdd/scripts/resolve-agent-context.sh` | Resolve SDLC Agents `#SkillName` / phase extensions for progressive loading | | `sdlc-spdd/scripts/resolve-context-backend.sh` | Runtime probe: is the Guide DICE backend enabled and reachable? | @@ -75,7 +75,7 @@ Preview first: ./scripts/upgrade-project.sh --target /path/to/app --all --dry-run -The upgrade updates framework-owned prompts, harness/skills files, target-local docs under `docs/sdlc-spdd/`, and target-local runtime scripts. It preserves application source, application docs outside `docs/sdlc-spdd/`, requirements, canvases, reviews, sync logs, the lessons ledger, existing root `CLAUDE.md`, and target workflow customizations. Legacy memory layouts are converted by `sdlc-engine storage migrate` — see [Framework upgrade](framework-upgrade.md). +The upgrade updates framework-owned prompts, harness/skills files, target-local docs under `docs/sdlc-spdd/`, and target-local runtime scripts. It preserves application source, application docs outside `docs/sdlc-spdd/`, requirements, canvases, reviews, sync logs, the lessons ledger, existing root `CLAUDE.md`, and target workflow customizations. Pre-v3 layouts are refused (re-initialize instead) — see [Framework upgrade](framework-upgrade.md). ## 2. Start a New Agent Session diff --git a/docs/diagrams/02-container.puml b/docs/diagrams/02-container.puml index 9725aaff..d92a6c94 100644 --- a/docs/diagrams/02-container.puml +++ b/docs/diagrams/02-container.puml @@ -11,8 +11,8 @@ System_Boundary(repo, "Project repository") { System_Boundary(home, "sdlc-spdd/ (framework home)") { Container(contracts, "Contracts (committed)", "Markdown", "requirements/ + ROADMAP.md, spdd/canvas|analysis|reviews|sync, harness/ playbooks/ extensions/") ContainerDb(ledger, "Lessons ledger + registry (committed)", "JSONL", "spdd/memory/lessons.jsonl - accepted records (decision, pitfall, pattern, session, analysis). spdd/memory/registry.jsonl - claim/release events") - Container(scripts, "Workflow CLI", "Bash", "scripts/sdlc.sh: capture, accept, claim, release, db; start-agent-session digest; paths.sh home resolution") - Container(engine, "sdlc-engine", "Python", "context retrieve/show/digest/accept/parity, storage migrate, sqlite rebuild, guide projection client") + Container(scripts, "Workflow CLI", "Bash", "scripts/sdlc.sh: thin dispatcher to python -m sdlc_engine; start-agent-session / capture-session-memory call sdlc-engine session") + Container(engine, "sdlc-engine", "Python", "context retrieve/show/digest/accept/parity, workflow/registry/pointer, sqlite rebuild, guide projection client") ContainerDb(runtime, ".sdlc/ runtime (gitignored)", "Files", "sessions/current-session.md hot brief, staged/lessons.jsonl captures, index.sqlite opt-in cache (schema v5)") } } diff --git a/docs/diagrams/02-container.svg b/docs/diagrams/02-container.svg index ac2ab3b2..90175701 100644 --- a/docs/diagrams/02-container.svg +++ b/docs/diagrams/02-container.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" contentStyleType="text/css" data-diagram-type="DESCRIPTION" height="1424px" preserveAspectRatio="none" style="width:941px;height:1424px;background:#FFFFFF;" version="1.1" viewBox="0 0 941 1424" width="941px" zoomAndPan="magnify"><?plantuml 1.2026.6?><defs/><g><g class="title" data-source-line="3"><text fill="#000000" font-family="sans-serif" font-size="14" font-weight="700" lengthAdjust="spacing" textLength="567.1982" x="183.855" y="22.9951">Containers - SDLC-SPDD Orchestrator installed in a project (storage v3)</text></g><!--cluster repo--><g class="cluster" data-qualified-name="repo" data-source-line="1690" id="ent0002"><rect fill="none" height="1138" style="stroke:#444444;stroke-width:1;stroke-dasharray:7,7;" width="603" x="7" y="272.2969"/><text fill="#444444" font-family="sans-serif" font-size="16" font-weight="700" lengthAdjust="spacing" textLength="162.8047" x="227.0977" y="309.1484">Project repository</text><text fill="#444444" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="59.4551" x="278.7725" y="324.0605">[system]</text></g><!--cluster home--><g class="cluster" data-qualified-name="repo.home" data-source-line="1690" id="ent0004"><rect fill="none" height="820" style="stroke:#444444;stroke-width:1;stroke-dasharray:7,7;" width="555" x="31" y="566.2969"/><text fill="#444444" font-family="sans-serif" font-size="16" font-weight="700" lengthAdjust="spacing" textLength="265.4609" x="175.7695" y="603.1484">sdlc-spdd/ (framework home)</text><text fill="#444444" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="59.4551" x="278.7725" y="618.0605">[system]</text></g><!--entity adapters--><g class="entity" data-qualified-name="repo.adapters" data-source-line="76" id="ent0003"><rect fill="#438DD5" height="148.0469" style="stroke:#3C7FC0;stroke-width:0.5;" width="209.7861" x="106" y="343.2969"/><text fill="#FFFFFF" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="71.543" x="175.1216" y="364.4355">«container»</text><text fill="#FFFFFF" font-family="sans-serif" font-size="16" font-weight="700" lengthAdjust="spacing" textLength="83.9219" x="125.9399" y="382.1172">Assistant</text><text fill="#FFFFFF" font-family="sans-serif" font-size="16" font-weight="700" lengthAdjust="spacing" textLength="5.5703" x="209.8618" y="382.1172"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="16" font-weight="700" lengthAdjust="spacing" textLength="80.4141" x="215.4321" y="382.1172">adapters</text><text fill="#FFFFFF" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="66.6504" x="119.5542" y="397.0293">[Markdown</text><text fill="#FFFFFF" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="3.8145" x="186.2046" y="397.0293"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="32.4316" x="190.019" y="397.0293">stubs</text><text fill="#FFFFFF" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="3.8145" x="222.4507" y="397.0293"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="12.0586" x="226.2651" y="397.0293">at</text><text fill="#FFFFFF" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="3.8145" x="238.3237" y="397.0293"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="27.2754" x="242.1382" y="397.0293">repo</text><text fill="#FFFFFF" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="3.8145" x="269.4136" y="397.0293"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="29.0039" x="273.228" y="397.0293">root]</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="208.668" y="412.8545"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="130.2793" x="116" y="429.1514">.cursor/commands</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="246.2793" y="429.1514"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="11.7305" x="250.7295" y="429.1514">+</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="262.46" y="429.1514"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="38.876" x="266.9102" y="429.1514">rules,</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="49.0615" x="126.1206" y="445.4482">Copilot</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="175.1821" y="445.4482"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="62.9658" x="179.6323" y="445.4482">prompts,</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="242.5981" y="445.4482"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="48.6172" x="247.0483" y="445.4482">Claude</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="81.6211" x="121.855" y="461.7451">commands.</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="203.4761" y="461.7451"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="30.1875" x="207.9263" y="461.7451">Thin</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="238.1138" y="461.7451"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="57.3672" x="242.564" y="461.7451">pointers</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="26.8174" x="159.5142" y="478.042">into</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="186.3315" y="478.042"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="71.4902" x="190.7817" y="478.042">sdlc-spdd/</text></g><!--entity contracts--><g class="entity" data-qualified-name="repo.home.contracts" data-source-line="76" id="ent0005"><rect fill="#438DD5" height="182.9688" style="stroke:#3C7FC0;stroke-width:0.5;" width="269.3135" x="300.5" y="637.2969"/><text fill="#FFFFFF" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="71.543" x="399.3853" y="658.4355">«container»</text><text fill="#FFFFFF" font-family="sans-serif" font-size="16" font-weight="700" lengthAdjust="spacing" textLength="87.1172" x="391.5981" y="676.1172">Contracts</text><text fill="#FFFFFF" font-family="sans-serif" font-size="16" font-weight="700" lengthAdjust="spacing" textLength="111.5313" x="379.3911" y="694.7422">(committed)</text><text fill="#FFFFFF" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="71.332" x="399.4907" y="709.6543">[Markdown]</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="432.9316" y="725.4795"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="99.0117" x="377.5605" y="741.7764">requirements/</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="476.5723" y="741.7764"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="11.7305" x="481.0225" y="741.7764">+</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="102.6279" x="383.8428" y="758.0732">ROADMAP.md,</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="249.3135" x="310.5" y="774.3701">spdd/canvas|analysis|reviews|sync,</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="59.999" x="365.0439" y="790.667">harness/</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="425.043" y="790.667"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="75.7764" x="429.4932" y="790.667">playbooks/</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="80.5068" x="394.9033" y="806.9639">extensions/</text></g><!--entity ledger--><g class="entity" data-qualified-name="repo.home.ledger" data-source-line="80" id="ent0006"><path d="M301.5,1172.2969 C301.5,1162.2969 407.9072,1162.2969 407.9072,1162.2969 C407.9072,1162.2969 514.3145,1162.2969 514.3145,1172.2969 L514.3145,1360.5625 C514.3145,1370.5625 407.9072,1370.5625 407.9072,1370.5625 C407.9072,1370.5625 301.5,1370.5625 301.5,1360.5625 L301.5,1172.2969" fill="#438DD5" style="stroke:#3C7FC0;stroke-width:0.5;"/><path d="M301.5,1172.2969 C301.5,1182.2969 407.9072,1182.2969 407.9072,1182.2969 C407.9072,1182.2969 514.3145,1182.2969 514.3145,1172.2969" fill="none" style="stroke:#3C7FC0;stroke-width:0.5;"/><text fill="#FFFFFF" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="71.543" x="372.1357" y="1197.4355">«container»</text><text fill="#FFFFFF" font-family="sans-serif" font-size="16" font-weight="700" lengthAdjust="spacing" textLength="72" x="330.6416" y="1215.1172">Lessons</text><text fill="#FFFFFF" font-family="sans-serif" font-size="16" font-weight="700" lengthAdjust="spacing" textLength="5.5703" x="402.6416" y="1215.1172"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="16" font-weight="700" lengthAdjust="spacing" textLength="57.9844" x="408.2119" y="1215.1172">ledger</text><text fill="#FFFFFF" font-family="sans-serif" font-size="16" font-weight="700" lengthAdjust="spacing" textLength="5.5703" x="466.1963" y="1215.1172"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="16" font-weight="700" lengthAdjust="spacing" textLength="13.4063" x="471.7666" y="1215.1172">+</text><text fill="#FFFFFF" font-family="sans-serif" font-size="16" font-weight="700" lengthAdjust="spacing" textLength="71.1719" x="313.7705" y="1233.7422">registry</text><text fill="#FFFFFF" font-family="sans-serif" font-size="16" font-weight="700" lengthAdjust="spacing" textLength="5.5703" x="384.9424" y="1233.7422"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="16" font-weight="700" lengthAdjust="spacing" textLength="111.5313" x="390.5127" y="1233.7422">(committed)</text><text fill="#FFFFFF" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="45.627" x="385.0938" y="1248.6543">[JSONL]</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="405.6821" y="1264.4795"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="190.668" x="312.5732" y="1280.7764">spdd/memory/lessons.jsonl</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="5.0518" x="342.4155" y="1297.0732">-</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="347.4673" y="1297.0732"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="64.4629" x="351.9175" y="1297.0732">accepted</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="416.3804" y="1297.0732"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="52.5684" x="420.8306" y="1297.0732">records</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="67.6211" x="319.5767" y="1313.3701">(decision,</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="387.1978" y="1313.3701"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="44.0029" x="391.6479" y="1313.3701">pitfall,</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="435.6509" y="1313.3701"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="56.1367" x="440.1011" y="1313.3701">pattern,</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="56.2734" x="344.2476" y="1329.667">session,</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="400.521" y="1329.667"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="66.5957" x="404.9712" y="1329.667">analysis).</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="192.8145" x="311.5" y="1345.9639">spdd/memory/registry.jsonl</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="5.0518" x="330.4629" y="1362.2607">-</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="335.5146" y="1362.2607"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="93.7686" x="339.9648" y="1362.2607">claim/release</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="433.7334" y="1362.2607"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="47.168" x="438.1836" y="1362.2607">events</text></g><!--entity scripts--><g class="entity" data-qualified-name="repo.home.scripts" data-source-line="76" id="ent0007"><rect fill="#438DD5" height="148.0469" style="stroke:#3C7FC0;stroke-width:0.5;" width="207.2021" x="58.5" y="654.7969"/><text fill="#FFFFFF" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="71.543" x="126.3296" y="675.9355">«container»</text><text fill="#FFFFFF" font-family="sans-serif" font-size="16" font-weight="700" lengthAdjust="spacing" textLength="85.3906" x="102.6753" y="693.6172">Workflow</text><text fill="#FFFFFF" font-family="sans-serif" font-size="16" font-weight="700" lengthAdjust="spacing" textLength="5.5703" x="188.0659" y="693.6172"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="16" font-weight="700" lengthAdjust="spacing" textLength="27.8906" x="193.6362" y="693.6172">CLI</text><text fill="#FFFFFF" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="38.8066" x="142.6978" y="708.5293">[Bash]</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="159.876" y="724.3545"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="104.125" x="78.6411" y="740.6514">scripts/sdlc.sh:</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="182.7661" y="740.6514"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="58.3447" x="187.2163" y="740.6514">capture,</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="51.4131" x="69.498" y="756.9482">accept,</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="120.9111" y="756.9482"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="42.1436" x="125.3613" y="756.9482">claim,</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="167.5049" y="756.9482"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="55.8086" x="171.9551" y="756.9482">release,</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="227.7637" y="756.9482"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="22.4902" x="232.2139" y="756.9482">db;</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="134.9756" x="68.5" y="773.2451">start-agent-session</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="203.4756" y="773.2451"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="47.7764" x="207.9258" y="773.2451">digest;</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="59.7393" x="73.0322" y="789.542">paths.sh</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="132.7715" y="789.542"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="39.6895" x="137.2217" y="789.542">home</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="176.9111" y="789.542"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="69.8086" x="181.3613" y="789.542">resolution</text></g><!--entity engine--><g class="entity" data-qualified-name="repo.home.engine" data-source-line="76" id="ent0008"><rect fill="#438DD5" height="164.3438" style="stroke:#3C7FC0;stroke-width:0.5;" width="265.4238" x="304.5" y="909.2969"/><text fill="#FFFFFF" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="71.543" x="401.4404" y="930.4355">«container»</text><text fill="#FFFFFF" font-family="sans-serif" font-size="16" font-weight="700" lengthAdjust="spacing" textLength="104.0078" x="385.208" y="948.1172">sdlc-engine</text><text fill="#FFFFFF" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="50.959" x="411.7324" y="963.0293">[Python]</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="434.9868" y="978.8545"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="53.0127" x="410.7056" y="995.1514">context</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="245.4238" x="314.5" y="1011.4482">retrieve/show/digest/accept/parity,</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="53.1836" x="357.4878" y="1027.7451">storage</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="410.6714" y="1027.7451"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="59.3018" x="415.1216" y="1027.7451">migrate,</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="474.4233" y="1027.7451"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="38.0625" x="478.8735" y="1027.7451">sqlite</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="53.2451" x="351.4517" y="1044.042">rebuild,</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="404.6968" y="1044.042"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="39.1494" x="409.147" y="1044.042">guide</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="448.2964" y="1044.042"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="70.2256" x="452.7466" y="1044.042">projection</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="38.4521" x="417.9858" y="1060.3389">client</text></g><!--entity runtime--><g class="entity" data-qualified-name="repo.home.runtime" data-source-line="80" id="ent0009"><path d="M47.5,1180.2969 C47.5,1170.2969 156.8706,1170.2969 156.8706,1170.2969 C156.8706,1170.2969 266.2412,1170.2969 266.2412,1180.2969 L266.2412,1352.2656 C266.2412,1362.2656 156.8706,1362.2656 156.8706,1362.2656 C156.8706,1362.2656 47.5,1362.2656 47.5,1352.2656 L47.5,1180.2969" fill="#438DD5" style="stroke:#3C7FC0;stroke-width:0.5;"/><path d="M47.5,1180.2969 C47.5,1190.2969 156.8706,1190.2969 156.8706,1190.2969 C156.8706,1190.2969 266.2412,1190.2969 266.2412,1180.2969" fill="none" style="stroke:#3C7FC0;stroke-width:0.5;"/><text fill="#FFFFFF" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="71.543" x="121.0991" y="1205.4355">«container»</text><text fill="#FFFFFF" font-family="sans-serif" font-size="16" font-weight="700" lengthAdjust="spacing" textLength="47.8672" x="94.4878" y="1223.1172">.sdlc/</text><text fill="#FFFFFF" font-family="sans-serif" font-size="16" font-weight="700" lengthAdjust="spacing" textLength="5.5703" x="142.355" y="1223.1172"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="16" font-weight="700" lengthAdjust="spacing" textLength="71.3281" x="147.9253" y="1223.1172">runtime</text><text fill="#FFFFFF" font-family="sans-serif" font-size="16" font-weight="700" lengthAdjust="spacing" textLength="108.7266" x="102.5073" y="1241.7422">(gitignored)</text><text fill="#FFFFFF" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="36.5684" x="138.5864" y="1256.6543">[Files]</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="154.6455" y="1272.4795"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="198.7412" x="57.5" y="1288.7764">sessions/current-session.md</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="22.9277" x="124.9194" y="1305.0732">hot</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="147.8472" y="1305.0732"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="36.5244" x="152.2974" y="1305.0732">brief,</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="141.251" x="86.2451" y="1321.3701">staged/lessons.jsonl</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="65.6387" x="58.6929" y="1337.667">captures,</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="124.3315" y="1337.667"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="81.0605" x="128.7817" y="1337.667">index.sqlite</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="209.8423" y="1337.667"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="40.7559" x="214.2925" y="1337.667">opt-in</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="41.46" x="90.2852" y="1353.9639">cache</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="131.7451" y="1353.9639"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="60.1563" x="136.1953" y="1353.9639">(schema</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="196.3516" y="1353.9639"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="22.6543" x="200.8018" y="1353.9639">v5)</text></g><!--entity agent--><g class="entity" data-qualified-name="agent" data-source-line="382" id="ent0001"><rect fill="#08427B" height="153.1767" style="stroke:#073B6F;stroke-width:0.5;" width="190.9326" x="339.5" y="44.2969"/><text fill="#FFFFFF" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="55.8164" x="407.0581" y="65.4355">«person»</text><image height="52" width="52" x="409.1202" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADQAAAA0CAYAAADFeBvrAAAGWElEQVR4Xu2YC0xTVxjHdWabBXQ63eZ0W9S5GeYwM9PNmDgNIKIwJejAFzIVx5xOMep0AkWQN/ISHxVUUEEUQRGhVMrLElEBgSsw3iBwpRRKoRQuD6Fn57iwmHMp3EJDi/Gf/NLmy73n/L/ennO+746bqG8/7k2CFhjr0AKqYM4vnjs2OV0nfCLSiTBuDhGVShCRyfmE3w0BYetzm9Dd5nt8khF7On6fKqAFRsISm6DDHtdS0wT5VZVltU1AJJGBVhkFZFQXaOvoAo0t7aCqvhlkl9SRwXFPHhodvHhtuonzInyckUALDIcpxk5zf/OOcU3ILK6uF0tBb18fGEqStg6Q9U+d7BiHK5i/xccQH3O40ALKgpI5xkkMIirqGzq7X+K+h1QFKQZuV1MEetv9jfGxhwMtoCy7vaK9i2saRb19ctwrY6G/IftiUvo8S+81+PjKQgsow4+2Z/ZkFtYIu3qUfzK4CqsawG6vGI6OkeM0fB5loAUYMn7yavbMywnZQml7J+5t2IpOe5a1aGegxQDzMYYWYALLwOHd73YE2MNdTIqbGomeCyXgyNmEEHw+ZaAFmKCzypFlH8yjpO1duKcRKzwpL36aifMCfE6m0AJM+GC1Eystt4Lq7unF/YxY/OxysGzPuRh8TqbQAkyAWzWroVlG9cmHv7MpUk4JCSzZEaOb0NQ1J1hUVw+Fm1GFiAoh2OF+621C/dACTEB/OVirUUxKHGWVWfAc/Hw0bHQTgmcQ646giBpOqTOUkrLKwFLbs6ObENq2j57nUqo8VJHQrnkxPmv0t214sE6Yv9lnfeWLZnHfCGo4XCU1jWCvb+zoH6z9nI99RLbIVPeUbqYQWXrWfqNf+vSDitP03Eoh3PFwb0orv7we/Ooepbbi9H+sXG94QzOinpfDrxpQd/vXOW767I2e6m0fEKjB2+cXy35cWFPd0dmNex1SBVVCcPwCT6C79ZRmNHj9bHWJPHL7QWH184YW0M3gaYla2sGDvKpnsHWP/2KDh+a04K+z0Nr/sP0FXlrSk7Ji2LB11opaQFNrO2hpo4BESoF6cRsoq2sCj4pqivyjMgSwBbGAR8AUfJyRQAuogllmbutW2YXwDp2JJ05HPyQu3csigu8+IZwv84ktzpHEZ+buxlqGDlr4faqAFhjr0AJMQV2rtqEjC1UNk4z+Q9vQYSJL334Cfq0itAwc3u+/H31qQ2DsPTjGePxaptACTEDJfL3Zm21uf7XxYNA9yiU0mXK/mkpZsCNK4ALfhl+vCAO7YB4qodC9sPWmNjldpxbbBN35cO0JPfxaptACgzHTzE33GIdLPi6qJYXiNqlYSslbYaWA3orKIHDh9xbXNEqi0wrI48E80oIdTq7YxyldvOs0d4lNEN/QLoT8MyCWDOflknBzIJtaOrpQPYjuRZ/NcPMQSdo7CyqFooCoDHKp7Rk+7mEoaIGB+HS9q4G1WxSRXVJXil7vDtZ69/b2ARnV/WpbrhW1gqp6SV8FKaYgnfA7EDa3vfoBBqsBX8IxJDC5Z5UNnX43BA8+MnX5Afp4B/c1ELQAzjfbfE1OhqUUlpNiRq94VSk0H/wBeiL5eTlfbfb5A65RbdwfDi3wOvO3+Jg5h/KfovNEXZLL5eiJyu9mFJXNs/RaCTeNibhPRgnNWOeqfyAw7j56Mpog9HbWKzwt9XNz9zm4V0YJGR+65MDPKmvDB1an6hpbwQb7a5dhAzgX9ztoQh+buixzvZKSIJZ24GOqXVEpBPjWym8l7nnQhJbv5XgmZJao9DWvqoTqQpMjob/Dg3gq7lthQgcC4kLKYRGpqdofcFcAj5KluG+FCfnfzAhBZ4mm6uztRwAeJ+a47wETQttiKDcnDB9EkxSTXgBgicQsoWlrTywMv5/LxQfRJAmIagDX+ZuTUG4pCfT3B79NSGP1xiWUAdfQT0zXEKxodTixj68M1iKoWxH8PLBoZyCzhBBWJ2+GPC19gY+jEero6gG23jFhn6w7qYv7VpjQ7I0ey//mJCaW1mpWtdAMa8ugmIf1C6x8TbUMHVi4b4UJIXS3+q7Y6X7LA3aMgiuJT0Fkcr7auJ6cLz93J/OFXWCci952v12TjdgzcL9DJoRABeAsMzfThdYBXvBkVhvf2wR5fmnptV/b0FEH94hDC4x1aIGxzr9XBgujsWOPcQAAAABJRU5ErkJggg==" y="68.2656"/><text fill="#FFFFFF" font-family="sans-serif" font-size="16" font-weight="700" lengthAdjust="spacing" textLength="18.3359" x="362.1069" y="134.8095">AI</text><text fill="#FFFFFF" font-family="sans-serif" font-size="16" font-weight="700" lengthAdjust="spacing" textLength="5.5703" x="380.4429" y="134.8095"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="16" font-weight="700" lengthAdjust="spacing" textLength="62.5156" x="386.0132" y="134.8095">Coding</text><text fill="#FFFFFF" font-family="sans-serif" font-size="16" font-weight="700" lengthAdjust="spacing" textLength="5.5703" x="448.5288" y="134.8095"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="16" font-weight="700" lengthAdjust="spacing" textLength="53.7266" x="454.0991" y="134.8095">Agent</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="432.7412" y="151.578"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="46.0195" x="349.5" y="167.8749">Cursor</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="395.5195" y="167.8749"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.7168" x="399.9697" y="167.8749">/</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="404.6865" y="167.8749"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="49.0615" x="409.1367" y="167.8749">Copilot</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="458.1982" y="167.8749"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.7168" x="462.6484" y="167.8749">/</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="467.3652" y="167.8749"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="48.6172" x="471.8154" y="167.8749">Claude</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="35.8408" x="417.0459" y="184.1718">Code</text></g><!--entity guide--><g class="entity" data-qualified-name="guide" data-source-line="419" id="ent0010"><rect fill="#999999" height="101.4844" style="stroke:#8A8A8A;stroke-width:0.5;" width="196.7363" x="639.5" y="1215.7969"/><text fill="#FFFFFF" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="113.8652" x="680.9355" y="1236.9355">«external_system»</text><text fill="#FFFFFF" font-family="sans-serif" font-size="16" font-weight="700" lengthAdjust="spacing" textLength="52.3125" x="687.9736" y="1254.6172">Guide</text><text fill="#FFFFFF" font-family="sans-serif" font-size="16" font-weight="700" lengthAdjust="spacing" textLength="5.5703" x="740.2861" y="1254.6172"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="16" font-weight="700" lengthAdjust="spacing" textLength="41.9063" x="745.8564" y="1254.6172">DICE</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="735.6431" y="1271.3857"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="40.4482" x="649.5" y="1287.6826">Neo4j</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="689.9482" y="1287.6826"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="40.9814" x="694.3984" y="1287.6826">graph</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="735.3799" y="1287.6826"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="70.2256" x="739.8301" y="1287.6826">projection</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="810.0557" y="1287.6826"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="11.7305" x="814.5059" y="1287.6826">+</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="47.9541" x="677.3906" y="1303.9795">spdd_*</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="725.3447" y="1303.9795"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="30.2969" x="729.7949" y="1303.9795">MCP</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="760.0918" y="1303.9795"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="33.8037" x="764.542" y="1303.9795">tools</text></g><!--link agent to adapters--><g class="link" data-entity-1="ent0001" data-entity-2="ent0003" data-link-type="dependency" data-source-line="1803" id="lnk11"><path d="M339.21,167.9069 C314.14,183.7769 289.11,203.6369 271,227.2969 C245.37,260.7869 232.2991,297.6949 223.7891,335.2249" fill="none" id="agent-to-adapters" style="stroke:#666666;stroke-width:1;"/><polygon fill="#666666" points="222.02,343.0269,226.7148,335.8883,220.8634,334.5615,222.02,343.0269" style="stroke:#666666;stroke-width:1;stroke-linejoin:miter;stroke-miterlimit:10;"/><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="51.9844" x="272" y="239.4355">invokes</text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="4.1777" x="323.9844" y="239.4355"> </text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="80.4902" x="328.1621" y="239.4355">/sdlc-spdd-*</text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="72.7383" x="303.957" y="253.4043">commands</text></g><!--link adapters to scripts--><g class="link" data-entity-1="ent0003" data-entity-2="ent0007" data-link-type="dependency" data-source-line="1803" id="lnk12"><path d="M199.39,491.6469 C191.59,540.8869 182.6113,597.6153 174.8213,646.8053" fill="none" id="adapters-to-scripts" style="stroke:#666666;stroke-width:1;"/><polygon fill="#666666" points="173.57,654.7069,177.7844,647.2746,171.8583,646.3361,173.57,654.7069" style="stroke:#666666;stroke-width:1;stroke-linejoin:miter;stroke-miterlimit:10;"/><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="49.5176" x="195" y="533.4355">resolve</text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="4.1777" x="244.5176" y="533.4355"> </text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="55.8105" x="248.6953" y="533.4355">context,</text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="4.1777" x="304.5059" y="533.4355"> </text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="56.6953" x="308.6836" y="533.4355">capture,</text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="44.7891" x="257.7949" y="547.4043">accept</text></g><!--link agent to contracts--><g class="link" data-entity-1="ent0001" data-entity-2="ent0005" data-link-type="dependency" data-source-line="1803" id="lnk13"><path d="M435,197.3269 C435,308.9269 435,509.5569 435,629.2269" fill="none" id="agent-to-contracts" style="stroke:#666666;stroke-width:1;"/><polygon fill="#666666" points="435,637.2269,438,629.2269,432,629.2269,435,637.2269" style="stroke:#666666;stroke-width:1;stroke-linejoin:miter;stroke-miterlimit:10;"/><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="37.8867" x="436" y="414.9355">reads</text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="4.1777" x="473.8867" y="414.9355"> </text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="46.8164" x="478.0645" y="414.9355">canvas</text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="4.1777" x="524.8809" y="414.9355"> </text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="10.0547" x="529.0586" y="414.9355">+</text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="4.1777" x="539.1133" y="414.9355"> </text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="40.5117" x="543.291" y="414.9355">phase</text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="57.1758" x="453.4521" y="428.9043">artifacts</text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="4.1777" x="510.6279" y="428.9043"> </text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="51.5449" x="514.8057" y="428.9043">directly</text></g><!--link scripts to engine--><g class="link" data-entity-1="ent0007" data-entity-2="ent0008" data-link-type="dependency" data-source-line="1803" id="lnk14"><path d="M239.33,803.0569 C274.13,836.0069 309.5125,869.5248 345.2625,903.3948" fill="none" id="scripts-to-engine" style="stroke:#666666;stroke-width:1;"/><polygon fill="#666666" points="351.07,908.8969,347.3258,901.217,343.1992,905.5726,351.07,908.8969" style="stroke:#666666;stroke-width:1;stroke-linejoin:miter;stroke-miterlimit:10;"/><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="66.6855" x="316" y="862.4355">delegates</text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="4.1777" x="382.6855" y="862.4355"> </text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="36.3105" x="386.8633" y="862.4355">when</text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="4.1777" x="423.1738" y="862.4355"> </text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="46.0664" x="427.3516" y="862.4355">engine</text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="61.1836" x="364.1172" y="876.4043">available</text></g><!--link scripts to runtime--><g class="link" data-entity-1="ent0007" data-entity-2="ent0009" data-link-type="dependency" data-source-line="1803" id="lnk15"><path d="M130.53,803.0169 C118.41,835.0169 106.03,873.3669 100,909.2969 C87.94,981.1769 89.52,1001.1669 100,1073.2969 C104.65,1105.2969 111.0979,1131.7374 120.4179,1162.1774" fill="none" id="scripts-to-runtime" style="stroke:#666666;stroke-width:1;"/><polygon fill="#666666" points="122.76,1169.8269,123.2865,1161.2991,117.5494,1163.0557,122.76,1169.8269" style="stroke:#666666;stroke-width:1;stroke-linejoin:miter;stroke-miterlimit:10;"/><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="44.8477" x="101" y="988.9355">stages</text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="4.1777" x="145.8477" y="988.9355"> </text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="63.8379" x="150.0254" y="988.9355">captures,</text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="4.1777" x="213.8633" y="988.9355"> </text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="42.1348" x="218.041" y="988.9355">writes</text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="4.1777" x="260.1758" y="988.9355"> </text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="22.5234" x="264.3535" y="988.9355">hot</text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="31.9805" x="177.9482" y="1002.9043">brief</text></g><!--link engine to ledger--><g class="link" data-entity-1="ent0008" data-entity-2="ent0006" data-link-type="dependency" data-source-line="1803" id="lnk16"><path d="M397.47,1073.5069 C394.01,1083.3569 391.04,1093.4269 389,1103.2969 C385.1,1122.1569 384.0441,1134.4469 385.4341,1154.0469" fill="none" id="engine-to-ledger" style="stroke:#666666;stroke-width:1;"/><polygon fill="#666666" points="386,1162.0269,388.4266,1153.8347,382.4416,1154.2591,386,1162.0269" style="stroke:#666666;stroke-width:1;stroke-linejoin:miter;stroke-miterlimit:10;"/><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="27.6387" x="397.2568" y="1115.4355">sole</text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="4.1777" x="424.8955" y="1115.4355"> </text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="34.9922" x="429.0732" y="1115.4355">write</text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="4.1777" x="464.0654" y="1115.4355"> </text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="35.7656" x="468.2432" y="1115.4355">path:</text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="4.1777" x="504.0088" y="1115.4355"> </text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="44.7891" x="508.1865" y="1115.4355">accept</text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="64.5176" x="390" y="1129.4043">promotes</text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="4.1777" x="454.5176" y="1129.4043"> </text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="46.2949" x="458.6953" y="1129.4043">staged</text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="4.1777" x="504.9902" y="1129.4043"> </text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="51.0645" x="509.168" y="1129.4043">records</text></g><!--link engine to runtime--><g class="link" data-entity-1="ent0008" data-entity-2="ent0009" data-link-type="dependency" data-source-line="1803" id="lnk17"><path d="M304.35,1060.7369 C295.14,1065.0869 285.96,1069.3169 277,1073.2969 C243.05,1088.3669 223.73,1075.5869 199,1103.2969 C182.46,1121.8269 174.2839,1138.3119 168.0639,1162.3519" fill="none" id="engine-to-runtime" style="stroke:#666666;stroke-width:1;"/><polygon fill="#666666" points="166.06,1170.0969,170.9683,1163.1034,165.1595,1161.6005,166.06,1170.0969" style="stroke:#666666;stroke-width:1;stroke-linejoin:miter;stroke-miterlimit:10;"/><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="55.1484" x="200" y="1115.4355">rebuilds</text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="4.1777" x="255.1484" y="1115.4355"> </text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="37.834" x="259.3262" y="1115.4355">sqlite</text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="4.1777" x="297.1602" y="1115.4355"> </text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="39.0059" x="301.3379" y="1115.4355">cache</text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="4.1777" x="340.3438" y="1115.4355"> </text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="31.8867" x="344.5215" y="1115.4355">from</text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="43.4883" x="266.46" y="1129.4043">ledger</text></g><!--link engine to guide--><g class="link" data-entity-1="ent0008" data-entity-2="ent0010" data-link-type="dependency" data-source-line="1803" id="lnk18"><path d="M531.38,1073.4269 C542.77,1083.4169 554.18,1093.5469 565,1103.2969 C606.11,1140.3469 645.9744,1177.6828 680.0544,1209.9928" fill="none" id="engine-to-guide" style="stroke:#666666;stroke-width:1;"/><polygon fill="#666666" points="685.86,1215.4969,682.1184,1207.8157,677.9904,1212.1699,685.86,1215.4969" style="stroke:#666666;stroke-width:1;stroke-linejoin:miter;stroke-miterlimit:10;"/><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="74.0332" x="594" y="1115.4355">re-projects</text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="4.1777" x="668.0332" y="1115.4355"> </text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="43.4883" x="672.2109" y="1115.4355">ledger</text><text fill="#666666" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="40.2832" x="634.708" y="1129.4043">[HTTP]</text></g><!--link agent to guide--><g class="link" data-entity-1="ent0001" data-entity-2="ent0010" data-link-type="dependency" data-source-line="1803" id="lnk19"><path d="M530.79,162.4869 C623.47,209.0369 750,294.8269 750,416.2969 C750,416.2969 750,416.2969 750,992.2969 C750,1070.3369 745.5681,1152.8237 742.0181,1207.5237" fill="none" id="agent-to-guide" style="stroke:#666666;stroke-width:1;"/><polygon fill="#666666" points="741.5,1215.5069,745.0118,1207.718,739.0244,1207.3294,741.5,1215.5069" style="stroke:#666666;stroke-width:1;stroke-linejoin:miter;stroke-miterlimit:10;"/><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="76.2305" x="751" y="726.4355">on-demand</text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="4.1777" x="827.2305" y="726.4355"> </text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="39.7383" x="831.4082" y="726.4355">graph</text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="4.1777" x="871.1465" y="726.4355"> </text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="50.584" x="875.3242" y="726.4355">queries</text><text fill="#666666" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="35.332" x="820.7881" y="740.4043">[MCP]</text></g><?plantuml-src RLLDSzis4BthLsnreXAHF3IzE2T7Ibjtx5WJkzEZ1m8W4ZO8q00eMLFdl_S1_B2YMWS9NEx7UxiFgr-KuReJakeGMdzHbeuTYag5kjjsNZfkJP0c5DmsPQkP2LsZSn-4LjjozIdVHDFVDzTbmtoG3gQ75IAOCj8LBNndiw22bZHPFULqjxvUvNURzPfk7Q-b3uu5wqWP7vZMKk14Z5fd7oKFb7cyP9Ma_Q-BsMo35DPaU3PXIVFBAwGMobHq6Irpc5OTF1oLUD4gRKCyQHOfmbFEaUFk2B3DmnVR6S7SCNEojGZS30NZerSeUfmlwDyPuJF1pvXWBPZwMDjx1SGcq6YCvM-OUnBsOCY7Rkk9zVd8MPlG5Jp1Az7GXXdXwIEvJala6-62TzE6QEXHZvu5tTTeJ6kLYRNGf6392yrptmfHHc8HwZcvsZOITTySATivriY3TKyKNoucbZypZQDtZ4SayzLufYp2KI58iJXb6yzEFdVAoGPpy2LuVR-zNDzSRef6B2bLvipicNzbXkaZMlVgv5x9Wt_rHyENL3Ddf4SaT7RSMlk4eto1-hoonaT-_yEutcQGIoKTgbyZ5dxK6s9RPONnkECvvB_kRhzTnqF2rCW6Wovr7rqyubj3eenpYP4APE7MOKoPa5n594jgLTX1fpWm97MmU4IdLoErHK6dsKSeKtgkcMfWrf9vIN8VM_OUmSnpfv8OvlzWNZjj3xIwlehelp1V9nQzHna7NFZwWZYqs3dClEUmx8ijQQYs9B7z374o5_9qY_81FGbLuIv-ZhHgZrH97GZpLdS13ky2bAR2xoYm_Yd2sXn3RPCiqWfvYTSWE0MgfQ_jeUoBbJt6icLEXICQELprHbNO2KZhdxKA4SIsKnewgZg5Ap6iXWYQQnNl_VlYS9q9Aic_YF1A6WoKOSEfobWtQE9t1G6aRlQzy2MkgJjf3aICTWJQWi8kuWHASQQQeV4ObZ92lXG3Tjk67FUMCwmwB3DyDupsloq6p3zcFwQDzFKbP8aXaFoHcAwlLbyZhc_IVdeaDAMjJzb_J39x-40tgmq5QtLKqUoxrEE2F5bNokpja_HKJgiW_p0j5uIbgCdzJNX90FkuXz8Sb-S26qFxWgTRmqc6czFVUa1jwoXsw4xjqYeHM1OywEEGOAeuIKf0iHLauEbGIpFOYUsPqcohvNdSsxJJUFp9G0wG5mpJ18VGiTAqHC1Kzixf5bmC5DDEjZ53F_XnAvobUGCmwDMF-ks7lqEMeTPPv3Xq9_DXkkCYYyF_y_v-ytENHtzhSY7Z00TfF7SIzFh_eLMC-Gy0?></g></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" data-diagram-type="DESCRIPTION" style="width:859px;height:1425px;background:#FFFFFF;" width="859px" height="1425px" viewBox="0 0 859 1425" zoomAndPan="magnify" preserveAspectRatio="none" contentStyleType="text/css"><?plantuml 1.2026.8?><defs/><g font-family="sans-serif" lengthAdjust="spacing"><g class="title" data-source-line="3"><text x="179.684" y="24.966" fill="#000" font-size="14" textLength="492.756" font-weight="700">Containers - SDLC-SPDD Orchestrator installed in a project (storage v3)</text></g><!--cluster repo--><g class="cluster" data-qualified-name="repo" id="ent0002" data-source-line="1690"><rect x="14" y="238.068" width="564" height="1163" fill="none" style="stroke:#444;stroke-width:1;stroke-dasharray:7,7;"/><text x="224.16" y="277.172" fill="#444" font-size="16" textLength="143.68" font-weight="700">Project repository</text><text x="270.608" y="294.688" fill="#444" font-size="12" textLength="50.784" font-weight="700">[system]</text></g><!--cluster home--><g class="cluster" data-qualified-name="repo.home" id="ent0004" data-source-line="1690"><rect x="30" y="539.068" width="532" height="846" fill="none" style="stroke:#444;stroke-width:1;stroke-dasharray:7,7;"/><text x="178.448" y="578.172" fill="#444" font-size="16" textLength="235.104" font-weight="700">sdlc-spdd/ (framework home)</text><text x="270.608" y="595.688" fill="#444" font-size="12" textLength="50.784" font-weight="700">[system]</text></g><!--entity agent--><g class="entity" data-qualified-name="agent" id="ent0001" data-source-line="382"><rect x="305.5" y="44.068" width="218.827" height="147.964" fill="#08427B" style="stroke:#073B6F;stroke-width:0.5;"/><text x="390.889" y="66.896" fill="#FFF" font-size="12" textLength="48.048" font-style="italic">«person»</text><image width="52" height="52" x="389.067" y="70.412" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADQAAAA0CAYAAADFeBvrAAAGWElEQVR4Xu2YC0xTVxjHdWabBXQ63eZ0W9S5GeYwM9PNmDgNIKIwJejAFzIVx5xOMep0AkWQN/ISHxVUUEEUQRGhVMrLElEBgSsw3iBwpRRKoRQuD6Fn57iwmHMp3EJDi/Gf/NLmy73n/L/ennO+746bqG8/7k2CFhjr0AKqYM4vnjs2OV0nfCLSiTBuDhGVShCRyfmE3w0BYetzm9Dd5nt8khF7On6fKqAFRsISm6DDHtdS0wT5VZVltU1AJJGBVhkFZFQXaOvoAo0t7aCqvhlkl9SRwXFPHhodvHhtuonzInyckUALDIcpxk5zf/OOcU3ILK6uF0tBb18fGEqStg6Q9U+d7BiHK5i/xccQH3O40ALKgpI5xkkMIirqGzq7X+K+h1QFKQZuV1MEetv9jfGxhwMtoCy7vaK9i2saRb19ctwrY6G/IftiUvo8S+81+PjKQgsow4+2Z/ZkFtYIu3qUfzK4CqsawG6vGI6OkeM0fB5loAUYMn7yavbMywnZQml7J+5t2IpOe5a1aGegxQDzMYYWYALLwOHd73YE2MNdTIqbGomeCyXgyNmEEHw+ZaAFmKCzypFlH8yjpO1duKcRKzwpL36aifMCfE6m0AJM+GC1Eystt4Lq7unF/YxY/OxysGzPuRh8TqbQAkyAWzWroVlG9cmHv7MpUk4JCSzZEaOb0NQ1J1hUVw+Fm1GFiAoh2OF+621C/dACTEB/OVirUUxKHGWVWfAc/Hw0bHQTgmcQ646giBpOqTOUkrLKwFLbs6ObENq2j57nUqo8VJHQrnkxPmv0t214sE6Yv9lnfeWLZnHfCGo4XCU1jWCvb+zoH6z9nI99RLbIVPeUbqYQWXrWfqNf+vSDitP03Eoh3PFwb0orv7we/Ooepbbi9H+sXG94QzOinpfDrxpQd/vXOW767I2e6m0fEKjB2+cXy35cWFPd0dmNex1SBVVCcPwCT6C79ZRmNHj9bHWJPHL7QWH184YW0M3gaYla2sGDvKpnsHWP/2KDh+a04K+z0Nr/sP0FXlrSk7Ji2LB11opaQFNrO2hpo4BESoF6cRsoq2sCj4pqivyjMgSwBbGAR8AUfJyRQAuogllmbutW2YXwDp2JJ05HPyQu3csigu8+IZwv84ktzpHEZ+buxlqGDlr4faqAFhjr0AJMQV2rtqEjC1UNk4z+Q9vQYSJL334Cfq0itAwc3u+/H31qQ2DsPTjGePxaptACTEDJfL3Zm21uf7XxYNA9yiU0mXK/mkpZsCNK4ALfhl+vCAO7YB4qodC9sPWmNjldpxbbBN35cO0JPfxaptACgzHTzE33GIdLPi6qJYXiNqlYSslbYaWA3orKIHDh9xbXNEqi0wrI48E80oIdTq7YxyldvOs0d4lNEN/QLoT8MyCWDOflknBzIJtaOrpQPYjuRZ/NcPMQSdo7CyqFooCoDHKp7Rk+7mEoaIGB+HS9q4G1WxSRXVJXil7vDtZ69/b2ARnV/WpbrhW1gqp6SV8FKaYgnfA7EDa3vfoBBqsBX8IxJDC5Z5UNnX43BA8+MnX5Afp4B/c1ELQAzjfbfE1OhqUUlpNiRq94VSk0H/wBeiL5eTlfbfb5A65RbdwfDi3wOvO3+Jg5h/KfovNEXZLL5eiJyu9mFJXNs/RaCTeNibhPRgnNWOeqfyAw7j56Mpog9HbWKzwt9XNz9zm4V0YJGR+65MDPKmvDB1an6hpbwQb7a5dhAzgX9ztoQh+buixzvZKSIJZ24GOqXVEpBPjWym8l7nnQhJbv5XgmZJao9DWvqoTqQpMjob/Dg3gq7lthQgcC4kLKYRGpqdofcFcAj5KluG+FCfnfzAhBZ4mm6uztRwAeJ+a47wETQttiKDcnDB9EkxSTXgBgicQsoWlrTywMv5/LxQfRJAmIagDX+ZuTUG4pCfT3B79NSGP1xiWUAdfQT0zXEKxodTixj68M1iKoWxH8PLBoZyCzhBBWJ2+GPC19gY+jEero6gG23jFhn6w7qYv7VpjQ7I0ey//mJCaW1mpWtdAMa8ugmIf1C6x8TbUMHVi4b4UJIXS3+q7Y6X7LA3aMgiuJT0Fkcr7auJ6cLz93J/OFXWCci952v12TjdgzcL9DJoRABeAsMzfThdYBXvBkVhvf2wR5fmnptV/b0FEH94hDC4x1aIGxzr9XBgujsWOPcQAAAABJRU5ErkJggg=="/><text x="350.209" y="139.208" fill="#FFF" font-size="16" textLength="17.264" font-weight="700">AI</text><text x="367.474" y="139.208" fill="#FFF" font-size="16" font-weight="700"> </text><text x="371.634" y="139.208" fill="#FFF" font-size="16" textLength="55.744" font-weight="700">Coding</text><text x="427.378" y="139.208" fill="#FFF" font-size="16" font-weight="700"> </text><text x="431.538" y="139.208" fill="#FFF" font-size="16" textLength="48.08" font-weight="700">Agent</text><text x="413.094" y="158.862" fill="#FFF" font-size="14"> </text><text x="315.5" y="177.93" fill="#FFF" font-size="14" textLength="44.24">Cursor</text><text x="359.74" y="177.93" fill="#FFF" font-size="14"> </text><text x="363.38" y="177.93" fill="#FFF" font-size="14">/</text><text x="368.588" y="177.93" fill="#FFF" font-size="14"> </text><text x="372.228" y="177.93" fill="#FFF" font-size="14" textLength="46.676">Copilot</text><text x="418.904" y="177.93" fill="#FFF" font-size="14"> </text><text x="422.544" y="177.93" fill="#FFF" font-size="14">/</text><text x="427.752" y="177.93" fill="#FFF" font-size="14"> </text><text x="431.392" y="177.93" fill="#FFF" font-size="14" textLength="45.472">Claude</text><text x="476.863" y="177.93" fill="#FFF" font-size="14"> </text><text x="480.503" y="177.93" fill="#FFF" font-size="14" textLength="33.824">Code</text></g><!--entity guide--><g class="entity" data-qualified-name="guide" id="ent0010" data-source-line="419"><rect x="598" y="1192.068" width="183.939" height="115.34" fill="#999" style="stroke:#8A8A8A;stroke-width:0.5;"/><text x="641.957" y="1214.896" fill="#FFF" font-size="12" textLength="96.025" font-style="italic">«external_system»</text><text x="646.002" y="1235.516" fill="#FFF" font-size="16" textLength="46.56" font-weight="700">Guide</text><text x="692.562" y="1235.516" fill="#FFF" font-size="16" font-weight="700"> </text><text x="696.722" y="1235.516" fill="#FFF" font-size="16" textLength="37.216" font-weight="700">DICE</text><text x="688.15" y="1255.17" fill="#FFF" font-size="14"> </text><text x="608" y="1274.238" fill="#FFF" font-size="14" textLength="38.626">Neo4j</text><text x="646.626" y="1274.238" fill="#FFF" font-size="14"> </text><text x="650.266" y="1274.238" fill="#FFF" font-size="14" textLength="39.508">graph</text><text x="689.774" y="1274.238" fill="#FFF" font-size="14"> </text><text x="693.414" y="1274.238" fill="#FFF" font-size="14" textLength="66.878">projection</text><text x="760.291" y="1274.238" fill="#FFF" font-size="14"> </text><text x="763.931" y="1274.238" fill="#FFF" font-size="14">+</text><text x="631.933" y="1293.306" fill="#FFF" font-size="14" textLength="46.466">spdd_*</text><text x="678.399" y="1293.306" fill="#FFF" font-size="14"> </text><text x="682.039" y="1293.306" fill="#FFF" font-size="14" textLength="30.016">MCP</text><text x="712.055" y="1293.306" fill="#FFF" font-size="14"> </text><text x="715.695" y="1293.306" fill="#FFF" font-size="14" textLength="32.312">tools</text></g><!--entity adapters--><g class="entity" data-qualified-name="repo.adapters" id="ent0003" data-source-line="76"><rect x="60.5" y="323.068" width="217.133" height="169.82" fill="#438DD5" style="stroke:#3C7FC0;stroke-width:0.5;"/><text x="137.968" y="345.896" fill="#FFF" font-size="12" textLength="62.196" font-style="italic">«container»</text><text x="94.466" y="366.516" fill="#FFF" font-size="16" textLength="73.84" font-weight="700">Assistant</text><text x="168.307" y="366.516" fill="#FFF" font-size="16" font-weight="700"> </text><text x="172.467" y="366.516" fill="#FFF" font-size="16" textLength="71.2" font-weight="700">adapters</text><text x="87.976" y="384.032" fill="#FFF" font-size="12" textLength="60.432" font-style="italic">[Markdown</text><text x="148.408" y="384.032" fill="#FFF" font-size="12" font-style="italic"> </text><text x="151.529" y="384.032" fill="#FFF" font-size="12" textLength="28.248" font-style="italic">stubs</text><text x="179.777" y="384.032" fill="#FFF" font-size="12" font-style="italic"> </text><text x="182.897" y="384.032" fill="#FFF" font-size="12" textLength="10.8" font-style="italic">at</text><text x="193.697" y="384.032" fill="#FFF" font-size="12" font-style="italic"> </text><text x="196.817" y="384.032" fill="#FFF" font-size="12" textLength="24.468" font-style="italic">repo</text><text x="221.285" y="384.032" fill="#FFF" font-size="12" font-style="italic"> </text><text x="224.405" y="384.032" fill="#FFF" font-size="12" textLength="25.752" font-style="italic">root]</text><text x="167.247" y="402.514" fill="#FFF" font-size="14"> </text><text x="81.091" y="421.582" fill="#FFF" font-size="14" textLength="124.264">.cursor/commands</text><text x="205.354" y="421.582" fill="#FFF" font-size="14"> </text><text x="208.994" y="421.582" fill="#FFF" font-size="14">+</text><text x="217.002" y="421.582" fill="#FFF" font-size="14"> </text><text x="220.642" y="421.582" fill="#FFF" font-size="14" textLength="36.4">rules,</text><text x="89.316" y="440.65" fill="#FFF" font-size="14" textLength="46.676">Copilot</text><text x="135.992" y="440.65" fill="#FFF" font-size="14"> </text><text x="139.632" y="440.65" fill="#FFF" font-size="14" textLength="60.074">prompts,</text><text x="199.706" y="440.65" fill="#FFF" font-size="14"> </text><text x="203.346" y="440.65" fill="#FFF" font-size="14" textLength="45.472">Claude</text><text x="70.5" y="459.718" fill="#FFF" font-size="14" textLength="76.944">commands.</text><text x="147.444" y="459.718" fill="#FFF" font-size="14"> </text><text x="151.084" y="459.718" fill="#FFF" font-size="14" textLength="28.7">Thin</text><text x="179.784" y="459.718" fill="#FFF" font-size="14"> </text><text x="183.424" y="459.718" fill="#FFF" font-size="14" textLength="54.782">pointers</text><text x="238.205" y="459.718" fill="#FFF" font-size="14"> </text><text x="241.845" y="459.718" fill="#FFF" font-size="14" textLength="25.788">into</text><text x="135.117" y="478.786" fill="#FFF" font-size="14" textLength="67.9">sdlc-spdd/</text></g><!--entity contracts--><g class="entity" data-qualified-name="repo.home.contracts" id="ent0005" data-source-line="76"><rect x="284.5" y="624.068" width="260.897" height="188.888" fill="#438DD5" style="stroke:#3C7FC0;stroke-width:0.5;"/><text x="383.85" y="646.896" fill="#FFF" font-size="12" textLength="62.196" font-style="italic">«container»</text><text x="324.692" y="667.516" fill="#FFF" font-size="16" textLength="77.6" font-weight="700">Contracts</text><text x="402.293" y="667.516" fill="#FFF" font-size="16" font-weight="700"> </text><text x="406.453" y="667.516" fill="#FFF" font-size="16" textLength="98.752" font-weight="700">(committed)</text><text x="382.992" y="685.032" fill="#FFF" font-size="12" textLength="63.912" font-style="italic">[Markdown]</text><text x="413.129" y="703.514" fill="#FFF" font-size="14"> </text><text x="361.707" y="722.582" fill="#FFF" font-size="14" textLength="94.836">requirements/</text><text x="456.542" y="722.582" fill="#FFF" font-size="14"> </text><text x="460.182" y="722.582" fill="#FFF" font-size="14">+</text><text x="365.886" y="741.65" fill="#FFF" font-size="14" textLength="98.126">ROADMAP.md,</text><text x="294.5" y="760.718" fill="#FFF" font-size="14" textLength="240.897">spdd/canvas|analysis|reviews|sync,</text><text x="348.323" y="779.786" fill="#FFF" font-size="14" textLength="57.456">harness/</text><text x="405.779" y="779.786" fill="#FFF" font-size="14"> </text><text x="409.419" y="779.786" fill="#FFF" font-size="14" textLength="72.156">playbooks/</text><text x="376.82" y="798.854" fill="#FFF" font-size="14" textLength="76.258">extensions/</text></g><!--entity ledger--><g class="entity" data-qualified-name="repo.home.ledger" id="ent0006" data-source-line="80"><path d="M281.5,1140.068 C281.5,1130.068 390.852,1130.068 390.852,1130.068 C390.852,1130.068 500.204,1130.068 500.204,1140.068 L500.204,1358.816 C500.204,1368.816 390.852,1368.816 390.852,1368.816 C390.852,1368.816 281.5,1368.816 281.5,1358.816 L281.5,1140.068" style="stroke:#3C7FC0;stroke-width:0.5;" fill="#438DD5"/><path d="M281.5,1140.068 C281.5,1150.068 390.852,1150.068 390.852,1150.068 C390.852,1150.068 500.204,1150.068 500.204,1140.068" style="stroke:#3C7FC0;stroke-width:0.5;" fill="none"/><text x="359.754" y="1166.896" fill="#FFF" font-size="12" textLength="62.196" font-style="italic">«container»</text><text x="291.5" y="1187.516" fill="#FFF" font-size="16" textLength="62.768" font-weight="700">Lessons</text><text x="354.268" y="1187.516" fill="#FFF" font-size="16" font-weight="700"> </text><text x="358.428" y="1187.516" fill="#FFF" font-size="16" textLength="51.312" font-weight="700">ledger</text><text x="409.74" y="1187.516" fill="#FFF" font-size="16" font-weight="700"> </text><text x="413.9" y="1187.516" fill="#FFF" font-size="16" font-weight="700">+</text><text x="423.052" y="1187.516" fill="#FFF" font-size="16" font-weight="700"> </text><text x="427.212" y="1187.516" fill="#FFF" font-size="16" textLength="62.992" font-weight="700">registry</text><text x="341.476" y="1209.308" fill="#FFF" font-size="16" textLength="98.752" font-weight="700">(committed)</text><text x="371.268" y="1226.824" fill="#FFF" font-size="12" textLength="39.168" font-style="italic">[JSONL]</text><text x="389.032" y="1245.306" fill="#FFF" font-size="14"> </text><text x="295.793" y="1264.374" fill="#FFF" font-size="14" textLength="181.971">spdd/memory/lessons.jsonl</text><text x="477.764" y="1264.374" fill="#FFF" font-size="14"> </text><text x="481.404" y="1264.374" fill="#FFF" font-size="14">-</text><text x="301.435" y="1283.442" fill="#FFF" font-size="14" textLength="59.36">accepted</text><text x="360.794" y="1283.442" fill="#FFF" font-size="14"> </text><text x="364.434" y="1283.442" fill="#FFF" font-size="14" textLength="49.966">records</text><text x="414.4" y="1283.442" fill="#FFF" font-size="14"> </text><text x="418.04" y="1283.442" fill="#FFF" font-size="14" textLength="62.23">(decision,</text><text x="314.174" y="1302.51" fill="#FFF" font-size="14" textLength="40.922">pitfall,</text><text x="355.096" y="1302.51" fill="#FFF" font-size="14"> </text><text x="358.736" y="1302.51" fill="#FFF" font-size="14" textLength="52.654">pattern,</text><text x="411.39" y="1302.51" fill="#FFF" font-size="14"> </text><text x="415.03" y="1302.51" fill="#FFF" font-size="14" textLength="52.5">session,</text><text x="360.808" y="1321.578" fill="#FFF" font-size="14" textLength="60.088">analysis).</text><text x="294.876" y="1340.646" fill="#FFF" font-size="14" textLength="183.805">spdd/memory/registry.jsonl</text><text x="478.681" y="1340.646" fill="#FFF" font-size="14"> </text><text x="482.321" y="1340.646" fill="#FFF" font-size="14">-</text><text x="323.505" y="1359.714" fill="#FFF" font-size="14" textLength="87.738">claim/release</text><text x="411.243" y="1359.714" fill="#FFF" font-size="14"> </text><text x="414.883" y="1359.714" fill="#FFF" font-size="14" textLength="43.316">events</text></g><!--entity scripts--><g class="entity" data-qualified-name="repo.home.scripts" id="ent0007" data-source-line="76"><rect x="46" y="624.068" width="219.653" height="188.888" fill="#438DD5" style="stroke:#3C7FC0;stroke-width:0.5;"/><text x="124.728" y="646.896" fill="#FFF" font-size="12" textLength="62.196" font-style="italic">«container»</text><text x="102.403" y="667.516" fill="#FFF" font-size="16" textLength="77.232" font-weight="700">Workflow</text><text x="179.635" y="667.516" fill="#FFF" font-size="16" font-weight="700"> </text><text x="183.795" y="667.516" fill="#FFF" font-size="16" textLength="25.456" font-weight="700">CLI</text><text x="139.272" y="685.032" fill="#FFF" font-size="12" textLength="33.108" font-style="italic">[Bash]</text><text x="154.007" y="703.514" fill="#FFF" font-size="14"> </text><text x="56" y="722.582" fill="#FFF" font-size="14" textLength="96.908">scripts/sdlc.sh:</text><text x="152.908" y="722.582" fill="#FFF" font-size="14"> </text><text x="156.548" y="722.582" fill="#FFF" font-size="14" textLength="25.97">thin</text><text x="182.518" y="722.582" fill="#FFF" font-size="14"> </text><text x="186.158" y="722.582" fill="#FFF" font-size="14" textLength="69.496">dispatcher</text><text x="71.05" y="741.65" fill="#FFF" font-size="14" textLength="13.524">to</text><text x="84.574" y="741.65" fill="#FFF" font-size="14"> </text><text x="88.214" y="741.65" fill="#FFF" font-size="14" textLength="46.578">python</text><text x="134.792" y="741.65" fill="#FFF" font-size="14"> </text><text x="138.432" y="741.65" fill="#FFF" font-size="14" textLength="17.598">-m</text><text x="156.03" y="741.65" fill="#FFF" font-size="14"> </text><text x="159.67" y="741.65" fill="#FFF" font-size="14" textLength="80.934">sdlc_engine;</text><text x="88.263" y="760.718" fill="#FFF" font-size="14" textLength="126.28">start-agent-session</text><text x="214.542" y="760.718" fill="#FFF" font-size="14"> </text><text x="218.182" y="760.718" fill="#FFF" font-size="14">/</text><text x="61.208" y="779.786" fill="#FFF" font-size="14" textLength="163.799">capture-session-memory</text><text x="225.007" y="779.786" fill="#FFF" font-size="14"> </text><text x="228.647" y="779.786" fill="#FFF" font-size="14" textLength="21.798">call</text><text x="91.896" y="798.854" fill="#FFF" font-size="14" textLength="75.474">sdlc-engine</text><text x="167.37" y="798.854" fill="#FFF" font-size="14"> </text><text x="171.01" y="798.854" fill="#FFF" font-size="14" textLength="48.748">session</text></g><!--entity engine--><g class="entity" data-qualified-name="repo.home.engine" id="ent0008" data-source-line="76"><rect x="287" y="869.068" width="251.839" height="188.888" fill="#438DD5" style="stroke:#3C7FC0;stroke-width:0.5;"/><text x="381.821" y="891.896" fill="#FFF" font-size="12" textLength="62.196" font-style="italic">«container»</text><text x="367.279" y="912.516" fill="#FFF" font-size="16" textLength="91.28" font-weight="700">sdlc-engine</text><text x="390.917" y="930.032" fill="#FFF" font-size="12" textLength="44.004" font-style="italic">[Python]</text><text x="411.1" y="948.514" fill="#FFF" font-size="14"> </text><text x="388.294" y="967.582" fill="#FFF" font-size="14" textLength="49.252">context</text><text x="297" y="986.65" fill="#FFF" font-size="14" textLength="231.839">retrieve/show/digest/accept/parity,</text><text x="326.19" y="1005.718" fill="#FFF" font-size="14" textLength="173.459">workflow/registry/pointer,</text><text x="347.582" y="1024.786" fill="#FFF" font-size="14" textLength="35.49">sqlite</text><text x="383.072" y="1024.786" fill="#FFF" font-size="14"> </text><text x="386.712" y="1024.786" fill="#FFF" font-size="14" textLength="50.526">rebuild,</text><text x="437.237" y="1024.786" fill="#FFF" font-size="14"> </text><text x="440.877" y="1024.786" fill="#FFF" font-size="14" textLength="37.38">guide</text><text x="359.888" y="1043.854" fill="#FFF" font-size="14" textLength="66.878">projection</text><text x="426.766" y="1043.854" fill="#FFF" font-size="14"> </text><text x="430.405" y="1043.854" fill="#FFF" font-size="14" textLength="35.546">client</text></g><!--entity runtime--><g class="entity" data-qualified-name="repo.home.runtime" id="ent0009" data-source-line="80"><path d="M46.5,1159.068 C46.5,1149.068 155.207,1149.068 155.207,1149.068 C155.207,1149.068 263.913,1149.068 263.913,1159.068 L263.913,1339.68 C263.913,1349.68 155.207,1349.68 155.207,1349.68 C155.207,1349.68 46.5,1349.68 46.5,1339.68 L46.5,1159.068" style="stroke:#3C7FC0;stroke-width:0.5;" fill="#438DD5"/><path d="M46.5,1159.068 C46.5,1169.068 155.207,1169.068 155.207,1169.068 C155.207,1169.068 263.913,1169.068 263.913,1159.068" style="stroke:#3C7FC0;stroke-width:0.5;" fill="none"/><text x="124.108" y="1185.896" fill="#FFF" font-size="12" textLength="62.196" font-style="italic">«container»</text><text x="99.311" y="1206.516" fill="#FFF" font-size="16" textLength="42.352" font-weight="700">.sdlc/</text><text x="141.663" y="1206.516" fill="#FFF" font-size="16" font-weight="700"> </text><text x="145.823" y="1206.516" fill="#FFF" font-size="16" textLength="65.28" font-weight="700">runtime</text><text x="107.671" y="1228.308" fill="#FFF" font-size="16" textLength="95.072" font-weight="700">(gitignored)</text><text x="140.183" y="1245.824" fill="#FFF" font-size="12" textLength="30.048" font-style="italic">[Files]</text><text x="153.387" y="1264.306" fill="#FFF" font-size="14"> </text><text x="61.253" y="1283.374" fill="#FFF" font-size="14" textLength="187.907">sessions/current-session.md</text><text x="56.5" y="1302.442" fill="#FFF" font-size="14" textLength="22.176">hot</text><text x="78.676" y="1302.442" fill="#FFF" font-size="14"> </text><text x="82.316" y="1302.442" fill="#FFF" font-size="14" textLength="34.468">brief,</text><text x="116.784" y="1302.442" fill="#FFF" font-size="14"> </text><text x="120.424" y="1302.442" fill="#FFF" font-size="14" textLength="133.49">staged/lessons.jsonl</text><text x="63.892" y="1321.51" fill="#FFF" font-size="14" textLength="61.026">captures,</text><text x="124.918" y="1321.51" fill="#FFF" font-size="14"> </text><text x="128.558" y="1321.51" fill="#FFF" font-size="14" textLength="75.418">index.sqlite</text><text x="203.975" y="1321.51" fill="#FFF" font-size="14"> </text><text x="207.615" y="1321.51" fill="#FFF" font-size="14" textLength="38.906">opt-in</text><text x="95.427" y="1340.578" fill="#FFF" font-size="14" textLength="37.842">cache</text><text x="133.269" y="1340.578" fill="#FFF" font-size="14"> </text><text x="136.909" y="1340.578" fill="#FFF" font-size="14" textLength="55.118">(schema</text><text x="192.026" y="1340.578" fill="#FFF" font-size="14"> </text><text x="195.666" y="1340.578" fill="#FFF" font-size="14" textLength="19.32">v5)</text></g><g class="link" data-entity-1="ent0001" data-entity-2="ent0003" id="lnk11" data-link-type="dependency"><polygon points="168.53,323.063,172.307,315.399,166.336,314.805,168.53,323.063" fill="#666" style="stroke:#666;stroke-width:1;stroke-linejoin:miter;stroke-miterlimit:10;"/><path d="M305.444,145.145 C269.522,158.861 232.563,179.445 208,210.068 C182.694,241.617 173.107,277.062 169.322,315.102" style="stroke:#666;stroke-width:1;" fill="none"/><text x="210" y="224.896" fill="#666" font-size="12" textLength="46.296" font-weight="700">invokes</text><text x="256.296" y="224.896" fill="#666" font-size="12" font-weight="700"> </text><text x="259.416" y="224.896" fill="#666" font-size="12" textLength="71.364" font-weight="700">/sdlc-spdd-*</text><text x="330.781" y="224.896" fill="#666" font-size="12" font-weight="700"> </text><text x="333.901" y="224.896" fill="#666" font-size="12" textLength="65.856" font-weight="700">commands</text></g><g class="link" data-entity-1="ent0003" data-entity-2="ent0007" id="lnk12" data-link-type="dependency"><polygon points="159.948,623.869,163.283,616.003,157.288,615.75,159.948,623.869" fill="#666" style="stroke:#666;stroke-width:1;stroke-linejoin:miter;stroke-miterlimit:10;"/><path d="M165.448,493.367 C163.745,533.775 162.037,574.315 160.285,615.876" style="stroke:#666;stroke-width:1;" fill="none"/><text x="167" y="525.896" fill="#666" font-size="12" textLength="43.512" font-weight="700">resolve</text><text x="210.512" y="525.896" fill="#666" font-size="12" font-weight="700"> </text><text x="213.632" y="525.896" fill="#666" font-size="12" textLength="49.344" font-weight="700">context,</text><text x="262.977" y="525.896" fill="#666" font-size="12" font-weight="700"> </text><text x="266.097" y="525.896" fill="#666" font-size="12" textLength="50.064" font-weight="700">capture,</text><text x="316.161" y="525.896" fill="#666" font-size="12" font-weight="700"> </text><text x="319.281" y="525.896" fill="#666" font-size="12" textLength="39.48" font-weight="700">accept</text></g><g class="link" data-entity-1="ent0001" data-entity-2="ent0005" id="lnk13" data-link-type="dependency"><polygon points="415,624.04,418,616.04,412,616.04,415,624.04" fill="#666" style="stroke:#666;stroke-width:1;stroke-linejoin:miter;stroke-miterlimit:10;"/><path d="M415,192.156 C415,300.753 415,496.696 415,616.04" style="stroke:#666;stroke-width:1;" fill="none"/><text x="417" y="404.896" fill="#666" font-size="12" textLength="33.348" font-weight="700">reads</text><text x="450.348" y="404.896" fill="#666" font-size="12" font-weight="700"> </text><text x="453.468" y="404.896" fill="#666" font-size="12" textLength="41.34" font-weight="700">canvas</text><text x="494.809" y="404.896" fill="#666" font-size="12" font-weight="700"> </text><text x="497.929" y="404.896" fill="#666" font-size="12" font-weight="700">+</text><text x="504.793" y="404.896" fill="#666" font-size="12" font-weight="700"> </text><text x="507.913" y="404.896" fill="#666" font-size="12" textLength="35.784" font-weight="700">phase</text><text x="543.697" y="404.896" fill="#666" font-size="12" font-weight="700"> </text><text x="546.817" y="404.896" fill="#666" font-size="12" textLength="50.796" font-weight="700">artifacts</text><text x="484.476" y="421.24" fill="#666" font-size="12" textLength="45.66" font-weight="700">directly</text></g><g class="link" data-entity-1="ent0007" data-entity-2="ent0008" id="lnk14" data-link-type="dependency"><polygon points="314.056,869.014,310.304,861.337,306.182,865.697,314.056,869.014" fill="#666" style="stroke:#666;stroke-width:1;stroke-linejoin:miter;stroke-miterlimit:10;"/><path d="M254.995,813.17 C274.367,831.487 288.872,845.202 308.243,863.517" style="stroke:#666;stroke-width:1;" fill="none"/><text x="295" y="845.896" fill="#666" font-size="12" textLength="58.548" font-weight="700">delegates</text><text x="353.548" y="845.896" fill="#666" font-size="12" font-weight="700"> </text><text x="356.668" y="845.896" fill="#666" font-size="12" textLength="33.132" font-weight="700">when</text><text x="389.801" y="845.896" fill="#666" font-size="12" font-weight="700"> </text><text x="392.921" y="845.896" fill="#666" font-size="12" textLength="41.208" font-weight="700">engine</text><text x="434.129" y="845.896" fill="#666" font-size="12" font-weight="700"> </text><text x="437.249" y="845.896" fill="#666" font-size="12" textLength="54.24" font-weight="700">available</text></g><g class="link" data-entity-1="ent0007" data-entity-2="ent0009" id="lnk15" data-link-type="dependency"><polygon points="107.636,1148.839,107.476,1140.296,101.899,1142.508,107.636,1148.839" fill="#666" style="stroke:#666;stroke-width:1;stroke-linejoin:miter;stroke-miterlimit:10;"/><path d="M100.137,813.093 C91.807,831.104 84.511,850.239 80,869.068 C60.428,950.756 64.339,975.541 80,1058.068 C85.765,1088.449 93.325,1112.745 104.687,1141.402" style="stroke:#666;stroke-width:1;" fill="none"/><text x="82" y="968.396" fill="#666" font-size="12" textLength="39.072" font-weight="700">stages</text><text x="121.072" y="968.396" fill="#666" font-size="12" font-weight="700"> </text><text x="124.192" y="968.396" fill="#666" font-size="12" textLength="56.028" font-weight="700">captures,</text><text x="180.221" y="968.396" fill="#666" font-size="12" font-weight="700"> </text><text x="183.341" y="968.396" fill="#666" font-size="12" textLength="37.644" font-weight="700">writes</text><text x="220.985" y="968.396" fill="#666" font-size="12" font-weight="700"> </text><text x="224.105" y="968.396" fill="#666" font-size="12" textLength="20.52" font-weight="700">hot</text><text x="244.625" y="968.396" fill="#666" font-size="12" font-weight="700"> </text><text x="247.745" y="968.396" fill="#666" font-size="12" textLength="28.44" font-weight="700">brief</text></g><g class="link" data-entity-1="ent0008" data-entity-2="ent0006" id="lnk16" data-link-type="dependency"><polygon points="343.273,1129.663,345.196,1121.338,339.248,1122.127,343.273,1129.663" fill="#666" style="stroke:#666;stroke-width:1;stroke-linejoin:miter;stroke-miterlimit:10;"/><path d="M352.12,1058.181 C349.736,1064.098 347.66,1070.083 346,1076.068 C341.203,1093.366 339.837,1103.75 342.222,1121.732" style="stroke:#666;stroke-width:1;" fill="none"/><text x="348" y="1090.896" fill="#666" font-size="12" textLength="24.144" font-weight="700">sole</text><text x="372.144" y="1090.896" fill="#666" font-size="12" font-weight="700"> </text><text x="375.264" y="1090.896" fill="#666" font-size="12" textLength="31.68" font-weight="700">write</text><text x="406.944" y="1090.896" fill="#666" font-size="12" font-weight="700"> </text><text x="410.064" y="1090.896" fill="#666" font-size="12" textLength="31.356" font-weight="700">path:</text><text x="441.421" y="1090.896" fill="#666" font-size="12" font-weight="700"> </text><text x="444.541" y="1090.896" fill="#666" font-size="12" textLength="39.48" font-weight="700">accept</text><text x="484.021" y="1090.896" fill="#666" font-size="12" font-weight="700"> </text><text x="487.141" y="1090.896" fill="#666" font-size="12" textLength="57.948" font-weight="700">promotes</text><text x="402.06" y="1107.24" fill="#666" font-size="12" textLength="40.704" font-weight="700">staged</text><text x="442.765" y="1107.24" fill="#666" font-size="12" font-weight="700"> </text><text x="445.885" y="1107.24" fill="#666" font-size="12" textLength="45.144" font-weight="700">records</text></g><g class="link" data-entity-1="ent0008" data-entity-2="ent0009" id="lnk17" data-link-type="dependency"><polygon points="120.108,1148.947,122.185,1140.659,116.223,1141.337,120.108,1148.947" fill="#666" style="stroke:#666;stroke-width:1;stroke-linejoin:miter;stroke-miterlimit:10;"/><path d="M286.926,1052.301 C282.298,1054.381 277.649,1056.314 273,1058.068 C244.682,1068.749 157.937,1053.297 138,1076.068 C120.556,1095.992 116.21,1114.653 119.204,1140.998" style="stroke:#666;stroke-width:1;" fill="none"/><text x="140" y="1098.896" fill="#666" font-size="12" textLength="48.9" font-weight="700">rebuilds</text><text x="188.9" y="1098.896" fill="#666" font-size="12" font-weight="700"> </text><text x="192.02" y="1098.896" fill="#666" font-size="12" textLength="33.18" font-weight="700">sqlite</text><text x="225.201" y="1098.896" fill="#666" font-size="12" font-weight="700"> </text><text x="228.321" y="1098.896" fill="#666" font-size="12" textLength="34.56" font-weight="700">cache</text><text x="262.881" y="1098.896" fill="#666" font-size="12" font-weight="700"> </text><text x="266.001" y="1098.896" fill="#666" font-size="12" textLength="29.304" font-weight="700">from</text><text x="295.305" y="1098.896" fill="#666" font-size="12" font-weight="700"> </text><text x="298.425" y="1098.896" fill="#666" font-size="12" textLength="38.484" font-weight="700">ledger</text></g><g class="link" data-entity-1="ent0008" data-entity-2="ent0010" id="lnk18" data-link-type="dependency"><polygon points="636.59,1191.76,633.462,1183.809,629.006,1187.826,636.59,1191.76" fill="#666" style="stroke:#666;stroke-width:1;stroke-linejoin:miter;stroke-miterlimit:10;"/><path d="M530.201,1058.227 C536.054,1064.088 541.696,1070.052 547,1076.068 C559.557,1090.309 558.588,1096.894 570,1112.068 C590.483,1139.303 609.685,1161.91 631.234,1185.817" style="stroke:#666;stroke-width:1;" fill="none"/><text x="572" y="1090.896" fill="#666" font-size="12" textLength="64.968" font-weight="700">re-projects</text><text x="636.968" y="1090.896" fill="#666" font-size="12" font-weight="700"> </text><text x="640.089" y="1090.896" fill="#666" font-size="12" textLength="38.484" font-weight="700">ledger</text><text x="608.312" y="1107.24" fill="#666" font-size="12" textLength="33.948" font-style="italic">[HTTP]</text></g><g class="link" data-entity-1="ent0001" data-entity-2="ent0010" id="lnk19" data-link-type="dependency"><polygon points="691.266,1191.993,694.429,1184.056,688.43,1183.933,691.266,1191.993" fill="#666" style="stroke:#666;stroke-width:1;stroke-linejoin:miter;stroke-miterlimit:10;"/><path d="M524.868,176.343 C602.942,225.778 694,305.257 694,407.068 C694,407.068 694,407.068 694,964.568 C694,1043.465 692.601,1126.68 691.429,1183.995" style="stroke:#666;stroke-width:1;" fill="none"/><text x="696" y="715.396" fill="#666" font-size="12" textLength="68.376" font-weight="700">on-demand</text><text x="764.376" y="715.396" fill="#666" font-size="12" font-weight="700"> </text><text x="767.497" y="715.396" fill="#666" font-size="12" textLength="35.772" font-weight="700">graph</text><text x="803.269" y="715.396" fill="#666" font-size="12" font-weight="700"> </text><text x="806.389" y="715.396" fill="#666" font-size="12" textLength="44.736" font-weight="700">queries</text><text x="758.112" y="731.74" fill="#666" font-size="12" textLength="30.9" font-style="italic">[MCP]</text></g><?plantuml-src RLLDSzis4BthLsnreXAHF3Izf2T7Ibftx5WJkzEZ1o9M96mGe05GiYREV-y3lwmere43Bh6xx-s-NVscJA5RoLI5qFYFUUx48IjLgDfjwzaLrWGs8IjidJTQcD3MElL1QhNDLn_IJJJzUtsLry87TZ3Th-0Xb66NDRWxcmKLDDDazPJIxVfgbTvkrckwSKN5FZWHh2DbV11QiyI913NEFd0HAF7uA4gc_U-BsMo34DOaU3TXIVEBIuIMofHq4Irpc5OjRZZAyQ5HsePuqY9In4sU8yRj4M3h-q-sDLAuO-AuiN3S30dZgrT8UfmlwFkCy9lW9qAA1anzpEsz0c8JQ3J6zDV2FKfxCEH3k_Ka-dZahEtGPKK7BqT1Qs6af_VaMis8Dy85xxe9qT2Z7czcT5UXC8rL9kP2aO8bBtMH-aRAF1ABKC_9LRPcv7szICdEYPeFrZrI_BYOMFxCDBRUYI8YcQ_6CoKHZWg1vUAKRJmxVcgLunfzy3buVRkvM5zVRB9QBgdBN0Yp5_v56A6FADsBuxtYWt_nHrCigHBEi8SdT7RSMlk88pz3VLvPuoE_Np2kjmdaKh93zYluuXxrXbXMBj4UTpo7_C_jpTUhUEWmrLoZqRdklRC7F3KaAeg2qLA9A8LrQ5CYkL0HoP8Q5NRGAGu2GHqi7jxTfv7Q8gFJw2EKANoXXQfXrYmy4-zZoTuYcFZ2gKuCy__Ghvss1rfTNKRqduIlEXRzZJmsEFFLHmfH9L9veCDmEO9AccEehA6qxlHopwP4y3-XKE52serIEb30i1JGSkjujAGz1vYrxkNMku-atuJTNnbbrxz5i9iEI3nrY-Kv3aTm2WN8VMKFkLGbra7ULpzlX5FXkAJ3G7sgOpuC0Ih_f5LWHDcsIaDdPQimCiFgY7mAhU9UU5iyhZL1TUEHHQ0v3GPAi05LQQmRDFD5GI1TjNlIFiSOkvEwGUGOfa1RaDajOsLBbcUg6WgBPYeZ-JaRiDicfEXO8T0jB3iyQq7xFnO3vX-p7zF6-lmSaeuXaFpLCLrVhZv7N5_PVdYWqeccEcN_lfFX_JkwNcqW1AkZocRVM8yBz6ITAREtZ-mfdrP5-cvQFd3hlARhhy848wltSKzr7LsEF9V348skVSBJhU9OOBBwhG2eJHM70P9KksxLI2oJ8kZZ463AE8bBOdvA4O3vKB4Px2JsGccnrNpkzzhjhZt-f24720s6gOE3wvXfMZ9WoltbEFmOjfvYjxDjZD0tVjmQPs5U0GnwzQD---Rl46N8TUOvDXrZENHtN7In-N_VtMr-hl9u7-ChEJPmaCPJow3N_q-jei__?></g></svg> \ No newline at end of file diff --git a/docs/diagrams/03-component-engine.puml b/docs/diagrams/03-component-engine.puml index 8434f482..819db3fc 100644 --- a/docs/diagrams/03-component-engine.puml +++ b/docs/diagrams/03-component-engine.puml @@ -4,13 +4,12 @@ title Components - sdlc-engine (Python, storage v3) Container_Boundary(engine, "sdlc-engine") { - Component(cli, "cli.py", "argparse CLI", "context retrieve|show|digest|accept|parity|persist-lesson, storage migrate|status, db, registry, workflow") - Component(project, "project.py", "Path resolution", "Project.home: SDLC_HOME > <root>/sdlc-spdd/ > root (legacy). All artifact paths derive from home") + Component(cli, "cli.py", "argparse CLI", "context retrieve|show|digest|accept|parity|persist-lesson, db, registry, workflow, session") + Component(project, "project.py", "Path resolution", "Project.home: SDLC_HOME > <root>/sdlc-spdd/. All artifact paths derive from home") Component(ledgermod, "lessons_ledger.py", "System of record", "LessonRecord (5 kinds) + LessonsLedger: stage, accept, records, digest. Validates schema, dedupes ids") Component(store, "context_store.py", "Orchestration", "persist_lesson stages by default; accept promotes; fan-out to sqlite + guide; parity check/repair") Component(registry, "registry.py", "Work registry", "Append-only claim/release events in registry.jsonl; legacy TSV read-only fallback") Component(db, "db.py", "Opt-in cache", "sqlite schema v5 at .sdlc/index.sqlite; rebuilt from ledger + registry + canvases + governance docs") - Component(migrate, "storage_migrate.py", "One-time migration", "Legacy agent-context trees -> ledger records + .sdlc/legacy-export") Component(workflow, "workflow.py", "Phase gates", "Progress gate: any ledger record for work id. Retro gate: decision/pitfall/pattern present") Component(guideops, "installer/guide_ops.py", "Guide integration", "Projection trigger, RAG ops, ops console actions") } @@ -21,7 +20,6 @@ ContainerDb_Ext(sqlite, ".sdlc/index.sqlite", "Opt-in cache") System_Ext(guide, "Guide DICE", "Neo4j projection") Rel(cli, store, "commands") -Rel(cli, migrate, "storage migrate") Rel(store, ledgermod, "stage / accept / read") Rel(store, db, "upsert lesson records (when sqlite backend on)") Rel(store, guideops, "re-project after accept") diff --git a/docs/diagrams/03-component-engine.svg b/docs/diagrams/03-component-engine.svg index b0b208db..76573a6a 100644 --- a/docs/diagrams/03-component-engine.svg +++ b/docs/diagrams/03-component-engine.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" contentStyleType="text/css" data-diagram-type="DESCRIPTION" height="921px" preserveAspectRatio="none" style="width:1158px;height:921px;background:#FFFFFF;" version="1.1" viewBox="0 0 1158 921" width="1158px" zoomAndPan="magnify"><?plantuml 1.2026.6?><defs/><g><g class="title" data-source-line="3"><text fill="#000000" font-family="sans-serif" font-size="14" font-weight="700" lengthAdjust="spacing" textLength="372.8867" x="389.2559" y="22.9951">Components - sdlc-engine (Python, storage v3)</text></g><!--cluster engine--><g class="cluster" data-qualified-name="engine" data-source-line="1690" id="ent0001"><rect fill="none" height="711" style="stroke:#444444;stroke-width:1;stroke-dasharray:7,7;" width="996" x="10" y="44.2969"/><text fill="#444444" font-family="sans-serif" font-size="16" font-weight="700" lengthAdjust="spacing" textLength="104.0078" x="455.9961" y="81.1484">sdlc-engine</text><text fill="#444444" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="75.416" x="470.292" y="96.0605">[container]</text></g><!--entity cli--><g class="entity" data-qualified-name="engine.cli" data-source-line="83" id="ent0002"><rect fill="#85BBF0" height="148.0469" style="stroke:#78A8D8;stroke-width:0.5;" width="366.9443" x="355.5" y="115.2969"/><text fill="#000000" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="82.5703" x="497.687" y="136.4355">«component»</text><text fill="#000000" font-family="sans-serif" font-size="16" font-weight="700" lengthAdjust="spacing" textLength="48.4141" x="514.7651" y="154.1172">cli.py</text><text fill="#000000" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="58.125" x="496.3599" y="169.0293">[argparse</text><text fill="#000000" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="3.8145" x="554.4849" y="169.0293"> </text><text fill="#000000" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="23.2852" x="558.2993" y="169.0293">CLI]</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="536.7471" y="184.8545"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="53.0127" x="512.4658" y="201.1514">context</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="346.9443" x="365.5" y="217.4482">retrieve|show|digest|accept|parity|persist-lesson,</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="53.1836" x="443.2998" y="233.7451">storage</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="496.4834" y="233.7451"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="107.0371" x="500.9336" y="233.7451">migrate|status,</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="607.9707" y="233.7451"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="22.2236" x="612.4209" y="233.7451">db,</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="58.4199" x="476.1807" y="250.042">registry,</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="534.6006" y="250.042"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="62.7129" x="539.0508" y="250.042">workflow</text></g><!--entity project--><g class="entity" data-qualified-name="engine.project" data-source-line="83" id="ent0003"><rect fill="#85BBF0" height="148.0469" style="stroke:#78A8D8;stroke-width:0.5;" width="213.2314" x="273.5" y="583.2969"/><text fill="#000000" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="82.5703" x="338.8306" y="604.4355">«component»</text><text fill="#000000" font-family="sans-serif" font-size="16" font-weight="700" lengthAdjust="spacing" textLength="91.7656" x="334.2329" y="622.1172">project.py</text><text fill="#000000" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="31.582" x="330.1587" y="637.0293">[Path</text><text fill="#000000" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="3.8145" x="361.7407" y="637.0293"> </text><text fill="#000000" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="64.5176" x="365.5552" y="637.0293">resolution]</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="377.8906" y="652.8545"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="97.3096" x="285.8789" y="669.1514">Project.home:</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="383.1885" y="669.1514"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="86.7139" x="387.6387" y="669.1514">SDLC_HOME</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="11.7305" x="283.5" y="685.4482">></text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="295.2305" y="685.4482"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="128.0439" x="299.6807" y="685.4482"><root>/sdlc-spdd/</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="427.7246" y="685.4482"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="11.7305" x="432.1748" y="685.4482">></text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="443.9053" y="685.4482"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="28.376" x="448.3555" y="685.4482">root</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="61.3252" x="289.3345" y="701.7451">(legacy).</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="350.6597" y="701.7451"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="17.3564" x="355.1099" y="701.7451">All</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="372.4663" y="701.7451"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="50.4082" x="376.9165" y="701.7451">artifact</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="427.3247" y="701.7451"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="39.1221" x="431.7749" y="701.7451">paths</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="44.0439" x="317.355" y="718.042">derive</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="361.3989" y="718.042"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="32.8877" x="365.8491" y="718.042">from</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="398.7368" y="718.042"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="39.6895" x="403.187" y="718.042">home</text></g><!--entity ledgermod--><g class="entity" data-qualified-name="engine.ledgermod" data-source-line="83" id="ent0004"><rect fill="#85BBF0" height="164.3438" style="stroke:#78A8D8;stroke-width:0.5;" width="212.8965" x="25.5" y="575.2969"/><text fill="#000000" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="82.5703" x="90.6631" y="596.4355">«component»</text><text fill="#000000" font-family="sans-serif" font-size="16" font-weight="700" lengthAdjust="spacing" textLength="161.2344" x="51.3311" y="614.1172">lessons_ledger.py</text><text fill="#000000" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="49.4297" x="75.8916" y="629.0293">[System</text><text fill="#000000" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="3.8145" x="125.3213" y="629.0293"> </text><text fill="#000000" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="11.5664" x="129.1357" y="629.0293">of</text><text fill="#000000" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="3.8145" x="140.7021" y="629.0293"> </text><text fill="#000000" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="43.4883" x="144.5166" y="629.0293">record]</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="129.7231" y="644.8545"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="97.6855" x="42.124" y="661.1514">LessonRecord</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="139.8096" y="661.1514"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="14.3691" x="144.2598" y="661.1514">(5</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="158.6289" y="661.1514"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="42.5127" x="163.0791" y="661.1514">kinds)</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="205.5918" y="661.1514"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="11.7305" x="210.042" y="661.1514">+</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="109.0059" x="53.564" y="677.4482">LessonsLedger:</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="162.5698" y="677.4482"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="43.3125" x="167.02" y="677.4482">stage,</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="51.4131" x="49.5273" y="693.7451">accept,</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="100.9404" y="693.7451"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="57.0186" x="105.3906" y="693.7451">records,</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="162.4092" y="693.7451"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="47.5098" x="166.8594" y="693.7451">digest.</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="64.7979" x="35.5" y="710.042">Validates</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="100.2979" y="710.042"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="59.1445" x="104.748" y="710.042">schema,</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="163.8926" y="710.042"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="60.0537" x="168.3428" y="710.042">dedupes</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="20.0703" x="121.9131" y="726.3389">ids</text></g><!--entity store--><g class="entity" data-qualified-name="engine.store" data-source-line="83" id="ent0005"><rect fill="#85BBF0" height="148.0469" style="stroke:#78A8D8;stroke-width:0.5;" width="201.3643" x="382.5" y="338.2969"/><text fill="#000000" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="82.5703" x="441.897" y="359.4355">«component»</text><text fill="#000000" font-family="sans-serif" font-size="16" font-weight="700" lengthAdjust="spacing" textLength="151.2031" x="407.5806" y="377.1172">context_store.py</text><text fill="#000000" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="91.5586" x="437.4028" y="392.0293">[Orchestration]</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="480.957" y="407.8545"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="98.752" x="397.6919" y="424.1514">persist_lesson</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="496.4438" y="424.1514"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="46.1563" x="500.894" y="424.1514">stages</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="547.0503" y="424.1514"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="17.1719" x="551.5005" y="424.1514">by</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="53.9766" x="392.5" y="440.4482">default;</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="446.4766" y="440.4482"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="46.9629" x="450.9268" y="440.4482">accept</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="497.8896" y="440.4482"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="71.5244" x="502.3398" y="440.4482">promotes;</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="50.3604" x="395.2446" y="456.7451">fan-out</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="445.605" y="456.7451"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="14.0547" x="450.0552" y="456.7451">to</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="464.1099" y="456.7451"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="38.0625" x="468.5601" y="456.7451">sqlite</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="506.6226" y="456.7451"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="11.7305" x="511.0728" y="456.7451">+</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="522.8032" y="456.7451"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="43.8662" x="527.2534" y="456.7451">guide;</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="40.8857" x="416.9214" y="473.042">parity</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="457.8071" y="473.042"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="87.1855" x="462.2573" y="473.042">check/repair</text></g><!--entity registry--><g class="entity" data-qualified-name="engine.registry" data-source-line="83" id="ent0006"><rect fill="#85BBF0" height="148.0469" style="stroke:#78A8D8;stroke-width:0.5;" width="206.6074" x="782.5" y="115.2969"/><text fill="#000000" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="82.5703" x="844.5186" y="136.4355">«component»</text><text fill="#000000" font-family="sans-serif" font-size="16" font-weight="700" lengthAdjust="spacing" textLength="99.1328" x="836.2373" y="154.1172">registry.py</text><text fill="#000000" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="35.7715" x="840.54" y="169.0293">[Work</text><text fill="#000000" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="3.8145" x="876.3115" y="169.0293"> </text><text fill="#000000" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="50.9414" x="880.126" y="169.0293">registry]</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="883.5786" y="184.8545"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="88.3887" x="792.5" y="201.1514">Append-only</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="880.8887" y="201.1514"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="93.7686" x="885.3389" y="201.1514">claim/release</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="47.168" x="803.564" y="217.4482">events</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="850.7319" y="217.4482"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="12.7627" x="855.1821" y="217.4482">in</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="867.9448" y="217.4482"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="95.6484" x="872.395" y="217.4482">registry.jsonl;</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="45.9512" x="811.6201" y="233.7451">legacy</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="857.5713" y="233.7451"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="27.0156" x="862.0215" y="233.7451">TSV</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="889.0371" y="233.7451"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="66.5" x="893.4873" y="233.7451">read-only</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="54.5576" x="858.5249" y="250.042">fallback</text></g><!--entity db--><g class="entity" data-qualified-name="engine.db" data-source-line="83" id="ent0007"><rect fill="#85BBF0" height="164.3438" style="stroke:#78A8D8;stroke-width:0.5;" width="192.7646" x="521.5" y="575.2969"/><text fill="#000000" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="82.5703" x="576.5972" y="596.4355">«component»</text><text fill="#000000" font-family="sans-serif" font-size="16" font-weight="700" lengthAdjust="spacing" textLength="50.8672" x="592.4487" y="614.1172">db.py</text><text fill="#000000" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="41.7188" x="575.0063" y="629.0293">[Opt-in</text><text fill="#000000" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="3.8145" x="616.7251" y="629.0293"> </text><text fill="#000000" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="40.2188" x="620.5396" y="629.0293">cache]</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="615.6572" y="644.8545"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="38.0625" x="549.1982" y="661.1514">sqlite</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="587.2607" y="661.1514"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="54.6943" x="591.7109" y="661.1514">schema</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="646.4053" y="661.1514"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="17.1924" x="650.8555" y="661.1514">v5</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="668.0479" y="661.1514"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="14.0684" x="672.498" y="661.1514">at</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="122.7119" x="531.6025" y="677.4482">.sdlc/index.sqlite;</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="654.3145" y="677.4482"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="45.3975" x="658.7646" y="677.4482">rebuilt</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="32.8877" x="531.5" y="693.7451">from</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="564.3877" y="693.7451"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="44.6455" x="568.8379" y="693.7451">ledger</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="613.4834" y="693.7451"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="11.7305" x="617.9336" y="693.7451">+</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="629.6641" y="693.7451"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="53.9697" x="634.1143" y="693.7451">registry</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="688.084" y="693.7451"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="11.7305" x="692.5342" y="693.7451">+</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="65.2148" x="533.5884" y="710.042">canvases</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="598.8032" y="710.042"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="11.7305" x="603.2534" y="710.042">+</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="614.9839" y="710.042"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="82.7422" x="619.4341" y="710.042">governance</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="32.4434" x="601.6606" y="726.3389">docs</text></g><!--entity migrate--><g class="entity" data-qualified-name="engine.migrate" data-source-line="83" id="ent0008"><rect fill="#85BBF0" height="131.75" style="stroke:#78A8D8;stroke-width:0.5;" width="213.0332" x="618.5" y="346.2969"/><text fill="#000000" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="82.5703" x="683.7314" y="367.4355">«component»</text><text fill="#000000" font-family="sans-serif" font-size="16" font-weight="700" lengthAdjust="spacing" textLength="175.9141" x="637.0596" y="385.1172">storage_migrate.py</text><text fill="#000000" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="60.5566" x="661.5332" y="400.0293">[One-time</text><text fill="#000000" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="3.8145" x="722.0898" y="400.0293"> </text><text fill="#000000" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="62.5957" x="725.9043" y="400.0293">migration]</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="722.7915" y="415.8545"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="49.8613" x="628.5" y="432.1514">Legacy</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="678.3613" y="432.1514"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="98.5059" x="682.8115" y="432.1514">agent-context</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="781.3174" y="432.1514"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="35.7656" x="785.7676" y="432.1514">trees</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="16.7822" x="655.478" y="448.4482">-></text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="672.2603" y="448.4482"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="44.6455" x="676.7104" y="448.4482">ledger</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="721.356" y="448.4482"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="52.5684" x="725.8062" y="448.4482">records</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="778.3745" y="448.4482"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="11.7305" x="782.8247" y="448.4482">+</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="133.5332" x="658.25" y="464.7451">.sdlc/legacy-export</text></g><!--entity workflow--><g class="entity" data-qualified-name="engine.workflow" data-source-line="83" id="ent0009"><rect fill="#85BBF0" height="148.0469" style="stroke:#78A8D8;stroke-width:0.5;" width="219.1172" x="26.5" y="338.2969"/><text fill="#000000" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="82.5703" x="94.7734" y="359.4355">«component»</text><text fill="#000000" font-family="sans-serif" font-size="16" font-weight="700" lengthAdjust="spacing" textLength="110.4844" x="80.8164" y="377.1172">workflow.py</text><text fill="#000000" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="40.5117" x="94.8994" y="392.0293">[Phase</text><text fill="#000000" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="3.8145" x="135.4111" y="392.0293"> </text><text fill="#000000" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="37.9922" x="139.2256" y="392.0293">gates]</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="133.8335" y="407.8545"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="60.6074" x="45.7456" y="424.1514">Progress</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="106.353" y="424.1514"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="36.2852" x="110.8032" y="424.1514">gate:</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="147.0884" y="424.1514"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="25.7373" x="151.5386" y="424.1514">any</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="177.2759" y="424.1514"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="44.6455" x="181.7261" y="424.1514">ledger</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="45.2744" x="50.2676" y="440.4482">record</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="95.542" y="440.4482"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="19.25" x="99.9922" y="440.4482">for</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="119.2422" y="440.4482"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="33.8789" x="123.6924" y="440.4482">work</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="157.5713" y="440.4482"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="17.2266" x="162.0215" y="440.4482">id.</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="179.248" y="440.4482"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="38.1514" x="183.6982" y="440.4482">Retro</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="36.2852" x="36.5" y="456.7451">gate:</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="72.7852" y="456.7451"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="158.3818" x="77.2354" y="456.7451">decision/pitfall/pattern</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="53.5254" x="109.2959" y="473.042">present</text></g><!--entity guideops--><g class="entity" data-qualified-name="engine.guideops" data-source-line="83" id="ent0010"><rect fill="#85BBF0" height="115.4531" style="stroke:#78A8D8;stroke-width:0.5;" width="218.9609" x="749.5" y="599.7969"/><text fill="#000000" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="82.5703" x="817.6953" y="620.9355">«component»</text><text fill="#000000" font-family="sans-serif" font-size="16" font-weight="700" lengthAdjust="spacing" textLength="198.9609" x="759.5" y="638.6172">installer/guide_ops.py</text><text fill="#000000" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="39.9199" x="801.8135" y="653.5293">[Guide</text><text fill="#000000" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="3.8145" x="841.7334" y="653.5293"> </text><text fill="#000000" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="70.5996" x="845.5479" y="653.5293">integration]</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="856.7554" y="669.3545"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="69.7813" x="761.876" y="685.6514">Projection</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="831.6572" y="685.6514"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="51.7275" x="836.1074" y="685.6514">trigger,</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="887.835" y="685.6514"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="30.1533" x="892.2852" y="685.6514">RAG</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="922.4385" y="685.6514"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="29.1963" x="926.8887" y="685.6514">ops,</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="24.7461" x="790.2144" y="701.9482">ops</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="814.9604" y="701.9482"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="53.498" x="819.4106" y="701.9482">console</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="872.9087" y="701.9482"> </text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="50.3877" x="877.3589" y="701.9482">actions</text></g><!--entity ledger--><g class="entity" data-qualified-name="ledger" data-source-line="92" id="ent0011"><path d="M927,830.7969 C927,820.7969 1030.1992,820.7969 1030.1992,820.7969 C1030.1992,820.7969 1133.3984,820.7969 1133.3984,830.7969 L1133.3984,886.3594 C1133.3984,896.3594 1030.1992,896.3594 1030.1992,896.3594 C1030.1992,896.3594 927,896.3594 927,886.3594 L927,830.7969" fill="#B3B3B3" style="stroke:#A6A6A6;stroke-width:0.5;"/><path d="M927,830.7969 C927,840.7969 1030.1992,840.7969 1030.1992,840.7969 C1030.1992,840.7969 1133.3984,840.7969 1133.3984,830.7969" fill="none" style="stroke:#A6A6A6;stroke-width:0.5;"/><text fill="#FFFFFF" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="127.3418" x="966.5283" y="855.9355">«external_container»</text><text fill="#FFFFFF" font-family="sans-serif" font-size="16" font-weight="700" lengthAdjust="spacing" textLength="186.3984" x="937" y="873.6172">spdd/memory/*.jsonl</text><text fill="#FFFFFF" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="71.5254" x="972.0566" y="888.5293">[Committed</text><text fill="#FFFFFF" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="3.8145" x="1043.582" y="888.5293"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="40.9453" x="1047.3965" y="888.5293">JSONL]</text></g><!--entity staged--><g class="entity" data-qualified-name="staged" data-source-line="92" id="ent0012"><path d="M6,830.7969 C6,820.7969 131.8398,820.7969 131.8398,820.7969 C131.8398,820.7969 257.6797,820.7969 257.6797,830.7969 L257.6797,886.3594 C257.6797,896.3594 131.8398,896.3594 131.8398,896.3594 C131.8398,896.3594 6,896.3594 6,886.3594 L6,830.7969" fill="#B3B3B3" style="stroke:#A6A6A6;stroke-width:0.5;"/><path d="M6,830.7969 C6,840.7969 131.8398,840.7969 131.8398,840.7969 C131.8398,840.7969 257.6797,840.7969 257.6797,830.7969" fill="none" style="stroke:#A6A6A6;stroke-width:0.5;"/><text fill="#FFFFFF" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="127.3418" x="68.1689" y="855.9355">«external_container»</text><text fill="#FFFFFF" font-family="sans-serif" font-size="16" font-weight="700" lengthAdjust="spacing" textLength="231.6797" x="16" y="873.6172">.sdlc/staged/lessons.jsonl</text><text fill="#FFFFFF" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="67.8516" x="77.0107" y="888.5293">[Gitignored</text><text fill="#FFFFFF" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="3.8145" x="144.8623" y="888.5293"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="37.9922" x="148.6768" y="888.5293">stage]</text></g><!--entity sqlite--><g class="entity" data-qualified-name="sqlite" data-source-line="92" id="ent0013"><path d="M531,830.7969 C531,820.7969 617.9453,820.7969 617.9453,820.7969 C617.9453,820.7969 704.8906,820.7969 704.8906,830.7969 L704.8906,886.3594 C704.8906,896.3594 617.9453,896.3594 617.9453,896.3594 C617.9453,896.3594 531,896.3594 531,886.3594 L531,830.7969" fill="#B3B3B3" style="stroke:#A6A6A6;stroke-width:0.5;"/><path d="M531,830.7969 C531,840.7969 617.9453,840.7969 617.9453,840.7969 C617.9453,840.7969 704.8906,840.7969 704.8906,830.7969" fill="none" style="stroke:#A6A6A6;stroke-width:0.5;"/><text fill="#FFFFFF" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="127.3418" x="554.2744" y="855.9355">«external_container»</text><text fill="#FFFFFF" font-family="sans-serif" font-size="16" font-weight="700" lengthAdjust="spacing" textLength="153.8906" x="541" y="873.6172">.sdlc/index.sqlite</text><text fill="#FFFFFF" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="41.7188" x="575.0693" y="888.5293">[Opt-in</text><text fill="#FFFFFF" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="3.8145" x="616.7881" y="888.5293"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="40.2188" x="620.6025" y="888.5293">cache]</text></g><!--entity guide--><g class="entity" data-qualified-name="guide" data-source-line="419" id="ent0014"><rect fill="#999999" height="85.1875" style="stroke:#8A8A8A;stroke-width:0.5;" width="135.8652" x="756" y="816.2969"/><text fill="#FFFFFF" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="113.8652" x="767" y="837.4355">«external_system»</text><text fill="#FFFFFF" font-family="sans-serif" font-size="16" font-weight="700" lengthAdjust="spacing" textLength="52.3125" x="774.0381" y="855.1172">Guide</text><text fill="#FFFFFF" font-family="sans-serif" font-size="16" font-weight="700" lengthAdjust="spacing" textLength="5.5703" x="826.3506" y="855.1172"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="16" font-weight="700" lengthAdjust="spacing" textLength="41.9063" x="831.9209" y="855.1172">DICE</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="821.7075" y="871.8857"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="40.4482" x="766.3706" y="888.1826">Neo4j</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4.4502" x="806.8188" y="888.1826"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="70.2256" x="811.269" y="888.1826">projection</text></g><!--link cli to store--><g class="link" data-entity-1="ent0002" data-entity-2="ent0005" data-link-type="dependency" data-source-line="1803" id="lnk15"><path d="M520.43,263.5769 C514.35,287.5769 509.5654,306.4721 503.4854,330.4621" fill="none" id="cli-to-store" style="stroke:#666666;stroke-width:1;"/><polygon fill="#666666" points="501.52,338.2169,506.3934,331.1991,500.5773,329.725,501.52,338.2169" style="stroke:#666666;stroke-width:1;stroke-linejoin:miter;stroke-miterlimit:10;"/><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="72.7383" x="513" y="305.4355">commands</text></g><!--link cli to migrate--><g class="link" data-entity-1="ent0002" data-entity-2="ent0008" data-link-type="dependency" data-source-line="1803" id="lnk16"><path d="M600.68,263.5769 C623.1,290.2169 643.1084,313.9964 664.9084,339.8964" fill="none" id="cli-to-migrate" style="stroke:#666666;stroke-width:1;"/><polygon fill="#666666" points="670.06,346.0169,667.2036,337.9645,662.6132,341.8282,670.06,346.0169" style="stroke:#666666;stroke-width:1;stroke-linejoin:miter;stroke-miterlimit:10;"/><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="51.8672" x="638" y="305.4355">storage</text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="4.1777" x="689.8672" y="305.4355"> </text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="53.0977" x="694.0449" y="305.4355">migrate</text></g><!--link store to ledgermod--><g class="link" data-entity-1="ent0005" data-entity-2="ent0004" data-link-type="dependency" data-source-line="1803" id="lnk17"><path d="M382.37,444.1669 C337.29,461.0169 285.14,484.9269 244,516.2969 C222.47,532.7169 207.6073,547.8838 190.8073,569.0238" fill="none" id="store-to-ledgermod" style="stroke:#666666;stroke-width:1;"/><polygon fill="#666666" points="185.83,575.2869,193.156,570.8903,188.4586,567.1573,185.83,575.2869" style="stroke:#666666;stroke-width:1;stroke-linejoin:miter;stroke-miterlimit:10;"/><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="37.7051" x="245" y="535.4355">stage</text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="4.1777" x="282.7051" y="535.4355"> </text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="4.3828" x="286.8828" y="535.4355">/</text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="4.1777" x="291.2656" y="535.4355"> </text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="44.7891" x="295.4434" y="535.4355">accept</text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="4.1777" x="340.2324" y="535.4355"> </text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="4.3828" x="344.4102" y="535.4355">/</text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="4.1777" x="348.793" y="535.4355"> </text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="30.7441" x="352.9707" y="535.4355">read</text></g><!--link store to db--><g class="link" data-entity-1="ent0005" data-entity-2="ent0007" data-link-type="dependency" data-source-line="1803" id="lnk18"><path d="M542.91,486.4469 C549.76,496.2069 556.32,506.3069 562,516.2969 C572.42,534.6069 578.82,547.8347 586.82,567.5347" fill="none" id="store-to-db" style="stroke:#666666;stroke-width:1;"/><polygon fill="#666666" points="589.83,574.9469,589.5995,566.406,584.0404,568.6635,589.83,574.9469" style="stroke:#666666;stroke-width:1;stroke-linejoin:miter;stroke-miterlimit:10;"/><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="44.0684" x="576" y="528.4355">upsert</text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="4.1777" x="620.0684" y="528.4355"> </text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="43.3242" x="624.2461" y="528.4355">lesson</text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="4.1777" x="667.5703" y="528.4355"> </text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="51.0645" x="671.748" y="528.4355">records</text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="4.1777" x="722.8125" y="528.4355"> </text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="41.7949" x="726.9902" y="528.4355">(when</text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="37.834" x="609.6357" y="542.4043">sqlite</text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="4.1777" x="647.4697" y="542.4043"> </text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="57.0527" x="651.6475" y="542.4043">backend</text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="4.1777" x="708.7002" y="542.4043"> </text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="22.2715" x="712.8779" y="542.4043">on)</text></g><!--link store to guideops--><g class="link" data-entity-1="ent0005" data-entity-2="ent0010" data-link-type="dependency" data-source-line="1803" id="lnk19"><path d="M583.75,478.9869 C589.48,481.6569 595.25,484.1269 601,486.2969 C675.22,514.3569 711.73,471.1769 777,516.2969 C805.96,536.3169 822.9298,562.8613 836.0498,592.2613" fill="none" id="store-to-guideops" style="stroke:#666666;stroke-width:1;"/><polygon fill="#666666" points="839.31,599.5669,838.7894,591.0387,833.3102,593.4839,839.31,599.5669" style="stroke:#666666;stroke-width:1;stroke-linejoin:miter;stroke-miterlimit:10;"/><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="66.8906" x="806" y="535.4355">re-project</text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="4.1777" x="872.8906" y="535.4355"> </text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="33.1113" x="877.0684" y="535.4355">after</text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="4.1777" x="910.1797" y="535.4355"> </text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="44.7891" x="914.3574" y="535.4355">accept</text></g><!--link ledgermod to staged--><g class="link" data-entity-1="ent0004" data-entity-2="ent0012" data-link-type="dependency" data-source-line="1803" id="lnk20"><path d="M132,739.4369 C132,767.5969 132,789.8069 132,812.6569" fill="none" id="ledgermod-to-staged" style="stroke:#666666;stroke-width:1;"/><polygon fill="#666666" points="132,820.6569,135,812.6569,129,812.6569,132,820.6569" style="stroke:#666666;stroke-width:1;stroke-linejoin:miter;stroke-miterlimit:10;"/><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="59.2793" x="133" y="783.4355">captures</text></g><!--link ledgermod to ledger--><g class="link" data-entity-1="ent0004" data-entity-2="ent0011" data-link-type="dependency" data-source-line="1803" id="lnk21"><path d="M214.51,739.4169 C227.58,748.8169 241.58,757.1969 256,763.2969 C524.61,876.8269 624.33,757.7069 910,816.2969 C916.15,817.5569 914.6956,817.0124 920.9756,818.6724" fill="none" id="ledgermod-to-ledger" style="stroke:#666666;stroke-width:1;"/><polygon fill="#666666" points="928.71,820.7169,921.7423,815.7721,920.209,821.5728,928.71,820.7169" style="stroke:#666666;stroke-width:1;stroke-linejoin:miter;stroke-miterlimit:10;"/><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="61.5176" x="319" y="783.4355">accepted</text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="4.1777" x="380.5176" y="783.4355"> </text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="51.0645" x="384.6953" y="783.4355">records</text></g><!--link registry to ledger--><g class="link" data-entity-1="ent0006" data-entity-2="ent0011" data-link-type="dependency" data-source-line="1803" id="lnk22"><path d="M907.37,263.5969 C929.29,340.6769 963.3,465.7469 986,575.2969 C1004.08,662.5369 1017.4218,758.3919 1024.3818,812.6319" fill="none" id="registry-to-ledger" style="stroke:#666666;stroke-width:1;"/><polygon fill="#666666" points="1025.4,820.5669,1027.3574,812.2501,1021.4062,813.0138,1025.4,820.5669" style="stroke:#666666;stroke-width:1;stroke-linejoin:miter;stroke-miterlimit:10;"/><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="90.0117" x="980" y="535.4355">claim/release</text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="4.1777" x="1070.0117" y="535.4355"> </text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="45.5215" x="1074.1895" y="535.4355">events</text></g><!--link db to sqlite--><g class="link" data-entity-1="ent0007" data-entity-2="ent0013" data-link-type="dependency" data-source-line="1803" id="lnk23"><path d="M618,739.4369 C618,767.5969 618,789.8069 618,812.6569" fill="none" id="db-to-sqlite" style="stroke:#666666;stroke-width:1;"/><polygon fill="#666666" points="618,820.6569,621,812.6569,615,812.6569,618,820.6569" style="stroke:#666666;stroke-width:1;stroke-linejoin:miter;stroke-miterlimit:10;"/><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="48.0059" x="619" y="783.4355">rebuild</text></g><!--link guideops to guide--><g class="link" data-entity-1="ent0010" data-entity-2="ent0014" data-link-type="dependency" data-source-line="1803" id="lnk24"><path d="M849.06,714.9369 C843.41,747.1869 837.8222,779.0072 832.6722,808.3672" fill="none" id="guideops-to-guide" style="stroke:#666666;stroke-width:1;"/><polygon fill="#666666" points="831.29,816.2469,835.6271,808.8855,829.7173,807.8489,831.29,816.2469" style="stroke:#666666;stroke-width:1;stroke-linejoin:miter;stroke-miterlimit:10;"/><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="35.209" x="839" y="783.4355">HTTP</text></g><!--link workflow to ledgermod--><g class="link" data-entity-1="ent0009" data-entity-2="ent0004" data-link-type="dependency" data-source-line="1803" id="lnk25"><path d="M71.99,486.5769 C66.31,496.0869 61.43,506.0869 58,516.2969 C51.39,535.9769 51.8069,548.9351 60.3469,567.7051" fill="none" id="workflow-to-ledgermod" style="stroke:#666666;stroke-width:1;"/><polygon fill="#666666" points="63.66,574.9869,63.0776,566.4627,57.6163,568.9475,63.66,574.9869" style="stroke:#666666;stroke-width:1;stroke-linejoin:miter;stroke-miterlimit:10;"/><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="37.7051" x="59" y="535.4355">gates</text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="4.1777" x="96.7051" y="535.4355"> </text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="30.7441" x="100.8828" y="535.4355">read</text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="4.1777" x="131.627" y="535.4355"> </text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="43.4883" x="135.8047" y="535.4355">ledger</text></g><!--link store to project--><g class="link" data-entity-1="ent0005" data-entity-2="ent0003" data-link-type="dependency" data-source-line="1803" id="lnk26"><path d="M416.56,486.4769 C410.18,496.0369 404.44,506.0869 400,516.2969 C390.99,537.0169 386.9054,552.9829 384.0454,575.1129" fill="none" id="store-to-project" style="stroke:#666666;stroke-width:1;"/><polygon fill="#666666" points="383.02,583.0469,387.0206,575.4974,381.0701,574.7283,383.02,583.0469" style="stroke:#666666;stroke-width:1;stroke-linejoin:miter;stroke-miterlimit:10;"/><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="38.1094" x="401" y="535.4355">paths</text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="4.1777" x="439.1094" y="535.4355"> </text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="20.0332" x="443.2871" y="535.4355">via</text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="4.1777" x="463.3203" y="535.4355"> </text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="700" lengthAdjust="spacing" textLength="90.0469" x="467.498" y="535.4355">Project.home</text></g><?plantuml-src RLLDSzis4BthLsnrabfHF3IvM1tFkBB7IKU9FRQR7ZKGiQ9WWm0AWB8rKVzx5n-aw2WwQ8Z5Ol7sxTl5Bq9LikK8E--DkoXBorxdjV2xTjCwj9LM7fMVLxefZMJAjuqidETIRChbX-8-cFx-iYeRvZnQCgsNkZ5QXJE6V4SZBxn4wAqE2d1SLWMgMYY4oVt1xxIQWVFQiXfX__jqD5hIjOosxVfFtIhEx66I_6Sm7fmUJ-7x2EZNXvzKKf0F_S_DOKnVpDQ6MKS8Lf_3EYRqvi6YjmBtU7Gx_NhaeaRdZwogqFWZ7H3-S3HedN2-aEZS464ZQiiydVJCjsu6V3EZS3MvsiCCNhLzsKhzEfx-WCnO_OoL9mpvAoEyPtv7vvsMhHTQHLDssEa6B-3nUhLSVxhxSWENy8VLsb-MaGDdE2_95ammaLYpwZ2TmvMKmAmNMrPvC1JS0KShzWXRgni8CS-mIUGrsaPpkZobwzR9ba4-7gY-3UWj8QsqvS6sYeuFSGsJZ_0Y57TJ-0tIXbl501V46x4sWqJkB0S8l4NIv_2DIS69K0Uksc73Q0TvQsWjk3k36gg0fqAkupgZlBCKWCh0EY9p2TSffuJ4mUP0DsnPA_qYem8gIQC9mWAsJ1MwzU0rk7-by4W9rQtWk82a2w0xgfVIec72di4x2M7SVMPm_v0mUfq4mvKng7YXbQIOaec6Oafaf5NIPUWKeNh_-JFXbmj8LOQdns-qnVBXBPDomwgNCp11cME-wTWnlg2G5QC4mZgdbqY7_KTW7kP1MIKL4j_cQNz15srQ8NsIJr85ST8Xey-AgJt1Te4flKUhcAeGkAxEgvTx9zoUkccTBHr6XOKNJTTakOohb3MvArzq7UmjqfN5PGSfwuf0f2GILGM-6Mtz6P2kJIbwzzdruowKe0w2pBrOKtUwQBa0fWxlBuIjjh7fIQrpUA2nehChnqeuog0qmeSIbTICD2GLYGqTOJZ356Mc3RN6M2ZIgfGqKgDnJTOCxpQiIHeU1mJbWK4heaNKX6u63rUt48FH7n1fD62Gv1wSGbt-6qpPwytwvgsR0g4sOR8qs6XxA7zDqWkt4D96K0eS_dgy-xgY83z6Y0qMXaWgGLgMUQAS0jqABsf5VSjJI_uiKXHV7sceo3CjJqTfFCM3aQ-Uf-lFovlW_nNrXsSmFKrqPlI0CZqNfv7ID4p5cTFldGcsisIdV7OuG6DAK7QZfOoT-juzDcPhw87ra6TJ9z_9wmvLDtf2LzEC0AscxmCCf6Ano7a1svA-yhtPVm2iBqx5Z6z9W-SklG9I3AfFXfLTJzEjz_pPvChU8Sk-ZccAT3oSy7Vr-lJqT9ytJxqvf3LsPEGoczypCdXUuvkt5mo6xoXv_my0?></g></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" data-diagram-type="DESCRIPTION" style="width:1120px;height:930px;background:#FFFFFF;" width="1120px" height="930px" viewBox="0 0 1120 930" zoomAndPan="magnify" preserveAspectRatio="none" contentStyleType="text/css"><?plantuml 1.2026.8?><defs/><g font-family="sans-serif" lengthAdjust="spacing"><g class="title" data-source-line="3"><text x="394.56" y="24.966" fill="#000" font-size="14" textLength="324.141" font-weight="700">Components - sdlc-engine (Python, storage v3)</text></g><!--cluster engine--><g class="cluster" data-qualified-name="engine" id="ent0001" data-source-line="1690"><rect x="14" y="52.068" width="932" height="720" fill="none" style="stroke:#444;stroke-width:1;stroke-dasharray:7,7;"/><text x="434.36" y="91.172" fill="#444" font-size="16" textLength="91.28" font-weight="700">sdlc-engine</text><text x="447.018" y="108.688" fill="#444" font-size="12" textLength="65.964" font-weight="700">[container]</text></g><!--entity ledger--><g class="entity" data-qualified-name="ledger" id="ent0010" data-source-line="92"><path d="M875,834.568 C875,824.568 970.056,824.568 970.056,824.568 C970.056,824.568 1065.112,824.568 1065.112,834.568 L1065.112,898.048 C1065.112,908.048 970.056,908.048 970.056,908.048 C970.056,908.048 875,908.048 875,898.048 L875,834.568" style="stroke:#A6A6A6;stroke-width:0.5;" fill="#B3B3B3"/><path d="M875,834.568 C875,844.568 970.056,844.568 970.056,844.568 C970.056,844.568 1065.112,844.568 1065.112,834.568" style="stroke:#A6A6A6;stroke-width:0.5;" fill="none"/><text x="914.892" y="861.396" fill="#FFF" font-size="12" textLength="110.329" font-style="italic">«external_container»</text><text x="885" y="882.016" fill="#FFF" font-size="16" textLength="170.112" font-weight="700">spdd/memory/*.jsonl</text><text x="919.512" y="899.532" fill="#FFF" font-size="12" textLength="62.28" font-style="italic">[Committed</text><text x="981.792" y="899.532" fill="#FFF" font-size="12" font-style="italic"> </text><text x="984.912" y="899.532" fill="#FFF" font-size="12" textLength="35.688" font-style="italic">JSONL]</text></g><!--entity staged--><g class="entity" data-qualified-name="staged" id="ent0011" data-source-line="92"><path d="M478.5,834.568 C478.5,824.568 590.764,824.568 590.764,824.568 C590.764,824.568 703.028,824.568 703.028,834.568 L703.028,898.048 C703.028,908.048 590.764,908.048 590.764,908.048 C590.764,908.048 478.5,908.048 478.5,898.048 L478.5,834.568" style="stroke:#A6A6A6;stroke-width:0.5;" fill="#B3B3B3"/><path d="M478.5,834.568 C478.5,844.568 590.764,844.568 590.764,844.568 C590.764,844.568 703.028,844.568 703.028,834.568" style="stroke:#A6A6A6;stroke-width:0.5;" fill="none"/><text x="535.6" y="861.396" fill="#FFF" font-size="12" textLength="110.329" font-style="italic">«external_container»</text><text x="488.5" y="882.016" fill="#FFF" font-size="16" textLength="204.528" font-weight="700">.sdlc/staged/lessons.jsonl</text><text x="542.926" y="899.532" fill="#FFF" font-size="12" textLength="60.156" font-style="italic">[Gitignored</text><text x="603.082" y="899.532" fill="#FFF" font-size="12" font-style="italic"> </text><text x="606.202" y="899.532" fill="#FFF" font-size="12" textLength="32.4" font-style="italic">stage]</text></g><!--entity sqlite--><g class="entity" data-qualified-name="sqlite" id="ent0012" data-source-line="92"><path d="M290.5,834.568 C290.5,824.568 368.188,824.568 368.188,824.568 C368.188,824.568 445.876,824.568 445.876,834.568 L445.876,898.048 C445.876,908.048 368.188,908.048 368.188,908.048 C368.188,908.048 290.5,908.048 290.5,898.048 L290.5,834.568" style="stroke:#A6A6A6;stroke-width:0.5;" fill="#B3B3B3"/><path d="M290.5,834.568 C290.5,844.568 368.188,844.568 368.188,844.568 C368.188,844.568 445.876,844.568 445.876,834.568" style="stroke:#A6A6A6;stroke-width:0.5;" fill="none"/><text x="313.024" y="861.396" fill="#FFF" font-size="12" textLength="110.329" font-style="italic">«external_container»</text><text x="300.5" y="882.016" fill="#FFF" font-size="16" textLength="135.376" font-weight="700">.sdlc/index.sqlite</text><text x="331.144" y="899.532" fill="#FFF" font-size="12" textLength="36.864" font-style="italic">[Opt-in</text><text x="368.008" y="899.532" fill="#FFF" font-size="12" font-style="italic"> </text><text x="371.128" y="899.532" fill="#FFF" font-size="12" textLength="34.104" font-style="italic">cache]</text></g><!--entity guide--><g class="entity" data-qualified-name="guide" id="ent0013" data-source-line="419"><rect x="727.5" y="818.068" width="129.144" height="96.272" fill="#999" style="stroke:#8A8A8A;stroke-width:0.5;"/><text x="744.059" y="840.896" fill="#FFF" font-size="12" textLength="96.025" font-style="italic">«external_system»</text><text x="748.104" y="861.516" fill="#FFF" font-size="16" textLength="46.56" font-weight="700">Guide</text><text x="794.664" y="861.516" fill="#FFF" font-size="16" font-weight="700"> </text><text x="798.824" y="861.516" fill="#FFF" font-size="16" textLength="37.216" font-weight="700">DICE</text><text x="790.252" y="881.17" fill="#FFF" font-size="14"> </text><text x="737.5" y="900.238" fill="#FFF" font-size="14" textLength="38.626">Neo4j</text><text x="776.126" y="900.238" fill="#FFF" font-size="14"> </text><text x="779.766" y="900.238" fill="#FFF" font-size="14" textLength="66.878">projection</text></g><!--entity cli--><g class="entity" data-qualified-name="engine.cli" id="ent0002" data-source-line="83"><rect x="234" y="137.068" width="360.493" height="150.752" fill="#85BBF0" style="stroke:#78A8D8;stroke-width:0.5;"/><text x="378.39" y="159.896" fill="#000" font-size="12" textLength="71.712" font-style="italic">«component»</text><text x="393.358" y="180.516" fill="#000" font-size="16" textLength="41.776" font-weight="700">cli.py</text><text x="376.746" y="198.032" fill="#000" font-size="12" textLength="51.732" font-style="italic">[argparse</text><text x="428.478" y="198.032" fill="#000" font-size="12" font-style="italic"> </text><text x="431.599" y="198.032" fill="#000" font-size="12" textLength="20.148" font-style="italic">CLI]</text><text x="412.426" y="216.514" fill="#000" font-size="14"> </text><text x="389.62" y="235.582" fill="#000" font-size="14" textLength="49.252">context</text><text x="244" y="254.65" fill="#000" font-size="14" textLength="340.493">retrieve|show|digest|accept|parity|persist-lesson,</text><text x="314.567" y="273.718" fill="#000" font-size="14" textLength="20.972">db,</text><text x="335.539" y="273.718" fill="#000" font-size="14"> </text><text x="339.179" y="273.718" fill="#000" font-size="14" textLength="54.334">registry,</text><text x="393.512" y="273.718" fill="#000" font-size="14"> </text><text x="397.152" y="273.718" fill="#000" font-size="14" textLength="64.386">workflow,</text><text x="461.538" y="273.718" fill="#000" font-size="14"> </text><text x="465.178" y="273.718" fill="#000" font-size="14" textLength="48.748">session</text></g><!--entity project--><g class="entity" data-qualified-name="engine.project" id="ent0003" data-source-line="83"><rect x="30.5" y="595.568" width="210.847" height="150.752" fill="#85BBF0" style="stroke:#78A8D8;stroke-width:0.5;"/><text x="100.067" y="618.396" fill="#000" font-size="12" textLength="71.712" font-style="italic">«component»</text><text x="95.628" y="639.016" fill="#000" font-size="16" textLength="80.592" font-weight="700">project.py</text><text x="91.841" y="656.532" fill="#000" font-size="12" textLength="28.032" font-style="italic">[Path</text><text x="119.874" y="656.532" fill="#000" font-size="12" font-style="italic"> </text><text x="122.994" y="656.532" fill="#000" font-size="12" textLength="57.012" font-style="italic">resolution]</text><text x="134.104" y="675.014" fill="#000" font-size="14"> </text><text x="41.424" y="694.082" fill="#000" font-size="14" textLength="91.616">Project.home:</text><text x="133.04" y="694.082" fill="#000" font-size="14"> </text><text x="136.68" y="694.082" fill="#000" font-size="14" textLength="82.096">SDLC_HOME</text><text x="218.775" y="694.082" fill="#000" font-size="14"> </text><text x="222.415" y="694.082" fill="#000" font-size="14">></text><text x="40.5" y="713.15" fill="#000" font-size="14" textLength="120.652"><root>/sdlc-spdd/.</text><text x="161.152" y="713.15" fill="#000" font-size="14"> </text><text x="164.792" y="713.15" fill="#000" font-size="14" textLength="16.17">All</text><text x="180.961" y="713.15" fill="#000" font-size="14"> </text><text x="184.601" y="713.15" fill="#000" font-size="14" textLength="46.746">artifact</text><text x="56.439" y="732.218" fill="#000" font-size="14" textLength="36.876">paths</text><text x="93.315" y="732.218" fill="#000" font-size="14"> </text><text x="96.955" y="732.218" fill="#000" font-size="14" textLength="40.908">derive</text><text x="137.863" y="732.218" fill="#000" font-size="14"> </text><text x="141.503" y="732.218" fill="#000" font-size="14" textLength="32.158">from</text><text x="173.661" y="732.218" fill="#000" font-size="14"> </text><text x="177.3" y="732.218" fill="#000" font-size="14" textLength="38.108">home</text></g><!--entity ledgermod--><g class="entity" data-qualified-name="engine.ledgermod" id="ent0004" data-source-line="83"><rect x="494" y="586.068" width="216.111" height="169.82" fill="#85BBF0" style="stroke:#78A8D8;stroke-width:0.5;"/><text x="566.199" y="608.896" fill="#000" font-size="12" textLength="71.712" font-style="italic">«component»</text><text x="531.911" y="629.516" fill="#000" font-size="16" textLength="140.288" font-weight="700">lessons_ledger.py</text><text x="554.169" y="647.032" fill="#000" font-size="12" textLength="40.8" font-style="italic">[System</text><text x="594.97" y="647.032" fill="#000" font-size="12" font-style="italic"> </text><text x="598.09" y="647.032" fill="#000" font-size="12" textLength="10.572" font-style="italic">of</text><text x="608.662" y="647.032" fill="#000" font-size="12" font-style="italic"> </text><text x="611.782" y="647.032" fill="#000" font-size="12" textLength="38.16" font-style="italic">record]</text><text x="600.236" y="665.514" fill="#000" font-size="14"> </text><text x="520.884" y="684.582" fill="#000" font-size="14" textLength="91.952">LessonRecord</text><text x="612.836" y="684.582" fill="#000" font-size="14"> </text><text x="616.476" y="684.582" fill="#000" font-size="14" textLength="12.208">(5</text><text x="628.684" y="684.582" fill="#000" font-size="14"> </text><text x="632.324" y="684.582" fill="#000" font-size="14" textLength="39.256">kinds)</text><text x="671.579" y="684.582" fill="#000" font-size="14"> </text><text x="675.219" y="684.582" fill="#000" font-size="14">+</text><text x="504" y="703.65" fill="#000" font-size="14" textLength="102.354">LessonsLedger:</text><text x="606.354" y="703.65" fill="#000" font-size="14"> </text><text x="609.994" y="703.65" fill="#000" font-size="14" textLength="39.872">stage,</text><text x="649.865" y="703.65" fill="#000" font-size="14"> </text><text x="653.505" y="703.65" fill="#000" font-size="14" textLength="46.606">accept,</text><text x="519.638" y="722.718" fill="#000" font-size="14" textLength="53.718">records,</text><text x="573.356" y="722.718" fill="#000" font-size="14"> </text><text x="576.996" y="722.718" fill="#000" font-size="14" textLength="44.24">digest.</text><text x="621.236" y="722.718" fill="#000" font-size="14"> </text><text x="624.876" y="722.718" fill="#000" font-size="14" textLength="59.598">Validates</text><text x="533.127" y="741.786" fill="#000" font-size="14" textLength="54.67">schema,</text><text x="587.797" y="741.786" fill="#000" font-size="14"> </text><text x="591.437" y="741.786" fill="#000" font-size="14" textLength="56.98">dedupes</text><text x="648.416" y="741.786" fill="#000" font-size="14"> </text><text x="652.056" y="741.786" fill="#000" font-size="14" textLength="18.928">ids</text></g><!--entity store--><g class="entity" data-qualified-name="engine.store" id="ent0005" data-source-line="83"><rect x="320" y="344.068" width="188.209" height="169.82" fill="#85BBF0" style="stroke:#78A8D8;stroke-width:0.5;"/><text x="378.248" y="366.896" fill="#000" font-size="12" textLength="71.712" font-style="italic">«component»</text><text x="347.545" y="387.516" fill="#000" font-size="16" textLength="133.12" font-weight="700">context_store.py</text><text x="373.952" y="405.032" fill="#000" font-size="12" textLength="80.305" font-style="italic">[Orchestration]</text><text x="412.285" y="423.514" fill="#000" font-size="14"> </text><text x="334.865" y="442.582" fill="#000" font-size="14" textLength="92.624">persist_lesson</text><text x="427.489" y="442.582" fill="#000" font-size="14"> </text><text x="431.129" y="442.582" fill="#000" font-size="14" textLength="42.826">stages</text><text x="473.954" y="442.582" fill="#000" font-size="14"> </text><text x="477.594" y="442.582" fill="#000" font-size="14" textLength="15.75">by</text><text x="330" y="461.65" fill="#000" font-size="14" textLength="50.246">default;</text><text x="380.246" y="461.65" fill="#000" font-size="14"> </text><text x="383.886" y="461.65" fill="#000" font-size="14" textLength="42.854">accept</text><text x="426.74" y="461.65" fill="#000" font-size="14"> </text><text x="430.38" y="461.65" fill="#000" font-size="14" textLength="67.83">promotes;</text><text x="333.745" y="480.718" fill="#000" font-size="14" textLength="48.006">fan-out</text><text x="381.751" y="480.718" fill="#000" font-size="14"> </text><text x="385.391" y="480.718" fill="#000" font-size="14" textLength="13.524">to</text><text x="398.915" y="480.718" fill="#000" font-size="14"> </text><text x="402.555" y="480.718" fill="#000" font-size="14" textLength="35.49">sqlite</text><text x="438.045" y="480.718" fill="#000" font-size="14"> </text><text x="441.685" y="480.718" fill="#000" font-size="14">+</text><text x="449.693" y="480.718" fill="#000" font-size="14"> </text><text x="453.333" y="480.718" fill="#000" font-size="14" textLength="41.132">guide;</text><text x="352.155" y="499.786" fill="#000" font-size="14" textLength="38.052">parity</text><text x="390.207" y="499.786" fill="#000" font-size="14"> </text><text x="393.847" y="499.786" fill="#000" font-size="14" textLength="82.208">check/repair</text></g><!--entity registry--><g class="entity" data-qualified-name="engine.registry" id="ent0006" data-source-line="83"><rect x="712.5" y="137.068" width="217.469" height="150.752" fill="#85BBF0" style="stroke:#78A8D8;stroke-width:0.5;"/><text x="785.378" y="159.896" fill="#000" font-size="12" textLength="71.712" font-style="italic">«component»</text><text x="777.843" y="180.516" fill="#000" font-size="16" textLength="86.784" font-weight="700">registry.py</text><text x="782.144" y="198.032" fill="#000" font-size="12" textLength="31.224" font-style="italic">[Work</text><text x="813.369" y="198.032" fill="#000" font-size="12" font-style="italic"> </text><text x="816.489" y="198.032" fill="#000" font-size="12" textLength="43.836" font-style="italic">registry]</text><text x="819.415" y="216.514" fill="#000" font-size="14"> </text><text x="733.693" y="235.582" fill="#000" font-size="14" textLength="83.706">Append-only</text><text x="817.399" y="235.582" fill="#000" font-size="14"> </text><text x="821.039" y="235.582" fill="#000" font-size="14" textLength="87.738">claim/release</text><text x="722.5" y="254.65" fill="#000" font-size="14" textLength="43.316">events</text><text x="765.816" y="254.65" fill="#000" font-size="14"> </text><text x="769.456" y="254.65" fill="#000" font-size="14" textLength="12.264">in</text><text x="781.72" y="254.65" fill="#000" font-size="14"> </text><text x="785.36" y="254.65" fill="#000" font-size="14" textLength="89.138">registry.jsonl;</text><text x="874.497" y="254.65" fill="#000" font-size="14"> </text><text x="878.137" y="254.65" fill="#000" font-size="14" textLength="41.832">legacy</text><text x="749.121" y="273.718" fill="#000" font-size="14" textLength="23.87">TSV</text><text x="772.991" y="273.718" fill="#000" font-size="14"> </text><text x="776.631" y="273.718" fill="#000" font-size="14" textLength="62.524">read-only</text><text x="839.155" y="273.718" fill="#000" font-size="14"> </text><text x="842.795" y="273.718" fill="#000" font-size="14" textLength="50.554">fallback</text></g><!--entity db--><g class="entity" data-qualified-name="engine.db" id="ent0007" data-source-line="83"><rect x="260" y="586.068" width="216.433" height="169.82" fill="#85BBF0" style="stroke:#78A8D8;stroke-width:0.5;"/><text x="332.36" y="608.896" fill="#000" font-size="12" textLength="71.712" font-style="italic">«component»</text><text x="346.193" y="629.516" fill="#000" font-size="16" textLength="44.048" font-weight="700">db.py</text><text x="331.172" y="647.032" fill="#000" font-size="12" textLength="36.864" font-style="italic">[Opt-in</text><text x="368.037" y="647.032" fill="#000" font-size="12" font-style="italic"> </text><text x="371.157" y="647.032" fill="#000" font-size="12" textLength="34.104" font-style="italic">cache]</text><text x="366.397" y="665.514" fill="#000" font-size="14"> </text><text x="305.539" y="684.582" fill="#000" font-size="14" textLength="35.49">sqlite</text><text x="341.029" y="684.582" fill="#000" font-size="14"> </text><text x="344.669" y="684.582" fill="#000" font-size="14" textLength="50.918">schema</text><text x="395.587" y="684.582" fill="#000" font-size="14"> </text><text x="399.227" y="684.582" fill="#000" font-size="14" textLength="15.12">v5</text><text x="414.346" y="684.582" fill="#000" font-size="14"> </text><text x="417.986" y="684.582" fill="#000" font-size="14" textLength="12.908">at</text><text x="270" y="703.65" fill="#000" font-size="14" textLength="113.778">.sdlc/index.sqlite;</text><text x="383.778" y="703.65" fill="#000" font-size="14"> </text><text x="387.418" y="703.65" fill="#000" font-size="14" textLength="43.218">rebuilt</text><text x="430.635" y="703.65" fill="#000" font-size="14"> </text><text x="434.275" y="703.65" fill="#000" font-size="14" textLength="32.158">from</text><text x="270.861" y="722.718" fill="#000" font-size="14" textLength="42.406">ledger</text><text x="313.267" y="722.718" fill="#000" font-size="14"> </text><text x="316.907" y="722.718" fill="#000" font-size="14">+</text><text x="324.915" y="722.718" fill="#000" font-size="14"> </text><text x="328.555" y="722.718" fill="#000" font-size="14" textLength="50.582">registry</text><text x="379.137" y="722.718" fill="#000" font-size="14"> </text><text x="382.777" y="722.718" fill="#000" font-size="14">+</text><text x="390.785" y="722.718" fill="#000" font-size="14"> </text><text x="394.425" y="722.718" fill="#000" font-size="14" textLength="59.5">canvases</text><text x="453.924" y="722.718" fill="#000" font-size="14"> </text><text x="457.564" y="722.718" fill="#000" font-size="14">+</text><text x="312.322" y="741.786" fill="#000" font-size="14" textLength="77.644">governance</text><text x="389.966" y="741.786" fill="#000" font-size="14"> </text><text x="393.606" y="741.786" fill="#000" font-size="14" textLength="30.506">docs</text></g><!--entity workflow--><g class="entity" data-qualified-name="engine.workflow" id="ent0008" data-source-line="83"><rect x="712" y="344.068" width="218.309" height="169.82" fill="#85BBF0" style="stroke:#78A8D8;stroke-width:0.5;"/><text x="785.298" y="366.896" fill="#000" font-size="12" textLength="71.712" font-style="italic">«component»</text><text x="771.531" y="387.516" fill="#000" font-size="16" textLength="99.248" font-weight="700">workflow.py</text><text x="785.784" y="405.032" fill="#000" font-size="12" textLength="35.22" font-style="italic">[Phase</text><text x="821.005" y="405.032" fill="#000" font-size="12" font-style="italic"> </text><text x="824.125" y="405.032" fill="#000" font-size="12" textLength="32.4" font-style="italic">gates]</text><text x="819.335" y="423.514" fill="#000" font-size="14"> </text><text x="736.875" y="442.582" fill="#000" font-size="14" textLength="58.422">Progress</text><text x="795.297" y="442.582" fill="#000" font-size="14"> </text><text x="798.937" y="442.582" fill="#000" font-size="14" textLength="33.166">gate:</text><text x="832.103" y="442.582" fill="#000" font-size="14"> </text><text x="835.743" y="442.582" fill="#000" font-size="14" textLength="23.646">any</text><text x="859.388" y="442.582" fill="#000" font-size="14"> </text><text x="863.028" y="442.582" fill="#000" font-size="14" textLength="42.406">ledger</text><text x="722" y="461.65" fill="#000" font-size="14" textLength="43.26">record</text><text x="765.26" y="461.65" fill="#000" font-size="14"> </text><text x="768.9" y="461.65" fill="#000" font-size="14" textLength="19.068">for</text><text x="787.968" y="461.65" fill="#000" font-size="14"> </text><text x="791.608" y="461.65" fill="#000" font-size="14" textLength="32.732">work</text><text x="824.34" y="461.65" fill="#000" font-size="14"> </text><text x="827.98" y="461.65" fill="#000" font-size="14" textLength="15.974">id.</text><text x="843.954" y="461.65" fill="#000" font-size="14"> </text><text x="847.594" y="461.65" fill="#000" font-size="14" textLength="35.91">Retro</text><text x="883.503" y="461.65" fill="#000" font-size="14"> </text><text x="887.143" y="461.65" fill="#000" font-size="14" textLength="33.166">gate:</text><text x="745.772" y="480.718" fill="#000" font-size="14" textLength="150.765">decision/pitfall/pattern</text><text x="795.857" y="499.786" fill="#000" font-size="14" textLength="50.596">present</text></g><!--entity guideops--><g class="entity" data-qualified-name="engine.guideops" id="ent0009" data-source-line="83"><rect x="728" y="605.068" width="202.139" height="131.684" fill="#85BBF0" style="stroke:#78A8D8;stroke-width:0.5;"/><text x="793.213" y="627.896" fill="#000" font-size="12" textLength="71.712" font-style="italic">«component»</text><text x="740.821" y="648.516" fill="#000" font-size="16" textLength="176.496" font-weight="700">installer/guide_ops.py</text><text x="778.801" y="666.032" fill="#000" font-size="12" textLength="34.596" font-style="italic">[Guide</text><text x="813.398" y="666.032" fill="#000" font-size="12" font-style="italic"> </text><text x="816.518" y="666.032" fill="#000" font-size="12" textLength="62.82" font-style="italic">integration]</text><text x="827.25" y="684.514" fill="#000" font-size="14"> </text><text x="738" y="703.582" fill="#000" font-size="14" textLength="66.738">Projection</text><text x="804.738" y="703.582" fill="#000" font-size="14"> </text><text x="808.378" y="703.582" fill="#000" font-size="14" textLength="49.098">trigger,</text><text x="857.476" y="703.582" fill="#000" font-size="14"> </text><text x="861.116" y="703.582" fill="#000" font-size="14" textLength="27.846">RAG</text><text x="888.961" y="703.582" fill="#000" font-size="14"> </text><text x="892.601" y="703.582" fill="#000" font-size="14" textLength="27.538">ops,</text><text x="764.74" y="722.65" fill="#000" font-size="14" textLength="23.786">ops</text><text x="788.526" y="722.65" fill="#000" font-size="14"> </text><text x="792.166" y="722.65" fill="#000" font-size="14" textLength="50.526">console</text><text x="842.692" y="722.65" fill="#000" font-size="14"> </text><text x="846.332" y="722.65" fill="#000" font-size="14" textLength="47.068">actions</text></g><g class="link" data-entity-1="ent0002" data-entity-2="ent0005" id="lnk14" data-link-type="dependency"><polygon points="414,343.708,417,335.708,411,335.708,414,343.708" fill="#666" style="stroke:#666;stroke-width:1;stroke-linejoin:miter;stroke-miterlimit:10;"/><path d="M414,288.288 C414,306.183 414,317.411 414,335.708" style="stroke:#666;stroke-width:1;" fill="none"/><text x="416" y="320.896" fill="#666" font-size="12" textLength="65.856" font-weight="700">commands</text></g><g class="link" data-entity-1="ent0005" data-entity-2="ent0004" id="lnk15" data-link-type="dependency"><polygon points="526.081,586.028,523.369,577.926,518.71,581.708,526.081,586.028" fill="#666" style="stroke:#666;stroke-width:1;stroke-linejoin:miter;stroke-miterlimit:10;"/><path d="M476.44,514.084 C480.735,520.138 484.959,526.177 489,532.068 C499.741,547.725 500.574,552.904 512,568.068 C516.477,574.009 516.173,573.821 521.039,579.817" style="stroke:#666;stroke-width:1;" fill="none"/><text x="514" y="554.896" fill="#666" font-size="12" textLength="33.108" font-weight="700">stage</text><text x="547.108" y="554.896" fill="#666" font-size="12" font-weight="700"> </text><text x="550.228" y="554.896" fill="#666" font-size="12" font-weight="700">/</text><text x="555.184" y="554.896" fill="#666" font-size="12" font-weight="700"> </text><text x="558.304" y="554.896" fill="#666" font-size="12" textLength="39.48" font-weight="700">accept</text><text x="597.785" y="554.896" fill="#666" font-size="12" font-weight="700"> </text><text x="600.905" y="554.896" fill="#666" font-size="12" font-weight="700">/</text><text x="605.861" y="554.896" fill="#666" font-size="12" font-weight="700"> </text><text x="608.981" y="554.896" fill="#666" font-size="12" textLength="27.384" font-weight="700">read</text></g><g class="link" data-entity-1="ent0005" data-entity-2="ent0007" id="lnk16" data-link-type="dependency"><polygon points="316.47,585.876,317.104,577.355,311.345,579.039,316.47,585.876" fill="#666" style="stroke:#666;stroke-width:1;stroke-linejoin:miter;stroke-miterlimit:10;"/><path d="M325.886,514.195 C322.459,519.972 319.448,525.941 317,532.068 C310.082,549.383 309.011,560.365 314.225,578.197" style="stroke:#666;stroke-width:1;" fill="none"/><text x="319" y="546.896" fill="#666" font-size="12" textLength="39.192" font-weight="700">upsert</text><text x="358.192" y="546.896" fill="#666" font-size="12" font-weight="700"> </text><text x="361.312" y="546.896" fill="#666" font-size="12" textLength="37.992" font-weight="700">lesson</text><text x="399.305" y="546.896" fill="#666" font-size="12" font-weight="700"> </text><text x="402.425" y="546.896" fill="#666" font-size="12" textLength="45.144" font-weight="700">records</text><text x="447.569" y="546.896" fill="#666" font-size="12" font-weight="700"> </text><text x="450.689" y="546.896" fill="#666" font-size="12" textLength="37.2" font-weight="700">(when</text><text x="348.532" y="563.24" fill="#666" font-size="12" textLength="33.18" font-weight="700">sqlite</text><text x="381.712" y="563.24" fill="#666" font-size="12" font-weight="700"> </text><text x="384.832" y="563.24" fill="#666" font-size="12" textLength="51.024" font-weight="700">backend</text><text x="435.857" y="563.24" fill="#666" font-size="12" font-weight="700"> </text><text x="438.977" y="563.24" fill="#666" font-size="12" textLength="19.38" font-weight="700">on)</text></g><g class="link" data-entity-1="ent0005" data-entity-2="ent0009" id="lnk17" data-link-type="dependency"><polygon points="750.785,604.944,745.828,597.985,742.475,602.961,750.785,604.944" fill="#666" style="stroke:#666;stroke-width:1;stroke-linejoin:miter;stroke-miterlimit:10;"/><path d="M508.237,457.284 C551.718,473.508 601.632,497.762 638,532.068 C651.16,544.482 642.976,556.638 657,568.068 C679.241,586.196 693.384,573.141 719,586.068 C729.726,591.481 733.824,593.512 744.151,600.473" style="stroke:#666;stroke-width:1;" fill="none"/><text x="659" y="554.896" fill="#666" font-size="12" textLength="59.004" font-weight="700">re-project</text><text x="718.004" y="554.896" fill="#666" font-size="12" font-weight="700"> </text><text x="721.124" y="554.896" fill="#666" font-size="12" textLength="29.64" font-weight="700">after</text><text x="750.765" y="554.896" fill="#666" font-size="12" font-weight="700"> </text><text x="753.885" y="554.896" fill="#666" font-size="12" textLength="39.48" font-weight="700">accept</text></g><g class="link" data-entity-1="ent0004" data-entity-2="ent0011" id="lnk18" data-link-type="dependency"><polygon points="590.813,824.405,593.876,816.429,587.877,816.382,590.813,824.405" fill="#666" style="stroke:#666;stroke-width:1;stroke-linejoin:miter;stroke-miterlimit:10;"/><path d="M593.537,756.33 C592.931,764.347 592.402,772.358 592,780.068 C591.248,794.511 590.988,802.353 590.876,816.406" style="stroke:#666;stroke-width:1;" fill="none"/><text x="594" y="794.896" fill="#666" font-size="12" textLength="52.608" font-weight="700">captures</text></g><g class="link" data-entity-1="ent0004" data-entity-2="ent0010" id="lnk19" data-link-type="dependency"><polygon points="886.264,824.524,879.758,818.986,877.72,824.629,886.264,824.524" fill="#666" style="stroke:#666;stroke-width:1;stroke-linejoin:miter;stroke-miterlimit:10;"/><path d="M648.093,756.124 C661.342,773.288 677.353,789.356 696,800.068 C761.881,837.914 792.674,798.171 866,818.068 C872.693,819.884 871.975,819.366 878.739,821.808" style="stroke:#666;stroke-width:1;" fill="none"/><text x="698" y="794.896" fill="#666" font-size="12" textLength="54.168" font-weight="700">accepted</text><text x="752.168" y="794.896" fill="#666" font-size="12" font-weight="700"> </text><text x="755.288" y="794.896" fill="#666" font-size="12" textLength="45.144" font-weight="700">records</text></g><g class="link" data-entity-1="ent0006" data-entity-2="ent0010" id="lnk20" data-link-type="dependency"><polygon points="976.357,824.495,980.411,816.974,974.466,816.163,976.357,824.495" fill="#666" style="stroke:#666;stroke-width:1;stroke-linejoin:miter;stroke-miterlimit:10;"/><path d="M903.659,288.191 C917.796,305.127 930.609,324.07 939,344.068 C1008.943,510.758 989.781,726.125 977.439,816.568" style="stroke:#666;stroke-width:1;" fill="none"/><text x="988" y="554.896" fill="#666" font-size="12" textLength="81.073" font-weight="700">claim/release</text><text x="1069.073" y="554.896" fill="#666" font-size="12" font-weight="700"> </text><text x="1072.193" y="554.896" fill="#666" font-size="12" textLength="40.068" font-weight="700">events</text></g><g class="link" data-entity-1="ent0007" data-entity-2="ent0012" id="lnk21" data-link-type="dependency"><polygon points="368,824.379,371,816.379,365,816.379,368,824.379" fill="#666" style="stroke:#666;stroke-width:1;stroke-linejoin:miter;stroke-miterlimit:10;"/><path d="M368,756.159 C368,779.827 368,796.509 368,816.379" style="stroke:#666;stroke-width:1;" fill="none"/><text x="370" y="794.896" fill="#666" font-size="12" textLength="42.936" font-weight="700">rebuild</text></g><g class="link" data-entity-1="ent0009" data-entity-2="ent0013" id="lnk22" data-link-type="dependency"><polygon points="801.032,817.956,805.484,810.664,799.592,809.534,801.032,817.956" fill="#666" style="stroke:#666;stroke-width:1;stroke-linejoin:miter;stroke-miterlimit:10;"/><path d="M816.525,737.14 C811.448,763.623 807.198,785.794 802.538,810.099" style="stroke:#666;stroke-width:1;" fill="none"/><text x="811" y="794.896" fill="#666" font-size="12" textLength="30.612" font-weight="700">HTTP</text></g><g class="link" data-entity-1="ent0008" data-entity-2="ent0004" id="lnk23" data-link-type="dependency"><polygon points="710.11,590.295,718.606,589.393,715.918,584.029,710.11,590.295" fill="#666" style="stroke:#666;stroke-width:1;stroke-linejoin:miter;stroke-miterlimit:10;"/><path d="M823.258,514.2 C819.853,533.955 812.698,553.475 799,568.068 C774.057,594.64 752.247,571.141 719,586.068 C716.034,587.399 720.221,585.229 717.262,586.711" style="stroke:#666;stroke-width:1;" fill="none"/><text x="821" y="554.896" fill="#666" font-size="12" textLength="33.108" font-weight="700">gates</text><text x="854.108" y="554.896" fill="#666" font-size="12" font-weight="700"> </text><text x="857.228" y="554.896" fill="#666" font-size="12" textLength="27.384" font-weight="700">read</text><text x="884.612" y="554.896" fill="#666" font-size="12" font-weight="700"> </text><text x="887.732" y="554.896" fill="#666" font-size="12" textLength="38.484" font-weight="700">ledger</text></g><g class="link" data-entity-1="ent0005" data-entity-2="ent0003" id="lnk24" data-link-type="dependency"><polygon points="139.665,595.43,144.129,588.145,138.238,587.006,139.665,595.43" fill="#666" style="stroke:#666;stroke-width:1;stroke-linejoin:miter;stroke-miterlimit:10;"/><path d="M319.806,446.84 C267.493,460.952 205.409,486.473 167,532.068 C152.129,549.721 145.518,565.162 141.184,587.575" style="stroke:#666;stroke-width:1;" fill="none"/><text x="169" y="554.896" fill="#666" font-size="12" textLength="33.9" font-weight="700">paths</text><text x="202.9" y="554.896" fill="#666" font-size="12" font-weight="700"> </text><text x="206.02" y="554.896" fill="#666" font-size="12" textLength="17.736" font-weight="700">via</text><text x="223.756" y="554.896" fill="#666" font-size="12" font-weight="700"> </text><text x="226.876" y="554.896" fill="#666" font-size="12" textLength="80.149" font-weight="700">Project.home</text></g><?plantuml-src RLLDSnev4BtpAtevmIumXqqk9fKgBtOvsIAno_OcHqf8pI1R8sab3PYAzxzlwsC67CA1abgjrklkztf-avghLY1iGx3-egeSsyzg6RRjklNekD41TPXnqrHMCHtQHarz44gkgyMxwLqq_VDbMJNC1tHaMYrCOusETopv3WP11eNGMprCmGl5fwXhgH56TuUmDNe2FXZ7QeJTd-F1O47FCZfsgxzCgmLpXr7sdy3mvFPm33y6GBy-_8WhIJxqFxE78QsOgorpdX0iFyTzIkWbWCFW9ExmrM_D_bN86druPPoZ3QzqGOR3gqNdfGzJXTv7X68zeMircTnX0dlZdZVAx0avdKkZX-EVe5Xddf07UhIi2gGx5hOKo1lLXdWlceh3rZHu0GzNoyNgq-sNQ_W87vmnuMELalPMY6e6bqe1Sq5k60zWAPW7WKxk43RED11Zd65HA6fqZH7qMCx7hxAjW7euK0CRC1j2neqJqRPCZlTf3wFty2ory6Fu0_A1NwO05zGwQjm4SlKc9O2dWgMgpk0RKrAmW1uytsB3w0H5QsalXJ-36ec0nqwjqhwWl7KKWEhFkiAL7groJXc9X_M1NjYmLeLvGGNKWiOGX3bic9wQDa0muFzLCY0bLBTIu1nouu7UuC-LGykaEuDtPC2mMnPmtuaHFK6YuT9Qr69gjAAOYic6OYfaH4OYNfI2rBt_x8dmgpaeh1a_mEF3DpfYvVA6ARLc_FaCJAJaKAoxwjWmfP2SKG9nNzBBHOVTUs01Pf595JKINsRvV4uFhLkfGgPFPWNLf4D6IyxqZc3xM2cpGwUPvWZ2yFFk7MKnx9OTxRSnyphoe52-9X7uPBa0fW_Tqva_i34kYOn8CeDxagifhWAvZ8Ahh0onCXLfW8QFfXwZ9mndc59tZIL63gKcYYX5eoePLsGjy6xYdZeIi3xIg-YITa3ZeYPq4xY_l84KZFw0I4ewHc9PT8hb-EzaUbsjLzSldVXYGwA06soCErI_vux7LmXf8oa50Ny_t7vTKf2V8oHUH-tczkLjLOHy37GZWwmroKLa9VmgKkfv7-cK26SK6W_oL4WNKxtwEbrzNbn7_wzetZs1xSj4TmRtgF8OFYgvQPXEKezdnNmwaX9Qg3gnLedxRzqJrLjBtwG0HUrbqC1elqNTYJdgX5G7He_V1ZXXWSDfWGniGzGfxnR_4s1ztJcpeILwdRlqpSqngFG5Ld4zpelUyrUpe7Z7BFiMPLrsTJZYxrhnwV7nhXmUPNTQrYIsLCjYVbkHamzK-ehi98FJBn5v_my0?></g></svg> \ No newline at end of file diff --git a/docs/diagrams/06-stage-then-accept.puml b/docs/diagrams/06-stage-then-accept.puml index 97138ba7..2d43e05c 100644 --- a/docs/diagrams/06-stage-then-accept.puml +++ b/docs/diagrams/06-stage-then-accept.puml @@ -5,7 +5,7 @@ title Stage-then-accept - captures without git noise (storage v3) actor "AI Agent" as agent participant "sdlc.sh capture\n(capture-session-memory.sh)" as capture database ".sdlc/staged/\nlessons.jsonl\n(gitignored)" as staged -participant "sdlc.sh accept\n(accept-lessons.sh /\nsdlc-engine context accept)" as accept +participant "sdlc.sh accept\n(sdlc-engine accept)" as accept database "spdd/memory/\nlessons.jsonl\n(committed)" as ledger database ".sdlc/index.sqlite\n(opt-in cache)" as sqlite participant "Guide DICE\n(projection)" as guide diff --git a/docs/diagrams/06-stage-then-accept.svg b/docs/diagrams/06-stage-then-accept.svg index 26470871..6ee3e74c 100644 --- a/docs/diagrams/06-stage-then-accept.svg +++ b/docs/diagrams/06-stage-then-accept.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" contentStyleType="text/css" data-diagram-type="SEQUENCE" height="819px" preserveAspectRatio="none" style="width:1278px;height:819px;background:#FFFFFF;" version="1.1" viewBox="0 0 1278 819" width="1278px" zoomAndPan="magnify"><?plantuml 1.2026.6?><defs/><g><g class="title" data-source-line="2"><text fill="#000000" font-family="sans-serif" font-size="14" font-weight="700" lengthAdjust="spacing" textLength="471.3926" x="399.9854" y="27.9951">Stage-then-accept - captures without git noise (storage v3)</text></g><g class="participant-lifeline" data-entity-uid="part1" data-qualified-name="agent" data-source-line="4" id="part1-lifeline"><g><title>AI Agentsdlc.sh capture.sdlc.staged.sdlc.sh acceptspdd.memory..sdlc.index.sqliteGuide DICEDeveloperAI AgentAI Agentsdlc.sh capture(capture-session-memory.sh)sdlc.sh capture(capture-session-memory.sh).sdlc/staged/lessons.jsonl(gitignored).sdlc/staged/lessons.jsonl(gitignored)sdlc.sh accept(accept-lessons.sh /sdlc-engine context accept)sdlc.sh accept(accept-lessons.sh /sdlc-engine context accept)spdd/memory/lessons.jsonl(committed)spdd/memory/lessons.jsonl(committed).sdlc/index.sqlite(opt-in cache).sdlc/index.sqlite(opt-in cache)Guide DICE(projection)Guide DICE(projection)DeveloperDeveloperDuring code / api-test (any phase)work-idphasesummaryareas[decisionspitfalls --patterns]append LessonRecord JSONL(no git commit, no push)Repeated captures stay local.Git history stays quiet.At retro / sync gateaccept --work-id <ID>read + validate staged recordsappend accepted records(dedupe by id)drain promoted recordsupsert lesson rows(when sqlite backend enabled)re-project ledger(when Guide reachable)accepted N, staged_remaining 0Once per gategit add + commit(one batch: contracts + ledger) \ No newline at end of file +Stage-then-accept - captures without git noise (storage v3)AI Agentsdlc.sh capture.sdlc.staged.sdlc.sh acceptspdd.memory..sdlc.index.sqliteGuide DICEDeveloperAI Agentsdlc.sh capture(capture-session-memory.sh).sdlc/staged/lessons.jsonl(gitignored)sdlc.sh accept(sdlc-engine accept)spdd/memory/lessons.jsonl(committed).sdlc/index.sqlite(opt-in cache)Guide DICE(projection)DeveloperAI Agentsdlc.sh capture(capture-session-memory.sh).sdlc/staged/lessons.jsonl(gitignored)sdlc.sh accept(sdlc-engine accept)spdd/memory/lessons.jsonl(committed).sdlc/index.sqlite(opt-in cache)Guide DICE(projection)DeveloperDuring code / api-test (any phase)work-idphasesummaryareas[decisionspitfalls --patterns]append LessonRecord JSONL(no git commit, no push)Repeated captures stay local.Git history stays quiet.At retro / sync gateaccept --work-id <ID>read + validate staged recordsappend accepted records(dedupe by id)drain promoted recordsupsert lesson rows(when sqlite backend enabled)re-project ledger(when Guide reachable)accepted N, staged_remaining 0Once per gategit add + commit(one batch: contracts + ledger) \ No newline at end of file diff --git a/docs/engine-v2.md b/docs/engine-v2.md index d507050c..a166d725 100644 --- a/docs/engine-v2.md +++ b/docs/engine-v2.md @@ -6,14 +6,19 @@ 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. +**One engine (REF-003).** `sdlc_engine` is the only workflow implementation. +`scripts/sdlc.sh` (installed as `sdlc-spdd/scripts/sdlc.sh`) is a thin +dispatcher that execs `python -m sdlc_engine --root `; there is no +bash workflow twin and no `SDLC_ENGINE` / `SDLC_GATE_ENGINE` switch (setting +either makes `sdlc.sh` exit 2). Python `WorkflowEngine.gate_check` is the +system under test for every gate. Shell remains only for install/upgrade +packaging and the session-brief renderers (`start-agent-session.sh`, +`capture-session-memory.sh`), which call back into the engine for all state. + +Storage v3 is the only layout: everything lives under the project's +`sdlc-spdd/` home — `sdlc-spdd/.sdlc/` (gitignored runtime), +`sdlc-spdd/spdd/memory/lessons.jsonl`, `sdlc-spdd/spdd/memory/registry.jsonl`, +canvases under `sdlc-spdd/spdd/canvas/`. See [storage-v3.md](storage-v3.md). ## Layout @@ -22,9 +27,15 @@ engine/ pyproject.toml README.md src/sdlc_engine/ - cli.py # argparse CLI entry (sdlc-engine) - cli_commands.py # command handlers + cli.py # entry point (sdlc-engine / python -m sdlc_engine) cli_parser.py # argparse builders + commands/ # handlers by concern + state.py # next/status/advance/gate/skip/shelf/pointer/shell + team.py # claim/release/team/list-work/sync-team/archive + context.py # context store, start/capture/complete/accept, session bridge + integrations.py # links/sync-links/sync-roadmap/issues/commit-message/sunset + storage.py # db, local sessions, work init-from-adf, viewer/installer/template + session.py # session bridge used by start-agent-session/capture-session-memory timeutil.py # shared UTC stamps io_util.py # JSON/path helpers placeholders.py # TBD/TODO/NONE/N/A sentinels @@ -36,8 +47,9 @@ engine/ lessons_ledger.py # lessons.jsonl + gitignored stage context_store.py # persist/retrieve/accept/parity persistence.py # CONTEXT_BACKENDS config - storage_migrate.py # legacy → ledger migration - archive.py + integration_config.py # Jira/GitHub tracker config (.sdlc/integrations-config.json) + quiet.py # quiet/product-test mode + archive.py # storage v3: remove completed work from the tree canvas.py links.py # milestone/canvas/registry link parsing sync_local.py # sync-links / sync-roadmap @@ -60,17 +72,17 @@ engine/ ## Usage ```bash -# From orchestrator checkout (no install) -PYTHONPATH=engine/src python3 -m sdlc_engine next --root . +# Orchestrator checkout: create the 3.12 venv once, then use the dispatcher +./scripts/setup-engine-venv.sh +./scripts/sdlc.sh next +./scripts/sdlc.sh claim FEAT-001-demo -# 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 +# Equivalent direct invocation +PYTHONPATH=engine/src python3.12 -m sdlc_engine next --root . -# Editable install -python3 -m pip install -e './engine[dev]' +# Installed target: sdlc-spdd/scripts/sdlc.sh finds /.venv/bin/python +# or python3.12 on PATH with sdlc-engine installed +python3.12 -m pip install -e '/path/to/orchestrator/engine[dev]' sdlc-engine team sdlc-engine archive --all --dry-run ``` @@ -83,24 +95,41 @@ sdlc-engine archive --all --dry-run ./scripts/sdlc.sh release ./scripts/sdlc.sh advance [--force] ./scripts/sdlc.sh shelf --reason "…" -./scripts/sdlc.sh gate +./scripts/sdlc.sh gate --phase [--work-id ] ./scripts/sdlc.sh quick [--work-id LOCAL-…] -./scripts/sdlc.sh archive [--all] [--dry-run] +./scripts/sdlc.sh archive [] [--all] [--dry-run] [--force] +``` + +## Session verbs + +`start`, `capture`, `complete`, `accept` are engine verbs. `capture` and +`complete` forward their remaining options to `capture-session-memory.sh`; +`session *` are the callbacks those scripts use for workflow state. + +```bash +./scripts/sdlc.sh start --work-id FEAT-001-demo --phase code +./scripts/sdlc.sh capture --summary "Wired the endpoint" --validation "unit green" +./scripts/sdlc.sh complete --summary "T01 done" \ + --verify-command "pytest -q" --verify-exit 0 --verify-result pass +./scripts/sdlc.sh accept --list +./scripts/sdlc.sh accept --work-id FEAT-001-demo --commit +./scripts/sdlc.sh session brief --work-id FEAT-001-demo +./scripts/sdlc.sh session recommend --work-id FEAT-001-demo --phase code ``` ## 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) @@ -112,8 +141,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 @@ -130,8 +157,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 @@ -169,15 +194,16 @@ sdlc-engine context parity --repair # Backend set (default git + guide; sqlite opt-in) sdlc-engine context backends sdlc-engine context backends --set git-pointers,guide-dice,sqlite - -# Legacy install migration -sdlc-engine storage status -sdlc-engine storage migrate [--dry-run] ``` +Pre-v3 layouts (`agent-context/`, root `spdd/`, `work-registry.tsv`) are not +migrated: `upgrade-project.sh` refuses them and the console reports +`unsupported`. Remove the legacy paths (git history keeps them) and re-run +`init-project.sh`. + ## Local SQLite cache (opt-in) -Regenerable query cache in `.sdlc/index.sqlite` (schema v5). See +Regenerable query cache in `sdlc-spdd/.sdlc/index.sqlite` (schema v5). See [local-sqlite-index.md](local-sqlite-index.md). ```bash @@ -189,7 +215,7 @@ Regenerable query cache in `.sdlc/index.sqlite` (schema v5). See ## Local / offline work sessions Machine-private `LOCAL-NNN-slug` sessions under gitignored -`.sdlc/local-sessions/`: +`sdlc-spdd/.sdlc/local-sessions/`: ```bash ./scripts/sdlc.sh local start --name scratch-sync --intent "Explore without a FEAT yet" @@ -203,8 +229,8 @@ requirement, then claims the new Work ID (unless `--no-claim`). ## Bridge to shell scripts -Install, upgrade, adapter generation, and some session helpers remain shell. -Call them through: +Install, upgrade, adapter generation, and the session-brief renderers remain +shell (they hold no workflow state). Call them through: ```bash python3 -m sdlc_engine shell setup-agent-prompts.sh -- --target /tmp/demo --all @@ -213,7 +239,9 @@ python3 -m sdlc_engine shell setup-agent-prompts.sh -- --target /tmp/demo --all ## Tests ```bash -PYTHONPATH=engine/src python3 -m pytest -q engine/tests +./scripts/run-test-suites.sh unit # engine/tests_unit +./scripts/run-test-suites.sh integration # engine/tests_integration (Flask test clients) +./tests/test-sdlc-workflow.sh # bash harnesses drive the installed dispatcher ``` See [TESTING.md](../TESTING.md) for CI jobs and integration flags. diff --git a/docs/framework-upgrade.md b/docs/framework-upgrade.md index 17a52d60..7d429444 100644 --- a/docs/framework-upgrade.md +++ b/docs/framework-upgrade.md @@ -53,27 +53,16 @@ managed grounding block inside `CLAUDE.md`: ... -## Storage migration and consolidation +## Layout contract (v3 only) -After upgrading framework files, run the engine storage commands from the target -project root: - -```bash -# Detect legacy layouts and show what would change -sdlc-engine storage status - -# One-shot: convert legacy memory trees → ledger + registry -sdlc-engine storage migrate [--dry-run] - -# Move scattered framework paths into the single sdlc-spdd/ home -./scripts/upgrade-project.sh --target . --all -``` - -Migration exports converted originals aside; the committed system of record becomes -`spdd/memory/lessons.jsonl` + `spdd/memory/registry.jsonl`. Hot runtime stays +The upgrade only operates on a `sdlc-spdd/` home. If it finds a pre-v3 layout +(root `spdd/`, `agent-context/`, `work-registry.tsv`, root `.sdlc/`) it exits +non-zero with a message telling you to re-initialize; there is no migration or +consolidation path. The committed system of record is +`spdd/memory/lessons.jsonl` + `spdd/memory/registry.jsonl`; hot runtime stays gitignored under `.sdlc/`. Full model: [Storage v3](storage-v3.md). -Verify parity after migration: +Verify parity after an upgrade: ```bash sdlc-engine context parity diff --git a/docs/installing-into-your-project.md b/docs/installing-into-your-project.md index 3b24e689..bbdeba5d 100644 --- a/docs/installing-into-your-project.md +++ b/docs/installing-into-your-project.md @@ -139,10 +139,9 @@ If the project already has SDLC-SPDD files from an older version: ./scripts/upgrade-project.sh --target /path/to/app --all The upgrade script updates framework-owned files and preserves application source, -requirements, canvases, reviews, sync logs, and the lessons ledger. Legacy memory -layouts are converted by `sdlc-engine storage migrate`; consolidating scattered -paths into `sdlc-spdd/` — run `upgrade-project.sh` (consolidation is always-on) — see -[Framework upgrade](framework-upgrade.md). +requirements, canvases, reviews, sync logs, and the lessons ledger. It only +operates on the `sdlc-spdd/` home; pre-v3 layouts are refused and must be +re-initialized — see [Framework upgrade](framework-upgrade.md). Backups of overwritten framework files are stored under: diff --git a/docs/local-sqlite-index.md b/docs/local-sqlite-index.md index 5221fbee..fc121d5d 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`): +Engine command: ```bash ./scripts/sdlc.sh db rebuild diff --git a/docs/maintaining-your-project.md b/docs/maintaining-your-project.md index 96ccb68d..4610f164 100644 --- a/docs/maintaining-your-project.md +++ b/docs/maintaining-your-project.md @@ -31,7 +31,7 @@ From the orchestrator repository: ./scripts/upgrade-project.sh --target /path/to/app --all The upgrade preserves application work, requirements, canvases, and the committed -lessons ledger. Legacy layouts are migrated by `sdlc-engine storage migrate` — see +lessons ledger. Pre-v3 layouts are refused (re-initialize) — see [Framework upgrade](framework-upgrade.md). Review backups under: @@ -117,7 +117,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..37b47468 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` is the only engine (no shell fallback) | | [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/docs/storage-v3.md b/docs/storage-v3.md index 42460a97..dabd36bb 100644 --- a/docs/storage-v3.md +++ b/docs/storage-v3.md @@ -13,7 +13,8 @@ quietly and accepted at gates.** - **One home.** Framework-owned paths live under `/sdlc-spdd/`. Contracts, harness, scripts, and ledgers share one install layout (this orchestrator dogfoods the same layout). Install *source* lives under `templates/`; root - `scripts/` is orchestrator tooling only. Leftover `agent-context/` is archived. + `scripts/` is orchestrator tooling only. Pre-v3 layouts (`agent-context/`, + root `spdd/`, `work-registry.tsv`) are unsupported and never migrated. - **Ledger-first.** One committed JSONL lessons file is the system of record; the work registry is a separate append-only event log. Neither is hand-edited. - **Stage-then-accept.** Captures land in gitignored `.sdlc/staged/`; accept at @@ -27,8 +28,8 @@ quietly and accepted at gates.** - **Projections are regenerable.** SQLite cache and Guide are pure projections of the ledger — one write path, parity by construction. -Older installs with scattered memory trees are converted by -`sdlc-engine storage migrate` ([migration](#migrating-a-legacy-install)). +- **One engine.** Every read and write goes through the Python `sdlc_engine` + package; `sdlc.sh` only dispatches to it ([engine-v2.md](engine-v2.md)). ## The model at a glance @@ -42,9 +43,9 @@ Older installs with scattered memory trees are converted by | SQLite cache | `.sdlc/index.sqlite` | No (projection) | Opt-in local query cache, schema v5 | Everything the framework owns lives in a single folder, `/sdlc-spdd/` -(the *home*; `SDLC_HOME` overrides). Older sprawled root layouts still resolve -read-only until `upgrade-project.sh` consolidates them (leftovers are archived -under `sdlc-spdd/.sdlc/legacy-layout-archive/`). See the +(the *home*; `SDLC_HOME` overrides). Paths in the table above are relative to +the home. There is no root-layout fallback: `upgrade-project.sh` refuses a +pre-v3 tree and the ops console reports it as `unsupported`. See the [install layout diagram](diagrams/09-install-layout.svg) and [installing into your project](installing-into-your-project.md). @@ -164,7 +165,7 @@ tools augment the same queries with cross-work graph context. Full guidance: | Path | Contents | |---|---| | `.sdlc/sessions/current-session.md` | Hot brief: workflow state, Resolved Context, Related Past Work digest, Resume Prompt | -| `.sdlc/sessions/--.md` | Timestamped briefs (rotated to `archive/`) | +| `.sdlc/sessions/--.md` | Timestamped briefs (ephemeral) | | `.sdlc/staged/lessons.jsonl` | Captures awaiting accept | | `.sdlc/index.sqlite` | Opt-in query cache (below) | | `.sdlc/pointer`, `.sdlc/workflows/` | Active Work ID pointer + phase/gate tracking | @@ -205,16 +206,19 @@ Archive **never truncates**, filters, or deletes `spdd/memory/lessons.jsonl`. De Policy: `sdlc-spdd/docs/research/dogfood-ledger-policy.md`. -## Migrating a legacy install +## Pre-v3 installs + +Legacy layouts are not migrated. `upgrade-project.sh` exits 3 and lists the +offending paths; `sdlc-engine installer detect` / the console report +`unsupported` with recommendation `reinit`. ```bash -sdlc-engine storage status # detect non-v3 layouts -sdlc-engine storage migrate # one-shot → ledger + registry -./scripts/upgrade-project.sh --target . --all # always consolidates into sdlc-spdd/ -sdlc-engine context parity --repair # rebuild projections +git rm -r agent-context spdd requirements session-notes .sdlc # whatever it lists +./scripts/init-project.sh --target . # fresh sdlc-spdd/ home +sdlc-engine context parity --repair # rebuild projections ``` -See [framework upgrade](framework-upgrade.md). +Git history keeps the old files. See [framework upgrade](framework-upgrade.md). ## Related diff --git a/docs/useful-concepts-and-commands.md b/docs/useful-concepts-and-commands.md index 0110c8cf..16e3b3a5 100644 --- a/docs/useful-concepts-and-commands.md +++ b/docs/useful-concepts-and-commands.md @@ -136,7 +136,7 @@ Create one: The active Work ID on **this machine**. Stored in `.sdlc/pointer` (gitignored). Guarded commands (for example `sdlc.sh capture`) refuse to run when the pointer does not match the requested Work ID. ./sdlc-spdd/scripts/sdlc.sh claim # sets pointer + team registry - ./agent-context/sdlc-pointer.sh get + ./sdlc-spdd/scripts/sdlc.sh pointer get See [agent-context/README.md](../agent-context/README.md#sdlc-pointer-current-choretask). diff --git a/engine/README.md b/engine/README.md index 2e637b2d..938b50a2 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): +`scripts/sdlc.sh` is a thin dispatcher to `python -m sdlc_engine` (no shell fallback): ```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 @@ -58,7 +56,7 @@ SDLC_ENGINE=auto ./scripts/sdlc.sh next # python if importable, else shell | `phases` | Phase order, gates, recommended assistant commands | | `pointer` | `.sdlc/pointer` get/set/reset + guarded run | | `workflow` | Resume/advance/skip/shelf/sync/next/status | -| `registry` | `spdd/memory/registry.jsonl` claim/release/team/list-work (legacy TSV fallback) | +| `registry` | `spdd/memory/registry.jsonl` claim/release/team/list-work | | `archive` | Remove Complete/Cancelled work artifacts (git history retains them) | | `canvas` | Final Status + next-operation inference | | `links` / `sync_local` | Milestone↔canvas↔registry drift check/repair + ROADMAP sync | @@ -71,7 +69,8 @@ SDLC_ENGINE=auto ./scripts/sdlc.sh next # python if importable, else shell | `sunset` | Close-out snapshot: GitHub PR + issue + commits + Jira → lesson ledger | | `viewer` | ADF WYSIWYG editor for checked-in `adf/*.json` (optional `[viewer]` / Flask) | | `installer` / `console` / `dashboard` | **EXPERIMENTAL** ops console: install/upgrade, SQLite, rollback, Guide+Neo4j, ADF viewer lifecycle (optional `[viewer]` / Flask) | -| `cli` / `cli_commands` / `cli_parser` | `sdlc-engine` / `python -m sdlc_engine` | +| `session` | Session bridge for `start-agent-session.sh` / `capture-session-memory.sh` (touch, brief, record-capture, Jira status, recommend) | +| `cli` / `cli_parser` / `commands/` | `sdlc-engine` / `python -m sdlc_engine`; handlers split into `commands/{state,team,context,integrations,storage}.py` | Two local GUIs + Guide map: [docs/ops-console.md](../docs/ops-console.md) (Guide pin for console dogfood: **`jmjava/orch-guide`** tag **`sdlc-spdd-projection-v2`**). @@ -85,9 +84,8 @@ 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. +- One engine: the bash workflow twins and `SDLC_ENGINE` / `SDLC_GATE_ENGINE` switches are gone. Installed `sdlc.sh` requires an importable `sdlc_engine`. +- Only the v3 `sdlc-spdd/` home is read; pre-v3 layouts are refused by `upgrade-project.sh`. ## Tests diff --git a/sdlc-spdd/docs/adf-viewer.md b/sdlc-spdd/docs/adf-viewer.md index 56484167..9521d41d 100644 --- a/sdlc-spdd/docs/adf-viewer.md +++ b/sdlc-spdd/docs/adf-viewer.md @@ -31,7 +31,7 @@ Flask is an optional extra so the core engine stays stdlib-only. # Preferred wrappers ./scripts/sdlc.sh viewer --port 5050 # or -SDLC_ENGINE=python ./scripts/sdlc.sh viewer --port 5050 +./scripts/sdlc.sh viewer --port 5050 # Direct python3 -m sdlc_engine.viewer --root . --port 5050 @@ -67,7 +67,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/sdlc-spdd/docs/agent-session-scripts.md b/sdlc-spdd/docs/agent-session-scripts.md index c47d1747..a26acc42 100644 --- a/sdlc-spdd/docs/agent-session-scripts.md +++ b/sdlc-spdd/docs/agent-session-scripts.md @@ -20,7 +20,7 @@ They solve four operational needs: | `sdlc-spdd/scripts/start-agent-session.sh` | Target-local script that creates a session brief for a new agent | | `sdlc-spdd/scripts/resync-agent-session.sh` | Target-local script that checks or reconciles the canonical canvas, validates it, and creates a session brief | | `sdlc-spdd/scripts/capture-session-memory.sh` | Stage session summary, decisions/pitfalls/patterns, and optional metrics as lesson records in `.sdlc/staged/lessons.jsonl` | -| `sdlc-spdd/scripts/accept-lessons.sh` | Promote staged lesson records into the committed `spdd/memory/lessons.jsonl` (also `sdlc.sh accept`) | +| `sdlc-spdd/scripts/sdlc.sh accept` | Promote staged lesson records into the committed `spdd/memory/lessons.jsonl` | | `sdlc-spdd/scripts/index-spdd-analysis.sh` | Stage an `analysis` lesson record from a Fowler analysis artifact | | `sdlc-spdd/scripts/resolve-agent-context.sh` | Resolve SDLC Agents `#SkillName` / phase extensions for progressive loading | | `sdlc-spdd/scripts/resolve-context-backend.sh` | Runtime probe: is the Guide DICE backend enabled and reachable? | @@ -75,7 +75,7 @@ Preview first: ./scripts/upgrade-project.sh --target /path/to/app --all --dry-run -The upgrade updates framework-owned prompts, harness/skills files, target-local docs under `docs/sdlc-spdd/`, and target-local runtime scripts. It preserves application source, application docs outside `docs/sdlc-spdd/`, requirements, canvases, reviews, sync logs, the lessons ledger, existing root `CLAUDE.md`, and target workflow customizations. Legacy memory layouts are converted by `sdlc-engine storage migrate` — see [Framework upgrade](framework-upgrade.md). +The upgrade updates framework-owned prompts, harness/skills files, target-local docs under `docs/sdlc-spdd/`, and target-local runtime scripts. It preserves application source, application docs outside `docs/sdlc-spdd/`, requirements, canvases, reviews, sync logs, the lessons ledger, existing root `CLAUDE.md`, and target workflow customizations. Pre-v3 layouts are refused (re-initialize instead) — see [Framework upgrade](framework-upgrade.md). ## 2. Start a New Agent Session diff --git a/sdlc-spdd/docs/framework-upgrade.md b/sdlc-spdd/docs/framework-upgrade.md index 17a52d60..7d429444 100644 --- a/sdlc-spdd/docs/framework-upgrade.md +++ b/sdlc-spdd/docs/framework-upgrade.md @@ -53,27 +53,16 @@ managed grounding block inside `CLAUDE.md`: ... -## Storage migration and consolidation +## Layout contract (v3 only) -After upgrading framework files, run the engine storage commands from the target -project root: - -```bash -# Detect legacy layouts and show what would change -sdlc-engine storage status - -# One-shot: convert legacy memory trees → ledger + registry -sdlc-engine storage migrate [--dry-run] - -# Move scattered framework paths into the single sdlc-spdd/ home -./scripts/upgrade-project.sh --target . --all -``` - -Migration exports converted originals aside; the committed system of record becomes -`spdd/memory/lessons.jsonl` + `spdd/memory/registry.jsonl`. Hot runtime stays +The upgrade only operates on a `sdlc-spdd/` home. If it finds a pre-v3 layout +(root `spdd/`, `agent-context/`, `work-registry.tsv`, root `.sdlc/`) it exits +non-zero with a message telling you to re-initialize; there is no migration or +consolidation path. The committed system of record is +`spdd/memory/lessons.jsonl` + `spdd/memory/registry.jsonl`; hot runtime stays gitignored under `.sdlc/`. Full model: [Storage v3](storage-v3.md). -Verify parity after migration: +Verify parity after an upgrade: ```bash sdlc-engine context parity diff --git a/sdlc-spdd/docs/installing-into-your-project.md b/sdlc-spdd/docs/installing-into-your-project.md index 3b24e689..bbdeba5d 100644 --- a/sdlc-spdd/docs/installing-into-your-project.md +++ b/sdlc-spdd/docs/installing-into-your-project.md @@ -139,10 +139,9 @@ If the project already has SDLC-SPDD files from an older version: ./scripts/upgrade-project.sh --target /path/to/app --all The upgrade script updates framework-owned files and preserves application source, -requirements, canvases, reviews, sync logs, and the lessons ledger. Legacy memory -layouts are converted by `sdlc-engine storage migrate`; consolidating scattered -paths into `sdlc-spdd/` — run `upgrade-project.sh` (consolidation is always-on) — see -[Framework upgrade](framework-upgrade.md). +requirements, canvases, reviews, sync logs, and the lessons ledger. It only +operates on the `sdlc-spdd/` home; pre-v3 layouts are refused and must be +re-initialized — see [Framework upgrade](framework-upgrade.md). Backups of overwritten framework files are stored under: diff --git a/sdlc-spdd/docs/maintaining-your-project.md b/sdlc-spdd/docs/maintaining-your-project.md index 96ccb68d..4610f164 100644 --- a/sdlc-spdd/docs/maintaining-your-project.md +++ b/sdlc-spdd/docs/maintaining-your-project.md @@ -31,7 +31,7 @@ From the orchestrator repository: ./scripts/upgrade-project.sh --target /path/to/app --all The upgrade preserves application work, requirements, canvases, and the committed -lessons ledger. Legacy layouts are migrated by `sdlc-engine storage migrate` — see +lessons ledger. Pre-v3 layouts are refused (re-initialize) — see [Framework upgrade](framework-upgrade.md). Review backups under: @@ -117,7 +117,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/sdlc-spdd/docs/research/evaluation-protocol.md b/sdlc-spdd/docs/research/evaluation-protocol.md index 90f41abd..54df090a 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` (the only engine; `sdlc.sh` is a dispatcher to it) 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. +There is no shell gate engine. The bash workflow twins and the `SDLC_ENGINE` / `SDLC_GATE_ENGINE` switches were removed (REF-003), so every gate evaluation is Python `gate_check`; a shell fallback is not an evaluation condition because it no longer exists. --- @@ -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 …` (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` is the only gate engine (no shell fallback exists) | 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.” @@ -197,7 +197,7 @@ Do not pool runs across model versions. |---------|----------------|-------| | `examples/spring-boot-order-api/` has **no Java** sources | Intended gold cannot score C-DRIFT vs implementation | TEST-002 | | Live consumer matrix is **Cursor-oriented** | RQ5/C-PORT cannot be claimed from that harness | TEST-002 / live-consumer | -| Dual engine (shell vs Python `gate_check`) | Confound for “the method” | REF-001 Complete (Python `gate_check` is SUT; `SDLC_GATE_ENGINE=shell` is not an eval condition) | +| Dual engine (shell vs Python `gate_check`) | Confound for “the method” | REF-001 named Python `gate_check` as SUT; REF-003 removed the shell engine entirely | | Committed `lessons.jsonl` was empty after Milestone 1 archive | RQ4 dogfood retrieve was vacuous until seeded | CHORE-003 Complete (seed + archive leave-ledger; usefulness still unmeasured) | | Existence-only gates | RQ2 semantic arm needs raters until FEAT-014/016 | FEAT-014, FEAT-016 | | Retrieve is exact keyword-list filter | RQ3 relevance is rater-only until FEAT-017 | FEAT-017 (lexical title-body vs keyword-list; DICE still unmeasured) | 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..a3d2163e 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 (REF-001 named the SUT; REF-003 deleted the shell engine and its switches). The research SUT is Python `gate_check`; `sdlc.sh` only dispatches to it. | | `--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` is the only gate engine (no shell fallback exists) | 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` via `sdlc.sh gate` (single engine) | [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/docs/storage-v3.md b/sdlc-spdd/docs/storage-v3.md index 42460a97..dabd36bb 100644 --- a/sdlc-spdd/docs/storage-v3.md +++ b/sdlc-spdd/docs/storage-v3.md @@ -13,7 +13,8 @@ quietly and accepted at gates.** - **One home.** Framework-owned paths live under `/sdlc-spdd/`. Contracts, harness, scripts, and ledgers share one install layout (this orchestrator dogfoods the same layout). Install *source* lives under `templates/`; root - `scripts/` is orchestrator tooling only. Leftover `agent-context/` is archived. + `scripts/` is orchestrator tooling only. Pre-v3 layouts (`agent-context/`, + root `spdd/`, `work-registry.tsv`) are unsupported and never migrated. - **Ledger-first.** One committed JSONL lessons file is the system of record; the work registry is a separate append-only event log. Neither is hand-edited. - **Stage-then-accept.** Captures land in gitignored `.sdlc/staged/`; accept at @@ -27,8 +28,8 @@ quietly and accepted at gates.** - **Projections are regenerable.** SQLite cache and Guide are pure projections of the ledger — one write path, parity by construction. -Older installs with scattered memory trees are converted by -`sdlc-engine storage migrate` ([migration](#migrating-a-legacy-install)). +- **One engine.** Every read and write goes through the Python `sdlc_engine` + package; `sdlc.sh` only dispatches to it ([engine-v2.md](engine-v2.md)). ## The model at a glance @@ -42,9 +43,9 @@ Older installs with scattered memory trees are converted by | SQLite cache | `.sdlc/index.sqlite` | No (projection) | Opt-in local query cache, schema v5 | Everything the framework owns lives in a single folder, `/sdlc-spdd/` -(the *home*; `SDLC_HOME` overrides). Older sprawled root layouts still resolve -read-only until `upgrade-project.sh` consolidates them (leftovers are archived -under `sdlc-spdd/.sdlc/legacy-layout-archive/`). See the +(the *home*; `SDLC_HOME` overrides). Paths in the table above are relative to +the home. There is no root-layout fallback: `upgrade-project.sh` refuses a +pre-v3 tree and the ops console reports it as `unsupported`. See the [install layout diagram](diagrams/09-install-layout.svg) and [installing into your project](installing-into-your-project.md). @@ -164,7 +165,7 @@ tools augment the same queries with cross-work graph context. Full guidance: | Path | Contents | |---|---| | `.sdlc/sessions/current-session.md` | Hot brief: workflow state, Resolved Context, Related Past Work digest, Resume Prompt | -| `.sdlc/sessions/--.md` | Timestamped briefs (rotated to `archive/`) | +| `.sdlc/sessions/--.md` | Timestamped briefs (ephemeral) | | `.sdlc/staged/lessons.jsonl` | Captures awaiting accept | | `.sdlc/index.sqlite` | Opt-in query cache (below) | | `.sdlc/pointer`, `.sdlc/workflows/` | Active Work ID pointer + phase/gate tracking | @@ -205,16 +206,19 @@ Archive **never truncates**, filters, or deletes `spdd/memory/lessons.jsonl`. De Policy: `sdlc-spdd/docs/research/dogfood-ledger-policy.md`. -## Migrating a legacy install +## Pre-v3 installs + +Legacy layouts are not migrated. `upgrade-project.sh` exits 3 and lists the +offending paths; `sdlc-engine installer detect` / the console report +`unsupported` with recommendation `reinit`. ```bash -sdlc-engine storage status # detect non-v3 layouts -sdlc-engine storage migrate # one-shot → ledger + registry -./scripts/upgrade-project.sh --target . --all # always consolidates into sdlc-spdd/ -sdlc-engine context parity --repair # rebuild projections +git rm -r agent-context spdd requirements session-notes .sdlc # whatever it lists +./scripts/init-project.sh --target . # fresh sdlc-spdd/ home +sdlc-engine context parity --repair # rebuild projections ``` -See [framework upgrade](framework-upgrade.md). +Git history keeps the old files. See [framework upgrade](framework-upgrade.md). ## Related diff --git a/sdlc-spdd/docs/useful-concepts-and-commands.md b/sdlc-spdd/docs/useful-concepts-and-commands.md index 0110c8cf..16e3b3a5 100644 --- a/sdlc-spdd/docs/useful-concepts-and-commands.md +++ b/sdlc-spdd/docs/useful-concepts-and-commands.md @@ -136,7 +136,7 @@ Create one: The active Work ID on **this machine**. Stored in `.sdlc/pointer` (gitignored). Guarded commands (for example `sdlc.sh capture`) refuse to run when the pointer does not match the requested Work ID. ./sdlc-spdd/scripts/sdlc.sh claim # sets pointer + team registry - ./agent-context/sdlc-pointer.sh get + ./sdlc-spdd/scripts/sdlc.sh pointer get See [agent-context/README.md](../agent-context/README.md#sdlc-pointer-current-choretask). 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/spec/commands/lifecycle-commit-message.spec.md b/spec/commands/lifecycle-commit-message.spec.md index a191f34b..15777107 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. 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..d505d0da 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`. 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 3a83a585..cedb21d5 100644 --- a/templates/agent-context/README.md +++ b/templates/agent-context/README.md @@ -49,7 +49,7 @@ 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 +Every verb runs in the Python engine (`python -m sdlc_engine`) — 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..a38c32a6 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. 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..195b861d 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`. 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..f14f1715 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. 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..e9d17bbe 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`. 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..a38c32a6 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. 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..1f8d268f 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`. 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 ``` From 4af874c0b32e8476371bd2789d8bb0a7fbeccbbf Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 14 Sep 2026 00:51:23 +0000 Subject: [PATCH 20/22] engine: stop pre-creating spdd/memory/entries in ensure_runtime_dirs The progress ledger writer creates its own parent; runtime setup must not leave committed-tree side effects on every engine call (capture stages a JSONL record only). Co-authored-by: John Menke --- engine/src/sdlc_engine/project.py | 1 - 1 file changed, 1 deletion(-) diff --git a/engine/src/sdlc_engine/project.py b/engine/src/sdlc_engine/project.py index b06a0d36..ea8e6e2c 100644 --- a/engine/src/sdlc_engine/project.py +++ b/engine/src/sdlc_engine/project.py @@ -196,4 +196,3 @@ def ensure_runtime_dirs(self) -> None: self.workflows_dir.mkdir(parents=True, exist_ok=True) self.hot_session_dir().mkdir(parents=True, exist_ok=True) self.staged_ledger_path.parent.mkdir(parents=True, exist_ok=True) - (self.home / "spdd" / "memory" / "entries").mkdir(parents=True, exist_ok=True) From f133bf0e0e3e6e3be7e26c36d792228793f73908 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 14 Sep 2026 01:04:38 +0000 Subject: [PATCH 21/22] ci/tests: CLI smoke without SDLC_ENGINE; e2e fixtures use the sdlc-spdd home - test-sdlc-engine.yml smoke step exported SDLC_ENGINE=python, which the dispatcher now rejects by design (exit 2) - engine/tests_e2e wrote requirements/, spdd/ and .sdlc/ at the target root; the engine reads them under /sdlc-spdd, so issue-sync tests failed with FileNotFoundError before any gh call Co-authored-by: John Menke --- .github/workflows/test-sdlc-engine.yml | 6 ++--- engine/tests_e2e/conftest.py | 2 +- .../test_context_store_guide_live.py | 4 +-- .../test_guide_projection_roundtrip.py | 6 ++--- .../test_issues_github_integration.py | 10 +++---- .../tests_e2e/test_vue3_console_playwright.py | 26 +++++++++---------- 6 files changed, 27 insertions(+), 27 deletions(-) diff --git a/.github/workflows/test-sdlc-engine.yml b/.github/workflows/test-sdlc-engine.yml index 3724faa4..ecc73648 100644 --- a/.github/workflows/test-sdlc-engine.yml +++ b/.github/workflows/test-sdlc-engine.yml @@ -76,9 +76,9 @@ jobs: --cov-fail-under=90 - name: CLI smoke via sdlc.sh run: | - SDLC_ENGINE=python ./scripts/sdlc.sh version - SDLC_ENGINE=python ./scripts/sdlc.sh next - SDLC_ENGINE=python ./scripts/sdlc.sh viewer --help + ./scripts/sdlc.sh version + ./scripts/sdlc.sh next + ./scripts/sdlc.sh viewer --help - name: Shim regression harness run: ./tests/test-sdlc-engine-shim.sh diff --git a/engine/tests_e2e/conftest.py b/engine/tests_e2e/conftest.py index dcdfe569..8b156542 100644 --- a/engine/tests_e2e/conftest.py +++ b/engine/tests_e2e/conftest.py @@ -73,7 +73,7 @@ def live_console(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): def _save(target: Path | str, host: str, port: int) -> None: root = Path(target).expanduser().resolve() - (root / ".sdlc").mkdir(parents=True, exist_ok=True) + (root / "sdlc-spdd" / ".sdlc").mkdir(parents=True, exist_ok=True) vr._save_runtime( root, { diff --git a/engine/tests_e2e/test_context_store_guide_live.py b/engine/tests_e2e/test_context_store_guide_live.py index 8b969afb..8ff714ae 100644 --- a/engine/tests_e2e/test_context_store_guide_live.py +++ b/engine/tests_e2e/test_context_store_guide_live.py @@ -39,10 +39,10 @@ def _seed_canvas(root: Path, work_id: str) -> None: - req = root / "requirements" / "milestones" / f"{work_id}.md" + req = root / "sdlc-spdd" / "requirements" / "milestones" / f"{work_id}.md" req.parent.mkdir(parents=True, exist_ok=True) req.write_text(f"# Requirement: {work_id}\n\n## Summary\nProof.\n", encoding="utf-8") - canvas = root / "spdd" / "canvas" / f"{work_id}.md" + canvas = root / "sdlc-spdd" / "spdd" / "canvas" / f"{work_id}.md" canvas.parent.mkdir(parents=True, exist_ok=True) canvas.write_text( f"""# REASONS Canvas: {work_id} diff --git a/engine/tests_e2e/test_guide_projection_roundtrip.py b/engine/tests_e2e/test_guide_projection_roundtrip.py index 54e90340..16723e3e 100644 --- a/engine/tests_e2e/test_guide_projection_roundtrip.py +++ b/engine/tests_e2e/test_guide_projection_roundtrip.py @@ -44,10 +44,10 @@ def seeded_project() -> tuple[Path, str, str, str]: work_id = f"{WORK_PREFIX}-{uuid.uuid4().hex[:8]}" area = "engine-tests" body = f"{MARKER}-{uuid.uuid4().hex[:8]}" - req = root / "requirements" / "milestones" / f"{work_id}.md" + req = root / "sdlc-spdd" / "requirements" / "milestones" / f"{work_id}.md" req.parent.mkdir(parents=True, exist_ok=True) req.write_text(f"# Requirement: {work_id}\n\n## Summary\nRoundtrip seed.\n", encoding="utf-8") - canvas = root / "spdd" / "canvas" / f"{work_id}.md" + canvas = root / "sdlc-spdd" / "spdd" / "canvas" / f"{work_id}.md" canvas.parent.mkdir(parents=True, exist_ok=True) canvas.write_text( f"""# REASONS Canvas: {work_id} @@ -64,7 +64,7 @@ def seeded_project() -> tuple[Path, str, str, str]: """, encoding="utf-8", ) - (root / "spdd" / "memory").mkdir(parents=True, exist_ok=True) + (root / "sdlc-spdd" / "spdd" / "memory").mkdir(parents=True, exist_ok=True) save_config( root, { diff --git a/engine/tests_e2e/test_issues_github_integration.py b/engine/tests_e2e/test_issues_github_integration.py index 9f650e82..fae316c0 100644 --- a/engine/tests_e2e/test_issues_github_integration.py +++ b/engine/tests_e2e/test_issues_github_integration.py @@ -39,7 +39,7 @@ def _repo() -> str: def _seed(root: Path, work_id: str, *, number: str = "TBD") -> Path: - req = root / "requirements" / "milestones" / f"{work_id}.md" + req = root / "sdlc-spdd" / "requirements" / "milestones" / f"{work_id}.md" req.parent.mkdir(parents=True, exist_ok=True) req.write_text( f"""# Requirement: {work_id} @@ -62,7 +62,7 @@ def _seed(root: Path, work_id: str, *, number: str = "TBD") -> Path: """, encoding="utf-8", ) - canvas = root / "spdd" / "canvas" / f"{work_id}.md" + canvas = root / "sdlc-spdd" / "spdd" / "canvas" / f"{work_id}.md" canvas.parent.mkdir(parents=True, exist_ok=True) canvas.write_text( f"""# REASONS Canvas: {work_id} - GH integration @@ -150,14 +150,14 @@ def test_github_pull_existing_issue_apply(tmp_path: Path, gh_ready: str, monkeyp assert f"GitHub #{number}" in report assert remote["title"] in report - text = (tmp_path / "requirements" / "milestones" / f"{work_id}.md").read_text( + text = (tmp_path / "sdlc-spdd" / "requirements" / "milestones" / f"{work_id}.md").read_text( encoding="utf-8" ) assert f"Number: {number}" in text assert f"Title: {remote['title']}" in text assert remote["url"] in text - canvas = (tmp_path / "spdd" / "canvas" / f"{work_id}.md").read_text(encoding="utf-8") + canvas = (tmp_path / "sdlc-spdd" / "spdd" / "canvas" / f"{work_id}.md").read_text(encoding="utf-8") assert f"#{number}" in canvas or number in canvas @@ -168,7 +168,7 @@ def test_github_push_create_pull_roundtrip(tmp_path: Path, gh_ready: str, monkey work_id = "FEAT-911-gh-create-live" _seed(tmp_path, work_id, number="TBD") # Avoid labels — repos may not have them. - req = tmp_path / "requirements" / "milestones" / f"{work_id}.md" + req = tmp_path / "sdlc-spdd" / "requirements" / "milestones" / f"{work_id}.md" text = req.read_text(encoding="utf-8") req.write_text(text.replace("- Labels: sdlc\n", "- Labels:\n"), encoding="utf-8") # ensure title present without labels section issues diff --git a/engine/tests_e2e/test_vue3_console_playwright.py b/engine/tests_e2e/test_vue3_console_playwright.py index 67dde79d..4e8eef2a 100644 --- a/engine/tests_e2e/test_vue3_console_playwright.py +++ b/engine/tests_e2e/test_vue3_console_playwright.py @@ -86,7 +86,7 @@ def vue_dist() -> Path: def _seed_work(root: Path, work_id: str, summary: str) -> None: - milestones = root / "requirements" / "milestones" + milestones = root / "sdlc-spdd" / "requirements" / "milestones" milestones.mkdir(parents=True, exist_ok=True) (milestones / f"{work_id}.md").write_text( f"## Summary\n\n{summary}\n", @@ -95,7 +95,7 @@ def _seed_work(root: Path, work_id: str, summary: str) -> None: def _seed_issue_work(root: Path, work_id: str) -> None: - req = root / "requirements" / "milestones" / f"{work_id}.md" + req = root / "sdlc-spdd" / "requirements" / "milestones" / f"{work_id}.md" req.parent.mkdir(parents=True, exist_ok=True) req.write_text( f"""--- @@ -125,7 +125,7 @@ def _seed_issue_work(root: Path, work_id: str) -> None: """, encoding="utf-8", ) - canvas = root / "spdd" / "canvas" / f"{work_id}.md" + canvas = root / "sdlc-spdd" / "spdd" / "canvas" / f"{work_id}.md" canvas.parent.mkdir(parents=True, exist_ok=True) canvas.write_text( f"""# REASONS Canvas: {work_id} - Demo @@ -139,8 +139,8 @@ def _seed_issue_work(root: Path, work_id: str) -> None: """, encoding="utf-8", ) - (root / "spdd" / "memory").mkdir(parents=True, exist_ok=True) - reg = root / "spdd" / "memory" / "registry.jsonl" + (root / "sdlc-spdd" / "spdd" / "memory").mkdir(parents=True, exist_ok=True) + reg = root / "sdlc-spdd" / "spdd" / "memory" / "registry.jsonl" if not reg.is_file(): reg.write_text("", encoding="utf-8") @@ -162,7 +162,7 @@ def live_vue_console(tmp_path: Path, vue_dist: Path, monkeypatch: pytest.MonkeyP def _save(target: Path | str, host: str, port: int) -> None: root = Path(target).expanduser().resolve() - (root / ".sdlc").mkdir(parents=True, exist_ok=True) + (root / "sdlc-spdd" / ".sdlc").mkdir(parents=True, exist_ok=True) vr._save_runtime( root, { @@ -339,8 +339,8 @@ def test_vue3_dashboard_configure_opens_issues(page, live_vue_console) -> None: def test_vue3_dashboard_jumps_to_templates_with_work_id(page, live_vue_console) -> None: # type: ignore[no-untyped-def] target = Path(live_vue_console["target"]) work_id = live_vue_console["works"]["feature"] - (target / ".sdlc").mkdir(parents=True, exist_ok=True) - (target / ".sdlc" / "pointer").write_text(work_id + "\n", encoding="utf-8") + (target / "sdlc-spdd" / ".sdlc").mkdir(parents=True, exist_ok=True) + (target / "sdlc-spdd" / ".sdlc" / "pointer").write_text(work_id + "\n", encoding="utf-8") _goto_vue(page, live_vue_console) page.get_by_test_id("dashboard-panel").wait_for(state="visible") page.wait_for_function( @@ -385,7 +385,7 @@ def test_vue3_persistence_load_and_save(page, live_vue_console) -> None: # type }""" ) assert page.get_by_test_id("ps-guide").inner_text().strip() == "OFF" - cfg = json.loads((target / ".sdlc" / "persistence-config.json").read_text(encoding="utf-8")) + cfg = json.loads((target / "sdlc-spdd" / ".sdlc" / "persistence-config.json").read_text(encoding="utf-8")) assert "git-pointers" in cfg["backends"] assert "sqlite" in cfg["backends"] assert "guide-dice" not in cfg["backends"] @@ -713,7 +713,7 @@ def test_vue3_issues_integrations_save_and_tracker_toggle(page, live_vue_console assert page.get_by_test_id("issues-link-github").is_visible() assert not page.get_by_test_id("issues-link-jira").is_visible() cfg = json.loads( - (Path(live_vue_console["target"]) / ".sdlc" / "integrations-config.json").read_text( + (Path(live_vue_console["target"]) / "sdlc-spdd" / ".sdlc" / "integrations-config.json").read_text( encoding="utf-8" ) ) @@ -816,7 +816,7 @@ def test_vue3_guide_save_writes_config(page, live_vue_console) -> None: # type: """() => (document.querySelector('[data-testid="guide-action-status"]')?.textContent || '') .includes('Config saved')""" ) - cfg = json.loads((target / ".sdlc" / "guide-config.json").read_text(encoding="utf-8")) + cfg = json.loads((target / "sdlc-spdd" / ".sdlc" / "guide-config.json").read_text(encoding="utf-8")) assert cfg["notes"] == "vue3-playwright-guide" assert int(cfg["port"]) == 21338 @@ -954,14 +954,14 @@ def test_vue3_adf_browse_select_and_init(page, live_vue_console, monkeypatch) -> """() => (document.querySelector('[data-testid="adf-init-status"]')?.textContent || '') .includes('Would create FEAT-013-playwright-adf-init')""" ) - assert not (target / "spdd" / "canvas" / "FEAT-013-playwright-adf-init.md").exists() + assert not (target / "sdlc-spdd" / "spdd" / "canvas" / "FEAT-013-playwright-adf-init.md").exists() page.get_by_test_id("btn-adf-init").click() page.wait_for_function( """() => (document.querySelector('[data-testid="adf-init-status"]')?.textContent || '') .includes('Created FEAT-013-playwright-adf-init')""" ) - canvas = target / "spdd" / "canvas" / "FEAT-013-playwright-adf-init.md" + canvas = target / "sdlc-spdd" / "spdd" / "canvas" / "FEAT-013-playwright-adf-init.md" assert canvas.is_file() text = canvas.read_text(encoding="utf-8") assert "Playwright title" in text From fb0ba93c1a89f92be34c706561a5cd441adebec5 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 14 Sep 2026 01:04:38 +0000 Subject: [PATCH 22/22] engine: bring REF-003/005 additions under the complexity gate check-complexity.py now recognises functions moved between changed/deleted files (judged against their old CCN instead of as NEW) and silences git noise for files absent from the base. The seven genuine findings are refactored: cli.main, detect_target, cmd_accept, cmd_session, cmd_shell, SessionService.brief_markdown and SessionService.capture split helpers out so every function is at or below CCN 10. Co-authored-by: John Menke --- engine/src/sdlc_engine/cli.py | 25 +++--- engine/src/sdlc_engine/commands/context.py | 93 ++++++++++++---------- engine/src/sdlc_engine/commands/state.py | 18 +++-- engine/src/sdlc_engine/installer/detect.py | 52 ++++++------ engine/src/sdlc_engine/session.py | 67 ++++++++++------ scripts/check-complexity.py | 83 ++++++++++++++----- 6 files changed, 211 insertions(+), 127 deletions(-) diff --git a/engine/src/sdlc_engine/cli.py b/engine/src/sdlc_engine/cli.py index 772ea413..28e8d59a 100644 --- a/engine/src/sdlc_engine/cli.py +++ b/engine/src/sdlc_engine/cli.py @@ -2,6 +2,7 @@ from __future__ import annotations +import argparse import sys from .cli_parser import build_parser @@ -11,19 +12,23 @@ PASSTHROUGH_VERBS = frozenset({"capture", "complete"}) +def _parse(parser: argparse.ArgumentParser, argv: list[str]) -> argparse.Namespace: + """Parse argv; capture/complete forward unknown options to the session script.""" + args, extra = parser.parse_known_args(argv) + if not extra: + return args + if getattr(args, "command", None) not in PASSTHROUGH_VERBS: + parser.error(f"unrecognized arguments: {' '.join(extra)}") + args.script_args = list(getattr(args, "script_args", None) or []) + extra + return args + + def main(argv: list[str] | None = None) -> int: argv = list(sys.argv[1:] if argv is None else argv) - parser = build_parser() - args, extra = parser.parse_known_args(argv) - if extra: - if getattr(args, "command", None) in PASSTHROUGH_VERBS: - # capture/complete forward unknown options to the session script. - args.script_args = list(getattr(args, "script_args", None) or []) + extra - else: - parser.error(f"unrecognized arguments: {' '.join(extra)}") - if getattr(args, "version", False) and not getattr(args, "command", None): - return cmd_version(args) + args = _parse(build_parser(), argv) if not getattr(args, "command", None): + if getattr(args, "version", False): + return cmd_version(args) # default to next for parity with sdlc.sh args.command = "next" args.func = cmd_next diff --git a/engine/src/sdlc_engine/commands/context.py b/engine/src/sdlc_engine/commands/context.py index 327800f9..792d5243 100644 --- a/engine/src/sdlc_engine/commands/context.py +++ b/engine/src/sdlc_engine/commands/context.py @@ -6,10 +6,15 @@ import json import subprocess import sys +from typing import TYPE_CHECKING, Any, Callable + from ..context_store import ContextStore from .state import _project +if TYPE_CHECKING: + from ..project import Project + def cmd_context(args: argparse.Namespace) -> int: from ..lessons_ledger import LEDGER_KINDS @@ -262,39 +267,47 @@ def cmd_complete(args: argparse.Namespace) -> int: return SessionService(_project(args)).complete(list(args.script_args or [])) +def _list_staged(ledger: Any) -> int: + staged = [r for r in ledger.records(include_staged=True) if r.id in ledger.staged_ids()] + if not staged: + print("No staged records.") + return 0 + for rec in staged: + print(f"{rec.id}\t{rec.kind}\t{rec.work_id}\t{rec.title}") + return 0 + + +def _commit_ledger(project: Project, count: int, wid: str) -> None: + if count == 0 or not project.ledger_path.is_file(): + print("Nothing to commit (no records promoted).") + return + rel = project.rel(project.ledger_path) + subprocess.check_call(["git", "-C", str(project.root), "add", rel]) + subprocess.check_call( + ["git", "-C", str(project.root), "commit", "-m", f"memory: accept {count} lessons for {wid}"] + ) + print(f"Committed {rel}") + + def cmd_accept(args: argparse.Namespace) -> int: """Promote staged lessons to the committed ledger (optionally git-commit it).""" from ..lessons_ledger import LessonsLedger project = _project(args) - ledger = LessonsLedger(project) if getattr(args, "list", False): - staged = [r for r in ledger.records(include_staged=True) if r.id in ledger.staged_ids()] - if not staged: - print("No staged records.") - return 0 - for rec in staged: - print(f"{rec.id}\t{rec.kind}\t{rec.work_id}\t{rec.title}") - return 0 - store = ContextStore(project) - out = store.accept( - work_id=getattr(args, "work_id", "") or "", - ids=[i.strip() for i in (getattr(args, "ids", "") or "").split(",") if i.strip()] or None, + return _list_staged(LessonsLedger(project)) + wid = getattr(args, "work_id", "") or "" + ids = [i.strip() for i in (getattr(args, "ids", "") or "").split(",") if i.strip()] + out = ContextStore(project).accept( + work_id=wid, + ids=ids or None, discard_rest=bool(getattr(args, "discard_rest", False)), project_guide=not getattr(args, "no_guide", False), ) print(json.dumps(out, indent=2)) - count = int(out.get("accepted_count", 0) or 0) if getattr(args, "commit", False): - if count == 0 or not project.ledger_path.is_file(): - print("Nothing to commit (no records promoted).") - return 0 - rel = project.rel(project.ledger_path) - wid = getattr(args, "work_id", "") or "all" - subprocess.check_call(["git", "-C", str(project.root), "add", rel]) - subprocess.check_call(["git", "-C", str(project.root), "commit", "-m", f"memory: accept {count} lessons for {wid}"]) - print(f"Committed {rel}") + _commit_ledger(project, int(out.get("accepted_count", 0) or 0), wid or "all") return 0 @@ -304,26 +317,20 @@ def cmd_session(args: argparse.Namespace) -> int: project = _project(args) svc = SessionService(project) - action = args.session_cmd wid = getattr(args, "work_id", "") or "" - if action == "brief": - print(svc.brief_markdown(wid), end="") - return 0 - if action == "touch": - svc.touch_session(wid, args.phase, getattr(args, "milestone", "") or "") - return 0 - if action == "record-capture": - svc.record_capture(wid, getattr(args, "phase", "") or "resume") - return 0 - if action == "recommend": - print(svc.recommend(wid, args.phase, getattr(args, "operation", "") or "")) - return 0 - if action == "jira-status": - print(jira_status(project, wid)) - return 0 - if action == "jira-ask": - text = jira_ask_prompt(project, wid) - if text: - print(text) - return 0 - return 2 + phase = getattr(args, "phase", "") or "" + actions: dict[str, Callable[[], str | None]] = { + "brief": lambda: svc.brief_markdown(wid), + "touch": lambda: svc.touch_session(wid, phase, getattr(args, "milestone", "") or ""), + "record-capture": lambda: svc.record_capture(wid, phase or "resume"), + "recommend": lambda: svc.recommend(wid, phase, getattr(args, "operation", "") or "") + "\n", + "jira-status": lambda: jira_status(project, wid) + "\n", + "jira-ask": lambda: (lambda t: t + "\n" if t else "")(jira_ask_prompt(project, wid)), + } + fn = actions.get(args.session_cmd) + if fn is None: + return 2 + text = fn() + if text: + print(text, end="") + return 0 diff --git a/engine/src/sdlc_engine/commands/state.py b/engine/src/sdlc_engine/commands/state.py index 4aed3c4b..51de319e 100644 --- a/engine/src/sdlc_engine/commands/state.py +++ b/engine/src/sdlc_engine/commands/state.py @@ -6,6 +6,7 @@ import json import subprocess import sys +from pathlib import Path from .. import __version__ from ..local_sessions import LocalSessionService from ..phases import GATE_LABELS, advisory_gates_for_phase, format_advisory_gate_rows @@ -182,15 +183,20 @@ def cmd_version(_: argparse.Namespace) -> int: return 0 +def _find_script(project: Project, name: str) -> Path | None: + """Installed home first, then the orchestrator's scripts/; accepts bare or .sh names.""" + for base in (project.home / "scripts", project.root / "scripts"): + for cand in (base / name, base / f"{name}.sh"): + if cand.is_file(): + return cand + return None + + def cmd_shell(args: argparse.Namespace) -> int: """Bridge to the remaining shell session scripts (installed home first).""" project = _project(args) - name = args.script - candidates = [] - for base in (project.home / "scripts", project.root / "scripts"): - candidates += [base / name, base / f"{name}.sh"] - script = next((c for c in candidates if c.is_file()), None) + script = _find_script(project, args.script) if script is None: - print(f"shell bridge: script not found: {name}", file=sys.stderr) + print(f"shell bridge: script not found: {args.script}", file=sys.stderr) return 1 return subprocess.call([str(script), *args.script_args], cwd=project.root) diff --git a/engine/src/sdlc_engine/installer/detect.py b/engine/src/sdlc_engine/installer/detect.py index 5bd59588..9a02cd06 100644 --- a/engine/src/sdlc_engine/installer/detect.py +++ b/engine/src/sdlc_engine/installer/detect.py @@ -25,30 +25,36 @@ ) -def detect_target(target: Path | str) -> dict[str, Any]: - """Return install-mode diagnosis for ``target``.""" - root = Path(target).expanduser().resolve() - exists = root.is_dir() - markers_found = [rel for rel in MARKERS if exists and (root / rel).exists()] - unsupported_found = [rel for rel in UNSUPPORTED_MARKERS if exists and (root / rel).exists()] +ASSISTANT_MARKERS = { + "cursor": ".cursor/commands/sdlc-spdd-init.md", + "copilot": ".github/prompts/sdlc-spdd-init.prompt.md", + "claude": ".claude/commands/sdlc-spdd-init.md", +} + + +def _present(root: Path, rels: tuple[str, ...]) -> list[str]: + return [rel for rel in rels if (root / rel).exists()] - has_cursor = (root / ".cursor/commands/sdlc-spdd-init.md").is_file() if exists else False - has_copilot = (root / ".github/prompts/sdlc-spdd-init.prompt.md").is_file() if exists else False - has_claude = (root / ".claude/commands/sdlc-spdd-init.md").is_file() if exists else False +def _mode(exists: bool, unsupported: list[str], markers: list[str]) -> tuple[str, str]: if not exists: - mode = "missing" - recommendation = "create" - elif unsupported_found: + return "missing", "create" + if unsupported: # Matches upgrade-project.sh: any pre-v3 path blocks upgrade until removed. - mode = "unsupported" - recommendation = "reinit" - elif markers_found: - mode = "upgrade" - recommendation = "upgrade" - else: - mode = "fresh" - recommendation = "install" + return "unsupported", "reinit" + if markers: + return "upgrade", "upgrade" + return "fresh", "install" + + +def detect_target(target: Path | str) -> dict[str, Any]: + """Return install-mode diagnosis for ``target``.""" + root = Path(target).expanduser().resolve() + exists = root.is_dir() + markers_found = _present(root, MARKERS) if exists else [] + unsupported_found = _present(root, UNSUPPORTED_MARKERS) if exists else [] + assistants = {name: exists and (root / rel).is_file() for name, rel in ASSISTANT_MARKERS.items()} + mode, recommendation = _mode(exists, unsupported_found, markers_found) return { "path": str(root), @@ -57,9 +63,5 @@ def detect_target(target: Path | str) -> dict[str, Any]: "recommendation": recommendation, "markers": markers_found, "unsupported_markers": unsupported_found, - "assistants": { - "cursor": has_cursor, - "copilot": has_copilot, - "claude": has_claude, - }, + "assistants": assistants, } diff --git a/engine/src/sdlc_engine/session.py b/engine/src/sdlc_engine/session.py index e079f4fd..5d9ce70a 100644 --- a/engine/src/sdlc_engine/session.py +++ b/engine/src/sdlc_engine/session.py @@ -14,6 +14,7 @@ import sys from dataclasses import dataclass from pathlib import Path +from typing import Any from . import canvas as canvas_mod from .links import collect_links @@ -113,44 +114,56 @@ def recommend(self, work_id: str, phase: str, operation: str = "") -> str: # --- brief --- - def brief_markdown(self, work_id: str = "") -> str: - wid = (work_id or self.workflow.pointer.get() or "").strip() - if not wid: - return "No active Work ID. Run `./scripts/sdlc.sh resume `." - state = self.workflow.sync(wid) + def _canvas_facts(self, wid: str, state: Any) -> tuple[str, str, str, str]: + """(next op id, op title, effective operation, readiness label) from the canvas.""" canvas = self.project.canvas_path(wid) - op, title = canvas_mod.next_operation(canvas) if canvas.is_file() else ("", "") - operation = op or state.operation - readiness = "absent" - if canvas.is_file(): - raw = canvas_mod.extract_readiness_raw(canvas.read_text(encoding="utf-8")) - readiness = canvas_mod.normalize_readiness(raw) or raw or "absent" - pending = [ + if not canvas.is_file(): + return "", "", state.operation, "absent" + op, title = canvas_mod.next_operation(canvas) + raw = canvas_mod.extract_readiness_raw(canvas.read_text(encoding="utf-8")) + readiness = canvas_mod.normalize_readiness(raw) or raw or "absent" + return op, title, op or state.operation, readiness + + @staticmethod + def _pending_gates(state: Any) -> list[str]: + return [ GATE_LABELS.get(g, g) for g in gates_for_phase(state.phase) if state.gates.get(g, "pending") != "passed" ] + + def _brief_rows(self, wid: str, state: Any) -> list[str]: + op, title, operation, readiness = self._canvas_facts(wid, state) idx = PHASE_ORDER.index(state.phase) + 1 if state.phase in PHASE_ORDER else 0 - status = jira_status(self.project, wid) - ask = jira_ask_prompt(self.project, wid) - lines = [ + next_op = f"{op} — {title}" if op and title else (operation or "none") + status = "active" if state.active else "shelved" + return [ "| Field | Value |", "|-------|-------|", f"| Work ID | {wid} |", - f"| Workflow status | {'active' if state.active else 'shelved'} |", + f"| Workflow status | {status} |", f"| Phase | {state.phase} ({idx}/{len(PHASE_ORDER)}) |", f"| Readiness | {readiness} |", - f"| Jira | {status} |", - f"| Next operation | {(op + ' — ' + title) if op and title else (operation or 'none')} |", + f"| Jira | {jira_status(self.project, wid)} |", + f"| Next operation | {next_op} |", f"| Assistant command | {self.recommend(wid, state.phase, operation)} |", "| After this phase | `./scripts/sdlc.sh advance` |", '| Capture (guarded) | `./scripts/sdlc.sh capture --summary ""` |', "| Orient / status | `./scripts/sdlc.sh next` or `/sdlc-spdd-whereami` |", "", ] + + def brief_markdown(self, work_id: str = "") -> str: + wid = (work_id or self.workflow.pointer.get() or "").strip() + if not wid: + return "No active Work ID. Run `./scripts/sdlc.sh resume `." + state = self.workflow.sync(wid) + lines = self._brief_rows(wid, state) + pending = self._pending_gates(state) if pending: lines.append("Pending gates:") lines.extend(f"- {p}" for p in pending) + ask = jira_ask_prompt(self.project, wid) if ask: lines.extend(["", "Tracker follow-up:", f"- {ask}"]) return "\n".join(lines) + "\n" @@ -183,18 +196,24 @@ def start(self, work_id: str = "", phase: str = "") -> int: "--work-id", wid, "--phase", phase or state.phase, ]) - def capture(self, args: list[str], *, work_id: str = "", phase: str = "") -> int: + def _resolve_capture_target(self, work_id: str) -> tuple[str, int]: + """(Work ID, rc): rc 3 on pointer mismatch, 2 when no pointer, 0 when resolved.""" pointer = self.workflow.pointer.get() - wid = (work_id or pointer or "").strip() if work_id and pointer and pointer != work_id: print(f"capture: --work-id '{work_id}' does not match pointer '{pointer}'", file=sys.stderr) print(f"Run: ./scripts/sdlc.sh resume {work_id}", file=sys.stderr) - return 3 + return "", 3 + wid = (work_id or pointer or "").strip() if not wid: print("capture: no active pointer — run: ./scripts/sdlc.sh resume ") - return 2 - if not phase: - phase = self.workflow.ensure_state(wid).phase or "resume" + return "", 2 + return wid, 0 + + def capture(self, args: list[str], *, work_id: str = "", phase: str = "") -> int: + wid, rc = self._resolve_capture_target(work_id) + if rc: + return rc + phase = phase or self.workflow.ensure_state(wid).phase or "resume" script = self._script("capture-session-memory.sh") argv = [str(script), "--target", str(self.project.root), "--work-id", wid, "--phase", phase, *args] return self.workflow.pointer.run_against(wid, argv) diff --git a/scripts/check-complexity.py b/scripts/check-complexity.py index e3240160..883bcc80 100755 --- a/scripts/check-complexity.py +++ b/scripts/check-complexity.py @@ -4,6 +4,10 @@ Fails when a *changed* file introduces a new function with CCN > 10 or NLOC > 80, or when an existing function's CCN rises. Untouched hotspots do not fail. Docs-only diffs exit 0. + +A function that disappears from one changed/deleted file and reappears under the +same name in another changed file is a MOVE: it is judged against its old CCN +(rise fails) rather than as NEW, so splitting a module does not trip the gate. """ from __future__ import annotations @@ -25,8 +29,12 @@ def git(*args: str, cwd: Path) -> str: return subprocess.check_output(["git", *args], cwd=cwd, text=True).rstrip("\n") -def changed_files(repo: Path, base: str, paths: list[str], exts: set[str]) -> list[str]: - rels = git("diff", "--name-only", "--diff-filter=ACMR", f"{base}...HEAD", "--", *paths, cwd=repo) +def changed_files( + repo: Path, base: str, paths: list[str], exts: set[str], diff_filter: str = "ACMR" +) -> list[str]: + rels = git( + "diff", "--name-only", f"--diff-filter={diff_filter}", f"{base}...HEAD", "--", *paths, cwd=repo + ) files = [] for line in rels.splitlines(): line = line.strip() @@ -68,10 +76,12 @@ def lizard_rows(source: str, filename: str, language: str) -> list[dict[str, str def file_at(repo: Path, rev: str, rel: str) -> str | None: - try: - return git("show", f"{rev}:{rel}", cwd=repo) - except subprocess.CalledProcessError: + proc = subprocess.run( + ["git", "show", f"{rev}:{rel}"], cwd=repo, text=True, capture_output=True, check=False + ) + if proc.returncode != 0: return None + return proc.stdout.rstrip("\n") def index_funcs(rows: list[dict[str, str]]) -> dict[str, tuple[int, int]]: @@ -81,6 +91,50 @@ def index_funcs(rows: list[dict[str, str]]) -> dict[str, tuple[int, int]]: return out +def base_index(repo: Path, base: str, rels: list[str]) -> dict[str, dict[str, tuple[int, int]]]: + """Per-file function index at ``base`` for every path in ``rels``.""" + out: dict[str, dict[str, tuple[int, int]]] = {} + for rel in rels: + src = file_at(repo, base, rel) + out[rel] = index_funcs(lizard_rows(src, rel, EXTS[Path(rel).suffix])) if src else {} + return out + + +def moved_from(name: str, base_all: dict[str, dict[str, tuple[int, int]]]) -> tuple[int, int] | None: + """Return the base (ccn, nloc) if ``name`` existed in any base file of the diff. + + When the same name existed in several files, the largest CCN is used so a + move is never judged more harshly than its worst previous home. + """ + hits = [funcs[name] for funcs in base_all.values() if name in funcs] + return max(hits) if hits else None + + +def judge_file( + rel: str, + head_funcs: dict[str, tuple[int, int]], + base_funcs: dict[str, tuple[int, int]], + base_all: dict[str, dict[str, tuple[int, int]]], + ccn_limit: int, + nloc_limit: int, +) -> list[str]: + failures: list[str] = [] + for name, (ccn, nloc) in sorted(head_funcs.items()): + old = base_funcs.get(name) + tag = "RISE" + if old is None: + old = moved_from(name, base_all) + tag = "RISE (moved)" + if old is None: + if ccn > ccn_limit or nloc > nloc_limit: + failures.append( + f"NEW {rel}::{name} CCN={ccn} NLOC={nloc} (limits {ccn_limit}/{nloc_limit})" + ) + elif ccn > old[0]: + failures.append(f"{tag} {rel}::{name} CCN {old[0]} -> {ccn}") + return failures + + def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--base", default="origin/main", help="git ref to compare against") @@ -104,25 +158,16 @@ def main() -> int: print("check-complexity: no changed source files") return 0 + deleted = changed_files(repo, args.base, args.paths, set(EXTS), diff_filter="D") + base_all = base_index(repo, args.base, [*files, *deleted]) + failures: list[str] = [] for rel in files: - language = EXTS[Path(rel).suffix] head = file_at(repo, "HEAD", rel) if head is None: continue - base_src = file_at(repo, args.base, rel) - head_funcs = index_funcs(lizard_rows(head, rel, language)) - base_funcs = index_funcs(lizard_rows(base_src, rel, language)) if base_src is not None else {} - for name, (ccn, nloc) in sorted(head_funcs.items()): - if name not in base_funcs: - if ccn > args.ccn or nloc > args.nloc: - failures.append( - f"NEW {rel}::{name} CCN={ccn} NLOC={nloc} (limits {args.ccn}/{args.nloc})" - ) - continue - old_ccn, _old_nloc = base_funcs[name] - if ccn > old_ccn: - failures.append(f"RISE {rel}::{name} CCN {old_ccn} -> {ccn}") + head_funcs = index_funcs(lizard_rows(head, rel, EXTS[Path(rel).suffix])) + failures.extend(judge_file(rel, head_funcs, base_all[rel], base_all, args.ccn, args.nloc)) if failures: print("check-complexity: FAIL", file=sys.stderr)