From c1927c951d03b1bfdc8cd90c29075fe638124657 Mon Sep 17 00:00:00 2001 From: JonatasFreireDev Date: Sat, 18 Jul 2026 03:31:18 -0300 Subject: [PATCH 1/9] feat(skills): add portable cli workflow scripts --- framework/skills/README.md | 4 ++++ framework/skills/artifact-importer/SKILL.md | 1 + .../scripts/record-review-and-validate.ps1 | 14 ++++++++++++++ .../scripts/record-review-and-validate.sh | 18 ++++++++++++++++++ framework/skills/code-review/SKILL.md | 2 ++ .../code-review/scripts/preview-review.ps1 | 5 +++++ .../code-review/scripts/preview-review.sh | 6 ++++++ framework/skills/command-executor/SKILL.md | 3 +++ .../command-executor/scripts/invoke-cli.ps1 | 6 ++++++ .../command-executor/scripts/invoke-cli.sh | 4 ++++ framework/skills/command-planner/SKILL.md | 3 +++ .../command-planner/scripts/invoke-cli.ps1 | 6 ++++++ .../command-planner/scripts/invoke-cli.sh | 4 ++++ .../skills/delivery-orchestrator/SKILL.md | 3 +++ .../scripts/invoke-cli.ps1 | 6 ++++++ .../scripts/invoke-cli.sh | 4 ++++ framework/skills/design-system/SKILL.md | 3 +++ .../design-system/scripts/invoke-cli.ps1 | 6 ++++++ .../skills/design-system/scripts/invoke-cli.sh | 4 ++++ .../skills/dispatch-orchestrator/SKILL.md | 3 +++ .../scripts/invoke-cli.ps1 | 6 ++++++ .../scripts/invoke-cli.sh | 4 ++++ framework/skills/documentation-writer/SKILL.md | 1 + .../scripts/validate-artifacts.ps1 | 8 ++++++++ .../scripts/validate-artifacts.sh | 14 ++++++++++++++ .../domain-evolution-orchestrator/SKILL.md | 3 +++ .../scripts/invoke-cli.ps1 | 6 ++++++ .../scripts/invoke-cli.sh | 4 ++++ framework/skills/engineering-system/SKILL.md | 3 +++ .../engineering-system/scripts/invoke-cli.ps1 | 6 ++++++ .../engineering-system/scripts/invoke-cli.sh | 4 ++++ framework/skills/execution-graph/SKILL.md | 2 ++ .../execution-graph/scripts/inspect-graph.ps1 | 5 +++++ .../execution-graph/scripts/inspect-graph.sh | 4 ++++ framework/skills/execution-scheduler/SKILL.md | 2 ++ .../scripts/preview-schedule.ps1 | 5 +++++ .../scripts/preview-schedule.sh | 3 +++ framework/skills/framework-guide/SKILL.md | 2 ++ .../scripts/inspect-workspace.ps1 | 15 +++++++++++++++ .../scripts/inspect-workspace.sh | 18 ++++++++++++++++++ .../skills/integration-orchestrator/SKILL.md | 3 +++ .../scripts/invoke-cli.ps1 | 6 ++++++ .../scripts/invoke-cli.sh | 4 ++++ framework/skills/pr-finalizer/SKILL.md | 5 ++++- .../skills/pr-finalizer/scripts/invoke-cli.ps1 | 6 ++++++ .../skills/pr-finalizer/scripts/invoke-cli.sh | 4 ++++ framework/skills/product-historian/SKILL.md | 2 ++ .../scripts/check-decisions.ps1 | 5 +++++ .../scripts/check-decisions.sh | 4 ++++ framework/skills/qa/SKILL.md | 2 ++ framework/skills/qa/scripts/check-gates.ps1 | 7 +++++++ framework/skills/qa/scripts/check-gates.sh | 5 +++++ framework/skills/release-orchestrator/SKILL.md | 2 ++ .../scripts/check-release-gates.ps1 | 7 +++++++ .../scripts/check-release-gates.sh | 5 +++++ framework/skills/security-review/SKILL.md | 3 +++ .../security-review/scripts/invoke-cli.ps1 | 6 ++++++ .../security-review/scripts/invoke-cli.sh | 4 ++++ framework/skills/task-generator/SKILL.md | 3 +++ .../task-generator/scripts/invoke-cli.ps1 | 6 ++++++ .../task-generator/scripts/invoke-cli.sh | 4 ++++ internal/runtimeassets/runtime_test.go | 2 +- 62 files changed, 313 insertions(+), 2 deletions(-) create mode 100644 framework/skills/artifact-importer/scripts/record-review-and-validate.ps1 create mode 100644 framework/skills/artifact-importer/scripts/record-review-and-validate.sh create mode 100644 framework/skills/code-review/scripts/preview-review.ps1 create mode 100644 framework/skills/code-review/scripts/preview-review.sh create mode 100644 framework/skills/command-executor/scripts/invoke-cli.ps1 create mode 100644 framework/skills/command-executor/scripts/invoke-cli.sh create mode 100644 framework/skills/command-planner/scripts/invoke-cli.ps1 create mode 100644 framework/skills/command-planner/scripts/invoke-cli.sh create mode 100644 framework/skills/delivery-orchestrator/scripts/invoke-cli.ps1 create mode 100644 framework/skills/delivery-orchestrator/scripts/invoke-cli.sh create mode 100644 framework/skills/design-system/scripts/invoke-cli.ps1 create mode 100644 framework/skills/design-system/scripts/invoke-cli.sh create mode 100644 framework/skills/dispatch-orchestrator/scripts/invoke-cli.ps1 create mode 100644 framework/skills/dispatch-orchestrator/scripts/invoke-cli.sh create mode 100644 framework/skills/documentation-writer/scripts/validate-artifacts.ps1 create mode 100644 framework/skills/documentation-writer/scripts/validate-artifacts.sh create mode 100644 framework/skills/domain-evolution-orchestrator/scripts/invoke-cli.ps1 create mode 100644 framework/skills/domain-evolution-orchestrator/scripts/invoke-cli.sh create mode 100644 framework/skills/engineering-system/scripts/invoke-cli.ps1 create mode 100644 framework/skills/engineering-system/scripts/invoke-cli.sh create mode 100644 framework/skills/execution-graph/scripts/inspect-graph.ps1 create mode 100644 framework/skills/execution-graph/scripts/inspect-graph.sh create mode 100644 framework/skills/execution-scheduler/scripts/preview-schedule.ps1 create mode 100644 framework/skills/execution-scheduler/scripts/preview-schedule.sh create mode 100644 framework/skills/framework-guide/scripts/inspect-workspace.ps1 create mode 100644 framework/skills/framework-guide/scripts/inspect-workspace.sh create mode 100644 framework/skills/integration-orchestrator/scripts/invoke-cli.ps1 create mode 100644 framework/skills/integration-orchestrator/scripts/invoke-cli.sh create mode 100644 framework/skills/pr-finalizer/scripts/invoke-cli.ps1 create mode 100644 framework/skills/pr-finalizer/scripts/invoke-cli.sh create mode 100644 framework/skills/product-historian/scripts/check-decisions.ps1 create mode 100644 framework/skills/product-historian/scripts/check-decisions.sh create mode 100644 framework/skills/qa/scripts/check-gates.ps1 create mode 100644 framework/skills/qa/scripts/check-gates.sh create mode 100644 framework/skills/release-orchestrator/scripts/check-release-gates.ps1 create mode 100644 framework/skills/release-orchestrator/scripts/check-release-gates.sh create mode 100644 framework/skills/security-review/scripts/invoke-cli.ps1 create mode 100644 framework/skills/security-review/scripts/invoke-cli.sh create mode 100644 framework/skills/task-generator/scripts/invoke-cli.ps1 create mode 100644 framework/skills/task-generator/scripts/invoke-cli.sh diff --git a/framework/skills/README.md b/framework/skills/README.md index e0caddf..0bd62c6 100644 --- a/framework/skills/README.md +++ b/framework/skills/README.md @@ -23,6 +23,10 @@ Skills are written to work in both this framework repository and adopter reposit When a skill mentions a product-relative path such as `knowledge/conventions/gates.md`, `.product/decisions.json`, `domains/`, `audits/`, or `releases/`, resolve it under the active product root. Each skill owns the templates it generates in its own `assets/` directory and its detailed guidance in `references/`; resolve those paths relative to the skill directory. Resolve shared framework assets such as `FRAMEWORK.md` or `validators/` under the framework root. +## Scripts + +Use a skill-owned `scripts/` resource only for a deterministic, repeatable sequence. Every shipped script has a `.ps1` implementation for Windows and a `.sh` implementation for macOS/Linux. Scripts may call the CLI, but they must not bypass its validation, approval, or persisted-state boundaries. + ## Expected Files - `/SKILL.md`: one skill per folder. diff --git a/framework/skills/artifact-importer/SKILL.md b/framework/skills/artifact-importer/SKILL.md index 83887de..dccf6f0 100644 --- a/framework/skills/artifact-importer/SKILL.md +++ b/framework/skills/artifact-importer/SKILL.md @@ -28,6 +28,7 @@ Source documents; product context; existing Domains, User Goals, Features, gloss - Relevant parent context.md files. - This skill owns its generation resources: the import-run resources in `assets/`. - `framework/skills/artifact-importer/assets/import-traceability-template.json` when normalizing the traceability ledger. +- Use `scripts/record-review-and-validate.ps1` on Windows or `scripts/record-review-and-validate.sh` on macOS/Linux to record one reviewed chunk and rebuild the registry. The script does not materialize or approve artifacts. - Approved decisions are discovered through the active product root's `.product/decisions.json`; resolve each registered `path` from its declared domain root (`knowledge/decisions/`, `design/decisions/`, or `engineering/decisions/`). ## Workflow diff --git a/framework/skills/artifact-importer/scripts/record-review-and-validate.ps1 b/framework/skills/artifact-importer/scripts/record-review-and-validate.ps1 new file mode 100644 index 0000000..d0dfa11 --- /dev/null +++ b/framework/skills/artifact-importer/scripts/record-review-and-validate.ps1 @@ -0,0 +1,14 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory)] [string]$Run, + [Parameter(Mandatory)] [string]$Chunk, + [Parameter(Mandatory)] [string]$Input, + [Parameter(Mandatory)] [string]$Agent, + [string]$ProductRoot = "product" +) + +$ErrorActionPreference = "Stop" +& spec-framework import record-review --run $Run --chunk $Chunk --input $Input --agent $Agent --product-root $ProductRoot --yes +if ($LASTEXITCODE) { exit $LASTEXITCODE } +& spec-framework validate --product-root $ProductRoot --write-registry +exit $LASTEXITCODE diff --git a/framework/skills/artifact-importer/scripts/record-review-and-validate.sh b/framework/skills/artifact-importer/scripts/record-review-and-validate.sh new file mode 100644 index 0000000..dd588ca --- /dev/null +++ b/framework/skills/artifact-importer/scripts/record-review-and-validate.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +set -euo pipefail + +run=""; chunk=""; input=""; agent=""; product_root="product" +while [[ $# -gt 0 ]]; do + case "$1" in + --run) run="$2"; shift 2 ;; + --chunk) chunk="$2"; shift 2 ;; + --input) input="$2"; shift 2 ;; + --agent) agent="$2"; shift 2 ;; + --product-root) product_root="$2"; shift 2 ;; + *) echo "Usage: $0 --run --chunk --input --agent [--product-root ]" >&2; exit 2 ;; + esac +done +[[ -n "$run" && -n "$chunk" && -n "$input" && -n "$agent" ]] || { echo "run, chunk, input, and agent are required" >&2; exit 2; } + +spec-framework import record-review --run "$run" --chunk "$chunk" --input "$input" --agent "$agent" --product-root "$product_root" --yes +spec-framework validate --product-root "$product_root" --write-registry diff --git a/framework/skills/code-review/SKILL.md b/framework/skills/code-review/SKILL.md index 35af2cf..d998c9f 100644 --- a/framework/skills/code-review/SKILL.md +++ b/framework/skills/code-review/SKILL.md @@ -50,6 +50,8 @@ Code Review verifies that implementation matches the Specification, tasks, archi | `note` | Non-blocking observation or follow-up. | ## Workflow +Use `scripts/preview-review.ps1` on Windows or `scripts/preview-review.sh` on macOS/Linux to inspect one stage. The script never supplies `--yes`. + 1. Confirm the review target and status. 2. Read the source artifacts and code evidence. diff --git a/framework/skills/code-review/scripts/preview-review.ps1 b/framework/skills/code-review/scripts/preview-review.ps1 new file mode 100644 index 0000000..ae96068 --- /dev/null +++ b/framework/skills/code-review/scripts/preview-review.ps1 @@ -0,0 +1,5 @@ +[CmdletBinding()] +param([Parameter(Mandatory)] [string]$Work, [Parameter(Mandatory)] [string]$Stage, [string]$ProductRoot = "product") +$ErrorActionPreference = "Stop" +& spec-framework review --work $Work --stage $Stage --product-root $ProductRoot +exit $LASTEXITCODE \ No newline at end of file diff --git a/framework/skills/code-review/scripts/preview-review.sh b/framework/skills/code-review/scripts/preview-review.sh new file mode 100644 index 0000000..59faf53 --- /dev/null +++ b/framework/skills/code-review/scripts/preview-review.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -euo pipefail +work=""; stage=""; product_root="product" +while [[ $# -gt 0 ]]; do case "$1" in --work) work="$2"; shift 2;; --stage) stage="$2"; shift 2;; --product-root) product_root="$2"; shift 2;; *) exit 2;; esac; done +[[ -n "$work" && -n "$stage" ]] || { echo "Usage: $0 --work --stage [--product-root ]" >&2; exit 2; } +exec spec-framework review --work "$work" --stage "$stage" --product-root "$product_root" \ No newline at end of file diff --git a/framework/skills/command-executor/SKILL.md b/framework/skills/command-executor/SKILL.md index cf8f786..e702d17 100644 --- a/framework/skills/command-executor/SKILL.md +++ b/framework/skills/command-executor/SKILL.md @@ -30,6 +30,9 @@ Command evidence JSON; stdout/stderr digest or safe log reference; result; route - The canonical template belongs to the skill that generates the artifact; read that skill's `assets/` directory. - Approved decisions are discovered through the active product root's `.product/decisions.json`; resolve each registered `path` from its declared domain root (`knowledge/decisions/`, `design/decisions/`, or `engineering/decisions/`). + +Use scripts/invoke-cli.ps1 on Windows or scripts/invoke-cli.sh on macOS/Linux for the CLI operation in this skill's reviewed scope. The wrapper never adds --yes or an approver identity. + ## Workflow 1. Load the workspace, current commit, approved artifacts, and runtime policy. 2. Validate hashes, ownership, dependencies, attempts, and authority before acting. diff --git a/framework/skills/command-executor/scripts/invoke-cli.ps1 b/framework/skills/command-executor/scripts/invoke-cli.ps1 new file mode 100644 index 0000000..2f5cb32 --- /dev/null +++ b/framework/skills/command-executor/scripts/invoke-cli.ps1 @@ -0,0 +1,6 @@ +[CmdletBinding()] +param([Parameter(ValueFromRemainingArguments = $true)] [string[]]$Arguments) +$ErrorActionPreference = "Stop" +# Read-only by default. This wrapper never adds --yes or an approver identity. +& spec-framework commands @Arguments +exit $LASTEXITCODE diff --git a/framework/skills/command-executor/scripts/invoke-cli.sh b/framework/skills/command-executor/scripts/invoke-cli.sh new file mode 100644 index 0000000..448dcc2 --- /dev/null +++ b/framework/skills/command-executor/scripts/invoke-cli.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +# Read-only by default. Pass only the command flags required by the reviewed scope. +exec spec-framework commands "$@" diff --git a/framework/skills/command-planner/SKILL.md b/framework/skills/command-planner/SKILL.md index d422f4d..42e9ed3 100644 --- a/framework/skills/command-planner/SKILL.md +++ b/framework/skills/command-planner/SKILL.md @@ -30,6 +30,9 @@ CMDPLAN-NNN.json with argv, cwd, source, risk, timeout, expected exit code, hash - This skill owns its generation resources: `assets/command-plan-template.json`. - Approved decisions are discovered through the active product root's `.product/decisions.json`; resolve each registered `path` from its declared domain root (`knowledge/decisions/`, `design/decisions/`, or `engineering/decisions/`). + +Use scripts/invoke-cli.ps1 on Windows or scripts/invoke-cli.sh on macOS/Linux for the CLI operation in this skill's reviewed scope. The wrapper never adds --yes or an approver identity. + ## Workflow 1. Load the workspace, current commit, approved artifacts, and runtime policy. 2. Validate hashes, ownership, dependencies, attempts, and authority before acting. diff --git a/framework/skills/command-planner/scripts/invoke-cli.ps1 b/framework/skills/command-planner/scripts/invoke-cli.ps1 new file mode 100644 index 0000000..2f5cb32 --- /dev/null +++ b/framework/skills/command-planner/scripts/invoke-cli.ps1 @@ -0,0 +1,6 @@ +[CmdletBinding()] +param([Parameter(ValueFromRemainingArguments = $true)] [string[]]$Arguments) +$ErrorActionPreference = "Stop" +# Read-only by default. This wrapper never adds --yes or an approver identity. +& spec-framework commands @Arguments +exit $LASTEXITCODE diff --git a/framework/skills/command-planner/scripts/invoke-cli.sh b/framework/skills/command-planner/scripts/invoke-cli.sh new file mode 100644 index 0000000..448dcc2 --- /dev/null +++ b/framework/skills/command-planner/scripts/invoke-cli.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +# Read-only by default. Pass only the command flags required by the reviewed scope. +exec spec-framework commands "$@" diff --git a/framework/skills/delivery-orchestrator/SKILL.md b/framework/skills/delivery-orchestrator/SKILL.md index 0edc532..1eefd4b 100644 --- a/framework/skills/delivery-orchestrator/SKILL.md +++ b/framework/skills/delivery-orchestrator/SKILL.md @@ -30,6 +30,9 @@ Updated workspace state; structured handoff; checkpoint request; routed next orc - This skill owns its generation resources: `assets/handoff-template.json` and `assets/runtime-workspace-template.json`. - Approved decisions are discovered through the active product root's `.product/decisions.json`; resolve each registered `path` from its declared domain root (`knowledge/decisions/`, `design/decisions/`, or `engineering/decisions/`). + +Use scripts/invoke-cli.ps1 on Windows or scripts/invoke-cli.sh on macOS/Linux for the CLI operation in this skill's reviewed scope. The wrapper never adds --yes or an approver identity. + ## Workflow 1. Load the workspace, current commit, approved artifacts, and runtime policy. 2. Validate hashes, ownership, dependencies, attempts, and authority before acting. diff --git a/framework/skills/delivery-orchestrator/scripts/invoke-cli.ps1 b/framework/skills/delivery-orchestrator/scripts/invoke-cli.ps1 new file mode 100644 index 0000000..3818931 --- /dev/null +++ b/framework/skills/delivery-orchestrator/scripts/invoke-cli.ps1 @@ -0,0 +1,6 @@ +[CmdletBinding()] +param([Parameter(ValueFromRemainingArguments = $true)] [string[]]$Arguments) +$ErrorActionPreference = "Stop" +# Read-only by default. This wrapper never adds --yes or an approver identity. +& spec-framework status --graph @Arguments +exit $LASTEXITCODE diff --git a/framework/skills/delivery-orchestrator/scripts/invoke-cli.sh b/framework/skills/delivery-orchestrator/scripts/invoke-cli.sh new file mode 100644 index 0000000..542d45d --- /dev/null +++ b/framework/skills/delivery-orchestrator/scripts/invoke-cli.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +# Read-only by default. Pass only the command flags required by the reviewed scope. +exec spec-framework status --graph "$@" diff --git a/framework/skills/design-system/SKILL.md b/framework/skills/design-system/SKILL.md index dee771d..3a05105 100644 --- a/framework/skills/design-system/SKILL.md +++ b/framework/skills/design-system/SKILL.md @@ -33,6 +33,9 @@ Approved Vision and Strategy; personas; brand sources; existing interfaces or co Follow the shared [Discovery And Challenge contract](../discovery-and-challenge.md) before substantive creation or material revision. + +Use scripts/invoke-cli.ps1 on Windows or scripts/invoke-cli.sh on macOS/Linux for the CLI operation in this skill's reviewed scope. The wrapper never adds --yes or an approver identity. + ## Workflow 1. Confirm whether origin is `generate`, `evolve`, or `adopt`; never promote a reference to canonical authority implicitly. 2. Inventory foundations, sources, tokens, themes, components, patterns, consumers, and implementation links. diff --git a/framework/skills/design-system/scripts/invoke-cli.ps1 b/framework/skills/design-system/scripts/invoke-cli.ps1 new file mode 100644 index 0000000..95224e5 --- /dev/null +++ b/framework/skills/design-system/scripts/invoke-cli.ps1 @@ -0,0 +1,6 @@ +[CmdletBinding()] +param([Parameter(ValueFromRemainingArguments = $true)] [string[]]$Arguments) +$ErrorActionPreference = "Stop" +# Read-only by default. This wrapper never adds --yes or an approver identity. +& spec-framework design-system @Arguments +exit $LASTEXITCODE diff --git a/framework/skills/design-system/scripts/invoke-cli.sh b/framework/skills/design-system/scripts/invoke-cli.sh new file mode 100644 index 0000000..1c3ff94 --- /dev/null +++ b/framework/skills/design-system/scripts/invoke-cli.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +# Read-only by default. Pass only the command flags required by the reviewed scope. +exec spec-framework design-system "$@" diff --git a/framework/skills/dispatch-orchestrator/SKILL.md b/framework/skills/dispatch-orchestrator/SKILL.md index 2574d5a..6b27c60 100644 --- a/framework/skills/dispatch-orchestrator/SKILL.md +++ b/framework/skills/dispatch-orchestrator/SKILL.md @@ -29,6 +29,9 @@ Persisted envelopes; dispatch plan; wave observation; reconciliation findings; h - Read the return template owned by Subagent Return Reviewer when validating or routing a completed assignment. - Approved product decisions in the active product root's `knowledge/decisions/` and `.product/decisions.json`. + +Use scripts/invoke-cli.ps1 on Windows or scripts/invoke-cli.sh on macOS/Linux for the CLI operation in this skill's reviewed scope. The wrapper never adds --yes or an approver identity. + ## Workflow 1. Read current graph/chunk readiness and dispatch configuration. 2. Plan only canonical units with no dependency or scope conflict. diff --git a/framework/skills/dispatch-orchestrator/scripts/invoke-cli.ps1 b/framework/skills/dispatch-orchestrator/scripts/invoke-cli.ps1 new file mode 100644 index 0000000..268c7fb --- /dev/null +++ b/framework/skills/dispatch-orchestrator/scripts/invoke-cli.ps1 @@ -0,0 +1,6 @@ +[CmdletBinding()] +param([Parameter(ValueFromRemainingArguments = $true)] [string[]]$Arguments) +$ErrorActionPreference = "Stop" +# Read-only by default. This wrapper never adds --yes or an approver identity. +& spec-framework dispatch @Arguments +exit $LASTEXITCODE diff --git a/framework/skills/dispatch-orchestrator/scripts/invoke-cli.sh b/framework/skills/dispatch-orchestrator/scripts/invoke-cli.sh new file mode 100644 index 0000000..2cd29ae --- /dev/null +++ b/framework/skills/dispatch-orchestrator/scripts/invoke-cli.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +# Read-only by default. Pass only the command flags required by the reviewed scope. +exec spec-framework dispatch "$@" diff --git a/framework/skills/documentation-writer/SKILL.md b/framework/skills/documentation-writer/SKILL.md index 53c5190..face3dd 100644 --- a/framework/skills/documentation-writer/SKILL.md +++ b/framework/skills/documentation-writer/SKILL.md @@ -27,6 +27,7 @@ Updated docs; updated context.md files; index updates; changelog notes. - the framework root's `FRAMEWORK.md` - Relevant parent context.md files. - This skill owns its generation resources: `assets/context-template.md` and `assets/derivation-record-template.json`; read `references/template-authoring.md` before changing template conventions. +- Use `scripts/validate-artifacts.ps1` on Windows or `scripts/validate-artifacts.sh` on macOS/Linux for repeatable validation; pass `--write-registry` only after reviewing the intended registry change. - Approved decisions are discovered through the active product root's `.product/decisions.json`; resolve each registered `path` from its declared domain root (`knowledge/decisions/`, `design/decisions/`, or `engineering/decisions/`). ## Workflow diff --git a/framework/skills/documentation-writer/scripts/validate-artifacts.ps1 b/framework/skills/documentation-writer/scripts/validate-artifacts.ps1 new file mode 100644 index 0000000..28b83b2 --- /dev/null +++ b/framework/skills/documentation-writer/scripts/validate-artifacts.ps1 @@ -0,0 +1,8 @@ +[CmdletBinding()] +param([string]$ProductRoot = "product", [switch]$WriteRegistry) + +$ErrorActionPreference = "Stop" +$args = @("validate", "--product-root", $ProductRoot) +if ($WriteRegistry) { $args += "--write-registry" } +& spec-framework @args +exit $LASTEXITCODE diff --git a/framework/skills/documentation-writer/scripts/validate-artifacts.sh b/framework/skills/documentation-writer/scripts/validate-artifacts.sh new file mode 100644 index 0000000..6001704 --- /dev/null +++ b/framework/skills/documentation-writer/scripts/validate-artifacts.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail + +product_root="product"; write_registry=false +while [[ $# -gt 0 ]]; do + case "$1" in + --product-root) product_root="$2"; shift 2 ;; + --write-registry) write_registry=true; shift ;; + *) echo "Usage: $0 [--product-root ] [--write-registry]" >&2; exit 2 ;; + esac +done +args=(validate --product-root "$product_root") +$write_registry && args+=(--write-registry) +spec-framework "${args[@]}" diff --git a/framework/skills/domain-evolution-orchestrator/SKILL.md b/framework/skills/domain-evolution-orchestrator/SKILL.md index 4903b35..2530400 100644 --- a/framework/skills/domain-evolution-orchestrator/SKILL.md +++ b/framework/skills/domain-evolution-orchestrator/SKILL.md @@ -33,6 +33,9 @@ Approved domain; goals; journeys; strategy; roadmap; evidence; metrics; decision Follow the shared [Discovery And Challenge contract](../discovery-and-challenge.md) when comparing candidates or requesting human selection. + +Use scripts/invoke-cli.ps1 on Windows or scripts/invoke-cli.sh on macOS/Linux for the CLI operation in this skill's reviewed scope. The wrapper never adds --yes or an approver identity. + ## Workflow 1. Confirm the Domain and relevant Goals are approved. 2. Route missing user intent to User Goal and missing journeys to Journey. diff --git a/framework/skills/domain-evolution-orchestrator/scripts/invoke-cli.ps1 b/framework/skills/domain-evolution-orchestrator/scripts/invoke-cli.ps1 new file mode 100644 index 0000000..d5caf60 --- /dev/null +++ b/framework/skills/domain-evolution-orchestrator/scripts/invoke-cli.ps1 @@ -0,0 +1,6 @@ +[CmdletBinding()] +param([Parameter(ValueFromRemainingArguments = $true)] [string[]]$Arguments) +$ErrorActionPreference = "Stop" +# Read-only by default. This wrapper never adds --yes or an approver identity. +& spec-framework impact @Arguments +exit $LASTEXITCODE diff --git a/framework/skills/domain-evolution-orchestrator/scripts/invoke-cli.sh b/framework/skills/domain-evolution-orchestrator/scripts/invoke-cli.sh new file mode 100644 index 0000000..bd2d6d7 --- /dev/null +++ b/framework/skills/domain-evolution-orchestrator/scripts/invoke-cli.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +# Read-only by default. Pass only the command flags required by the reviewed scope. +exec spec-framework impact "$@" diff --git a/framework/skills/engineering-system/SKILL.md b/framework/skills/engineering-system/SKILL.md index 4cf8a08..60abdc7 100644 --- a/framework/skills/engineering-system/SKILL.md +++ b/framework/skills/engineering-system/SKILL.md @@ -35,6 +35,9 @@ Product context; real code and test tree; deployment and operations configuratio Follow the shared [Discovery And Challenge contract](../discovery-and-challenge.md) before substantive creation or material revision. + +Use scripts/invoke-cli.ps1 on Windows or scripts/invoke-cli.sh on macOS/Linux for the CLI operation in this skill's reviewed scope. The wrapper never adds --yes or an approver identity. + ## Workflow 1. Inspect the real product code, tests, configuration, environments, operations evidence, and existing engineering documents. 2. Define the covered product and repository boundaries and choose `generate`, `evolve`, or `adopt`. diff --git a/framework/skills/engineering-system/scripts/invoke-cli.ps1 b/framework/skills/engineering-system/scripts/invoke-cli.ps1 new file mode 100644 index 0000000..cc30149 --- /dev/null +++ b/framework/skills/engineering-system/scripts/invoke-cli.ps1 @@ -0,0 +1,6 @@ +[CmdletBinding()] +param([Parameter(ValueFromRemainingArguments = $true)] [string[]]$Arguments) +$ErrorActionPreference = "Stop" +# Read-only by default. This wrapper never adds --yes or an approver identity. +& spec-framework engineering-system @Arguments +exit $LASTEXITCODE diff --git a/framework/skills/engineering-system/scripts/invoke-cli.sh b/framework/skills/engineering-system/scripts/invoke-cli.sh new file mode 100644 index 0000000..9d10743 --- /dev/null +++ b/framework/skills/engineering-system/scripts/invoke-cli.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +# Read-only by default. Pass only the command flags required by the reviewed scope. +exec spec-framework engineering-system "$@" diff --git a/framework/skills/execution-graph/SKILL.md b/framework/skills/execution-graph/SKILL.md index 0d61ae7..5680cf4 100644 --- a/framework/skills/execution-graph/SKILL.md +++ b/framework/skills/execution-graph/SKILL.md @@ -35,6 +35,8 @@ execution-graph.json; ordered DAG; Delivery Level/Priority on graph and nodes; p Follow the shared [Discovery And Challenge contract](../discovery-and-challenge.md) before substantive creation or material revision. ## Workflow +Use `scripts/inspect-graph.ps1` on Windows or `scripts/inspect-graph.sh` on macOS/Linux for a read-only graph status snapshot. + 1. Read the parent context and confirm the artifact status. 2. Create the graph as `draft`, validate its DAG and contracts, then move it to `proposed` for human review without requiring task files to exist. 3. After review, use confirmed graph materialization to create canonical task files and `tasks.md`; never create them ad hoc or overwrite existing tasks. diff --git a/framework/skills/execution-graph/scripts/inspect-graph.ps1 b/framework/skills/execution-graph/scripts/inspect-graph.ps1 new file mode 100644 index 0000000..5905ab3 --- /dev/null +++ b/framework/skills/execution-graph/scripts/inspect-graph.ps1 @@ -0,0 +1,5 @@ +[CmdletBinding()] +param([Parameter(Mandatory)] [string]$Graph, [string]$ProductRoot = "product") +$ErrorActionPreference = "Stop" +& spec-framework graph status --graph $Graph --product-root $ProductRoot +exit $LASTEXITCODE \ No newline at end of file diff --git a/framework/skills/execution-graph/scripts/inspect-graph.sh b/framework/skills/execution-graph/scripts/inspect-graph.sh new file mode 100644 index 0000000..b4c4930 --- /dev/null +++ b/framework/skills/execution-graph/scripts/inspect-graph.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +[[ $# -ge 1 ]] || { echo "Usage: $0 [product-root]" >&2; exit 2; } +exec spec-framework graph status --graph "$1" --product-root "${2:-product}" \ No newline at end of file diff --git a/framework/skills/execution-scheduler/SKILL.md b/framework/skills/execution-scheduler/SKILL.md index 50d77c4..0465757 100644 --- a/framework/skills/execution-scheduler/SKILL.md +++ b/framework/skills/execution-scheduler/SKILL.md @@ -31,6 +31,8 @@ Versioned wave plan; ready/serialized tasks; conflict rationale; capability gaps - Approved decisions are discovered through the active product root's `.product/decisions.json`; resolve each registered `path` from its declared domain root (`knowledge/decisions/`, `design/decisions/`, or `engineering/decisions/`). ## Workflow +Use `scripts/preview-schedule.ps1` on Windows or `scripts/preview-schedule.sh` on macOS/Linux for a mandatory dry-run; it cannot submit a scheduling mutation. + 1. Load the workspace, current commit, approved artifacts, and runtime policy. 2. Validate hashes, ownership, dependencies, attempts, and authority before acting. 3. Refuse stale inputs, scope escapes, conflicting resources, and unsupported risk levels. diff --git a/framework/skills/execution-scheduler/scripts/preview-schedule.ps1 b/framework/skills/execution-scheduler/scripts/preview-schedule.ps1 new file mode 100644 index 0000000..348b83c --- /dev/null +++ b/framework/skills/execution-scheduler/scripts/preview-schedule.ps1 @@ -0,0 +1,5 @@ +[CmdletBinding()] +param([Parameter(ValueFromRemainingArguments = $true)] [string[]]$Arguments) +$ErrorActionPreference = "Stop" +& spec-framework schedule --dry-run @Arguments +exit $LASTEXITCODE \ No newline at end of file diff --git a/framework/skills/execution-scheduler/scripts/preview-schedule.sh b/framework/skills/execution-scheduler/scripts/preview-schedule.sh new file mode 100644 index 0000000..10ad896 --- /dev/null +++ b/framework/skills/execution-scheduler/scripts/preview-schedule.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +set -euo pipefail +exec spec-framework schedule --dry-run "$@" \ No newline at end of file diff --git a/framework/skills/framework-guide/SKILL.md b/framework/skills/framework-guide/SKILL.md index 8a8889c..0723cbc 100644 --- a/framework/skills/framework-guide/SKILL.md +++ b/framework/skills/framework-guide/SKILL.md @@ -89,6 +89,8 @@ Follow the shared [Discovery And Challenge contract](../discovery-and-challenge. ## Workflow +For a deterministic workspace snapshot, run `scripts/inspect-workspace.ps1` on Windows or `scripts/inspect-workspace.sh` on macOS/Linux instead of reconstructing the `guide`, `status`, and `dashboard` sequence. + When the intent concerns a decision or ADR, first run `spec-framework decisions check --product-root product --json`. Use the indexed `path` and reported domain as the routing evidence. `--strict` is the CI gate; `--fix-links` requires a reviewed preview followed by `--yes` and never creates approval records. 1. Restate the goal in one sentence and determine whether the request is explanation, inspection, planning, local mutation, approval, remote mutation, or release. 2. Apply the activation and dispatch boundaries. If active, discover the repository root, product root, pinned version, agents, and starting point from the canonical manifest; do not ask for information the CLI or repository can provide safely. diff --git a/framework/skills/framework-guide/scripts/inspect-workspace.ps1 b/framework/skills/framework-guide/scripts/inspect-workspace.ps1 new file mode 100644 index 0000000..be7fc5e --- /dev/null +++ b/framework/skills/framework-guide/scripts/inspect-workspace.ps1 @@ -0,0 +1,15 @@ +[CmdletBinding()] +param( + [string]$ProductRoot = "product", + [string]$Work = "" +) + +$ErrorActionPreference = "Stop" +$args = @("--product-root", $ProductRoot) +if ($Work) { $args += @("--work", $Work) } +& spec-framework guide @args +if ($LASTEXITCODE) { exit $LASTEXITCODE } +& spec-framework status --graph @args +if ($LASTEXITCODE) { exit $LASTEXITCODE } +& spec-framework dashboard --json @args +exit $LASTEXITCODE diff --git a/framework/skills/framework-guide/scripts/inspect-workspace.sh b/framework/skills/framework-guide/scripts/inspect-workspace.sh new file mode 100644 index 0000000..b25cb42 --- /dev/null +++ b/framework/skills/framework-guide/scripts/inspect-workspace.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +set -euo pipefail + +product_root="product" +work="" +while [[ $# -gt 0 ]]; do + case "$1" in + --product-root) product_root="$2"; shift 2 ;; + --work) work="$2"; shift 2 ;; + *) echo "Usage: $0 [--product-root ] [--work ]" >&2; exit 2 ;; + esac +done + +args=(--product-root "$product_root") +[[ -n "$work" ]] && args+=(--work "$work") +spec-framework guide "${args[@]}" +spec-framework status --graph "${args[@]}" +spec-framework dashboard --json "${args[@]}" diff --git a/framework/skills/integration-orchestrator/SKILL.md b/framework/skills/integration-orchestrator/SKILL.md index b7ba360..aa16a50 100644 --- a/framework/skills/integration-orchestrator/SKILL.md +++ b/framework/skills/integration-orchestrator/SKILL.md @@ -30,6 +30,9 @@ INTEGRATION-NNN.json; ordered cherry-pick plan; integrated diff hash; conflict r - This skill owns its generation resources: `assets/integration-template.json`. - Approved decisions are discovered through the active product root's `.product/decisions.json`; resolve each registered `path` from its declared domain root (`knowledge/decisions/`, `design/decisions/`, or `engineering/decisions/`). + +Use scripts/invoke-cli.ps1 on Windows or scripts/invoke-cli.sh on macOS/Linux for the CLI operation in this skill's reviewed scope. The wrapper never adds --yes or an approver identity. + ## Workflow 1. Load the workspace, current commit, approved artifacts, and runtime policy. 2. Validate hashes, ownership, dependencies, attempts, and authority before acting. diff --git a/framework/skills/integration-orchestrator/scripts/invoke-cli.ps1 b/framework/skills/integration-orchestrator/scripts/invoke-cli.ps1 new file mode 100644 index 0000000..a3b846d --- /dev/null +++ b/framework/skills/integration-orchestrator/scripts/invoke-cli.ps1 @@ -0,0 +1,6 @@ +[CmdletBinding()] +param([Parameter(ValueFromRemainingArguments = $true)] [string[]]$Arguments) +$ErrorActionPreference = "Stop" +# Read-only by default. This wrapper never adds --yes or an approver identity. +& spec-framework integrate @Arguments +exit $LASTEXITCODE diff --git a/framework/skills/integration-orchestrator/scripts/invoke-cli.sh b/framework/skills/integration-orchestrator/scripts/invoke-cli.sh new file mode 100644 index 0000000..d78c0d0 --- /dev/null +++ b/framework/skills/integration-orchestrator/scripts/invoke-cli.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +# Read-only by default. Pass only the command flags required by the reviewed scope. +exec spec-framework integrate "$@" diff --git a/framework/skills/pr-finalizer/SKILL.md b/framework/skills/pr-finalizer/SKILL.md index ca01c95..feea79b 100644 --- a/framework/skills/pr-finalizer/SKILL.md +++ b/framework/skills/pr-finalizer/SKILL.md @@ -1,4 +1,4 @@ ---- +--- name: pr-finalizer description: "PR Finalizer Skill. Use when an agent needs to prepare or open a pull request after gates, QA, Code Review, and Security Review are ready, without merging." --- @@ -48,6 +48,9 @@ PR Finalizer verifies hard preconditions, links evidence, writes PR metadata bac If any hard precondition fails, stop and report the blocker. + +Use scripts/invoke-cli.ps1 on Windows or scripts/invoke-cli.sh on macOS/Linux for the CLI operation in this skill's reviewed scope. The wrapper never adds --yes or an approver identity. + ## Workflow 1. Inspect branch, commits, diff, and task evidence. diff --git a/framework/skills/pr-finalizer/scripts/invoke-cli.ps1 b/framework/skills/pr-finalizer/scripts/invoke-cli.ps1 new file mode 100644 index 0000000..e1790f8 --- /dev/null +++ b/framework/skills/pr-finalizer/scripts/invoke-cli.ps1 @@ -0,0 +1,6 @@ +[CmdletBinding()] +param([Parameter(ValueFromRemainingArguments = $true)] [string[]]$Arguments) +$ErrorActionPreference = "Stop" +# Read-only by default. This wrapper never adds --yes or an approver identity. +& spec-framework review @Arguments +exit $LASTEXITCODE diff --git a/framework/skills/pr-finalizer/scripts/invoke-cli.sh b/framework/skills/pr-finalizer/scripts/invoke-cli.sh new file mode 100644 index 0000000..5dc364d --- /dev/null +++ b/framework/skills/pr-finalizer/scripts/invoke-cli.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +# Read-only by default. Pass only the command flags required by the reviewed scope. +exec spec-framework review "$@" diff --git a/framework/skills/product-historian/SKILL.md b/framework/skills/product-historian/SKILL.md index c610a66..4f8f7e6 100644 --- a/framework/skills/product-historian/SKILL.md +++ b/framework/skills/product-historian/SKILL.md @@ -30,6 +30,8 @@ Decision records; updated decisions index; supersede notes; historical rationale - Approved decisions are discovered through the active product root's `.product/decisions.json`; resolve each registered `path` from its declared domain root (`knowledge/decisions/`, `design/decisions/`, or `engineering/decisions/`). ## Workflow +Use `scripts/check-decisions.ps1` on Windows or `scripts/check-decisions.sh` on macOS/Linux for the read-only indexed decision check. + 1. Classify each DEC as product, architecture, security, data, or delivery; declare artifact/path scope and structured workflow effects in the decision index. 2. Keep architectural ADRs in the canonical DEC system rather than creating a parallel ADR store. 3. For legacy indexes, preview the guided migration, require review of ambiguous inferred types/scopes, preserve the original backup, and never change decision content/status or create approval records. diff --git a/framework/skills/product-historian/scripts/check-decisions.ps1 b/framework/skills/product-historian/scripts/check-decisions.ps1 new file mode 100644 index 0000000..b110297 --- /dev/null +++ b/framework/skills/product-historian/scripts/check-decisions.ps1 @@ -0,0 +1,5 @@ +[CmdletBinding()] +param([string]$ProductRoot = "product") +$ErrorActionPreference = "Stop" +& spec-framework decisions check --product-root $ProductRoot --json +exit $LASTEXITCODE \ No newline at end of file diff --git a/framework/skills/product-historian/scripts/check-decisions.sh b/framework/skills/product-historian/scripts/check-decisions.sh new file mode 100644 index 0000000..02f6a2c --- /dev/null +++ b/framework/skills/product-historian/scripts/check-decisions.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +product_root="${1:-product}" +exec spec-framework decisions check --product-root "$product_root" --json \ No newline at end of file diff --git a/framework/skills/qa/SKILL.md b/framework/skills/qa/SKILL.md index 185cf1a..5199a64 100644 --- a/framework/skills/qa/SKILL.md +++ b/framework/skills/qa/SKILL.md @@ -36,6 +36,8 @@ QA verdict; test evidence; blocking findings; residual risks; required fixes. - Approved decisions are discovered through the active product root's `.product/decisions.json`; resolve each registered `path` from its declared domain root (`knowledge/decisions/`, `design/decisions/`, or `engineering/decisions/`). ## Workflow +Run `scripts/check-gates.ps1` on Windows or `scripts/check-gates.sh` on macOS/Linux to validate the product before evaluating evidence. The script is read-only. + 1. Read the relevant context and identify artifact status. 2. Read the active product root's `knowledge/conventions/gates.md` and identify applicable gates for the delivery. 3. When the Engineering Quality System is configured, verify that `tests.md` pins its current id/version, selects configured environment, data, and platform values, applies its risk, coverage, and evidence policies, and declares `None` or only open, unexpired, in-scope exceptions. diff --git a/framework/skills/qa/scripts/check-gates.ps1 b/framework/skills/qa/scripts/check-gates.ps1 new file mode 100644 index 0000000..65d2d2a --- /dev/null +++ b/framework/skills/qa/scripts/check-gates.ps1 @@ -0,0 +1,7 @@ +[CmdletBinding()] +param([string]$ProductRoot = "product") +$ErrorActionPreference = "Stop" +& spec-framework validate --product-root $ProductRoot +if ($LASTEXITCODE) { exit $LASTEXITCODE } +& spec-framework gates --product-root $ProductRoot +exit $LASTEXITCODE \ No newline at end of file diff --git a/framework/skills/qa/scripts/check-gates.sh b/framework/skills/qa/scripts/check-gates.sh new file mode 100644 index 0000000..48fcefe --- /dev/null +++ b/framework/skills/qa/scripts/check-gates.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail +product_root="${1:-product}" +spec-framework validate --product-root "$product_root" +exec spec-framework gates --product-root "$product_root" \ No newline at end of file diff --git a/framework/skills/release-orchestrator/SKILL.md b/framework/skills/release-orchestrator/SKILL.md index 7cefacf..f34891f 100644 --- a/framework/skills/release-orchestrator/SKILL.md +++ b/framework/skills/release-orchestrator/SKILL.md @@ -23,6 +23,8 @@ Orchestrator. Controls workflow, gates, handoffs, and approval checkpoints. It s Gap Finder -> Conflict Finder -> UX/UI audit when UI exists -> QA -> Code Review -> Security Review when sensitive -> Documentation Writer ## Operating rules +Use `scripts/check-release-gates.ps1` on Windows or `scripts/check-release-gates.sh` on macOS/Linux for a strict, read-only release-gate check before requesting approval. + 1. Identify the current artifact status before routing work. 2. Route work to the smallest specialist skill that owns the next artifact. 3. Stop at approval gates when scope, architecture, data, security, roadmap, or release risk changes. diff --git a/framework/skills/release-orchestrator/scripts/check-release-gates.ps1 b/framework/skills/release-orchestrator/scripts/check-release-gates.ps1 new file mode 100644 index 0000000..68e422e --- /dev/null +++ b/framework/skills/release-orchestrator/scripts/check-release-gates.ps1 @@ -0,0 +1,7 @@ +[CmdletBinding()] +param([string]$ProductRoot = "product") +$ErrorActionPreference = "Stop" +& spec-framework validate --product-root $ProductRoot --strict +if ($LASTEXITCODE) { exit $LASTEXITCODE } +& spec-framework gates --product-root $ProductRoot +exit $LASTEXITCODE \ No newline at end of file diff --git a/framework/skills/release-orchestrator/scripts/check-release-gates.sh b/framework/skills/release-orchestrator/scripts/check-release-gates.sh new file mode 100644 index 0000000..bf6c3a5 --- /dev/null +++ b/framework/skills/release-orchestrator/scripts/check-release-gates.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail +product_root="${1:-product}" +spec-framework validate --product-root "$product_root" --strict +exec spec-framework gates --product-root "$product_root" \ No newline at end of file diff --git a/framework/skills/security-review/SKILL.md b/framework/skills/security-review/SKILL.md index 75809db..30ad588 100644 --- a/framework/skills/security-review/SKILL.md +++ b/framework/skills/security-review/SKILL.md @@ -36,6 +36,9 @@ Security verdict; threat model summary; control checklist; blocking findings; re - Related `tests.md`, `qa-evidence.md`, and `audit.md` when present. - Failure routing in `FRAMEWORK.md`. + +Use scripts/invoke-cli.ps1 on Windows or scripts/invoke-cli.sh on macOS/Linux for the CLI operation in this skill's reviewed scope. The wrapper never adds --yes or an approver identity. + ## Workflow 1. Read the local context and identify artifact status, delivery level, priority, and release intent. 2. Read the product security baseline and active threat register entries that affect the artifact. diff --git a/framework/skills/security-review/scripts/invoke-cli.ps1 b/framework/skills/security-review/scripts/invoke-cli.ps1 new file mode 100644 index 0000000..913bc82 --- /dev/null +++ b/framework/skills/security-review/scripts/invoke-cli.ps1 @@ -0,0 +1,6 @@ +[CmdletBinding()] +param([Parameter(ValueFromRemainingArguments = $true)] [string[]]$Arguments) +$ErrorActionPreference = "Stop" +# Read-only by default. This wrapper never adds --yes or an approver identity. +& spec-framework gates @Arguments +exit $LASTEXITCODE diff --git a/framework/skills/security-review/scripts/invoke-cli.sh b/framework/skills/security-review/scripts/invoke-cli.sh new file mode 100644 index 0000000..3a5fb97 --- /dev/null +++ b/framework/skills/security-review/scripts/invoke-cli.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +# Read-only by default. Pass only the command flags required by the reviewed scope. +exec spec-framework gates "$@" diff --git a/framework/skills/task-generator/SKILL.md b/framework/skills/task-generator/SKILL.md index 4fe6edd..36afbcd 100644 --- a/framework/skills/task-generator/SKILL.md +++ b/framework/skills/task-generator/SKILL.md @@ -38,6 +38,9 @@ tasks.md; task files or task records with Delivery Level/Priority; acceptance ch Follow the shared [Discovery And Challenge contract](../discovery-and-challenge.md) before substantive creation or material revision. + +Use scripts/invoke-cli.ps1 on Windows or scripts/invoke-cli.sh on macOS/Linux for the CLI operation in this skill's reviewed scope. The wrapper never adds --yes or an approver identity. + ## Workflow 1. Read the parent context and confirm the artifact status. 2. Identify missing information, assumptions, conflicts, and dependencies. diff --git a/framework/skills/task-generator/scripts/invoke-cli.ps1 b/framework/skills/task-generator/scripts/invoke-cli.ps1 new file mode 100644 index 0000000..3b7d95f --- /dev/null +++ b/framework/skills/task-generator/scripts/invoke-cli.ps1 @@ -0,0 +1,6 @@ +[CmdletBinding()] +param([Parameter(ValueFromRemainingArguments = $true)] [string[]]$Arguments) +$ErrorActionPreference = "Stop" +# Read-only by default. This wrapper never adds --yes or an approver identity. +& spec-framework task @Arguments +exit $LASTEXITCODE diff --git a/framework/skills/task-generator/scripts/invoke-cli.sh b/framework/skills/task-generator/scripts/invoke-cli.sh new file mode 100644 index 0000000..ef45c22 --- /dev/null +++ b/framework/skills/task-generator/scripts/invoke-cli.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +# Read-only by default. Pass only the command flags required by the reviewed scope. +exec spec-framework task "$@" diff --git a/internal/runtimeassets/runtime_test.go b/internal/runtimeassets/runtime_test.go index e6a45a9..506950a 100644 --- a/internal/runtimeassets/runtime_test.go +++ b/internal/runtimeassets/runtime_test.go @@ -38,7 +38,7 @@ func TestEnsureMaterializesVersionedAssets(t *testing.T) { if err != nil { t.Fatal(err) } - for _, name := range []string{"FRAMEWORK.md", "AGENTS.framework.md", "docs/execution-runtime.md", "docs/engineering-systems.md", "docs/lifecycle-and-approvals.md", "docs/artifact-registry-modules.md", "init/schema.json", "init/catalog.json", "init/bootstrap.json", "init/contracts/new-product.json", "skills/code-runner/SKILL.md", "skills/framework-guide/agents/openai.yaml", "skills/discovery-and-challenge.md", "skills/problem-discovery/assets/interview-note-template.md", "skills/problem-discovery/assets/research-summary-template.md", "skills/specification/assets/specification-template.md", "examples/events/domains/events/domain.md", ".complete"} { + for _, name := range []string{"FRAMEWORK.md", "AGENTS.framework.md", "docs/execution-runtime.md", "docs/engineering-systems.md", "docs/lifecycle-and-approvals.md", "docs/artifact-registry-modules.md", "init/schema.json", "init/catalog.json", "init/bootstrap.json", "init/contracts/new-product.json", "skills/code-runner/SKILL.md", "skills/framework-guide/agents/openai.yaml", "skills/framework-guide/scripts/inspect-workspace.ps1", "skills/framework-guide/scripts/inspect-workspace.sh", "skills/artifact-importer/scripts/record-review-and-validate.ps1", "skills/documentation-writer/scripts/validate-artifacts.sh", "skills/discovery-and-challenge.md", "skills/problem-discovery/assets/interview-note-template.md", "skills/problem-discovery/assets/research-summary-template.md", "skills/specification/assets/specification-template.md", "examples/events/domains/events/domain.md", ".complete"} { if _, err := os.Stat(filepath.Join(root, filepath.FromSlash(name))); err != nil { t.Fatal(err) } From 138b7b6fe235eaef5ea0f09cc435b36fe4e97049 Mon Sep 17 00:00:00 2001 From: JonatasFreireDev Date: Sat, 18 Jul 2026 04:06:28 -0300 Subject: [PATCH 2/9] feat(init): add code-aware baseline gates --- FRAMEWORK.md | 6 ++ docs/starting-points.md | 2 + framework/adoption.md | 10 ++ framework/init/bootstrap.json | 8 +- framework/init/catalog.json | 7 +- framework/init/contracts/audit-only.json | 4 +- .../init/contracts/existing-documents.json | 4 +- .../init/contracts/existing-feature.json | 4 +- framework/install.md | 2 + framework/skills/design-system/SKILL.md | 2 +- framework/skills/domain-architect/SKILL.md | 2 +- framework/skills/engineering-system/SKILL.md | 2 +- framework/skills/framework-guide/SKILL.md | 14 +-- .../skills/product-orchestrator/SKILL.md | 5 +- internal/cli/app.go | 23 +++- internal/install/code_discovery.go | 101 ++++++++++++++++++ internal/install/contracts_test.go | 6 +- internal/install/install.go | 63 +++++++++-- internal/install/install_test.go | 60 +++++++++++ .../install/testdata/entrypoints.golden.json | 12 +-- internal/runtimeassets/runtime.go | 25 +++-- internal/validator/pre_specification_test.go | 41 +++++++ internal/validator/rules.go | 47 ++++++++ internal/validator/validator.go | 20 ++++ starter/product/.product/artifacts.json | 21 ++++ .../product/design/system/design-system.md | 3 +- .../product/engineering/engineering-system.md | 3 +- starter/product/knowledge/README.md | 1 + .../assessments/product-landscape.md | 21 ++++ 29 files changed, 470 insertions(+), 49 deletions(-) create mode 100644 internal/install/code_discovery.go create mode 100644 internal/validator/pre_specification_test.go create mode 100644 starter/product/knowledge/assessments/product-landscape.md diff --git a/FRAMEWORK.md b/FRAMEWORK.md index 70ac06b..9a524c0 100644 --- a/FRAMEWORK.md +++ b/FRAMEWORK.md @@ -4,6 +4,12 @@ This document consolidates the architecture of the AI-driven Product Engineering The goal is not just to organize documents. The goal is to create a pipeline where product, specification, planning, execution, and audit form a single system. +## 1.1 Pre-Specification Baseline + +Before a real Specification is authored, every product establishes a complete Product Landscape and the shared `knowledge/`, `engineering/`, and `design/system/` contracts. With existing code, these documents distinguish observed evidence from inference and inventory every code root, module, interface, data boundary, integration, rule, test, configuration, and design asset before domain modeling. Without code, they are explicit hypotheses with pending human decisions. A delivery Domain, Goal, Feature, or Use Case is derived only after the landscape is complete; a Specification requires the approved Product Landscape, Engineering System, and Design System. + +Implementation roots sit beside `product/` and use semantic role names such as `web/`, `api/`, `worker/`, `mobile/`, `infrastructure/`, or `library/`. The framework records them in the manifest rather than embedding application code. When a requested implementation does not exist, the responsible agent first identifies the intended stack and verifies the official scaffolding command before proposing its execution. + ## 1. Thesis This framework treats documentation as engineering infrastructure. diff --git a/docs/starting-points.md b/docs/starting-points.md index 4991250..8a1fad9 100644 --- a/docs/starting-points.md +++ b/docs/starting-points.md @@ -4,6 +4,8 @@ Um `starting-point` descreve a situação do produto no momento em que um repositório começa a usar o Spec Framework. +Independentemente do ponto de partida, antes de uma Specification real o produto precisa completar e aprovar três bases: `knowledge/assessments/product-landscape.md`, `engineering/engineering-system.md` e `design/system/design-system.md`. Com código, o Landscape inventaria todas as raízes de código, módulos, superfícies, dados, integrações, regras, testes, configuração e ativos de design; sem código, esses contratos registram hipóteses explícitas e decisões pendentes. + Ele não representa: - o entrypoint técnico da aplicação, como `main.go` ou `index.ts`; diff --git a/framework/adoption.md b/framework/adoption.md index 81d0ad0..adbda50 100644 --- a/framework/adoption.md +++ b/framework/adoption.md @@ -39,6 +39,16 @@ Initialization plans the complete tree, explicit empty directories, and artifact For existing documents, use `--starting-point existing-documents` with `--source-dir` or `--sources`. The command inventories sources under `product/knowledge/imports/` but does not create Domains, User Goals, or Features without explicit approval. +## Code-aware initialization + +Keep implementation roots beside `product/`, using semantic roles such as `web/`, `api/`, `worker/`, `mobile/`, `infrastructure/`, or `library/`. `init` detects common roots at the repository top level and can receive an explicit mapping when discovery needs help: + +```powershell +spec-framework init . --agents codex --starting-point existing-implementation --code-roots web:web,api:api --yes +``` + +The resulting `product/knowledge/assessments/product-landscape.md` is the required comprehensive inventory. Complete it, `engineering/engineering-system.md`, and `design/system/design-system.md` before authoring a real Specification. With no code, these documents are explicit hypotheses; first confirm the intended stack and official scaffold command, then create the semantic sibling root rather than placing code under `product/`. + After the Artifact Importer fills `mapping.json`, review the selected mappings and materialize them explicitly: ```bash diff --git a/framework/init/bootstrap.json b/framework/init/bootstrap.json index 2dcfb6f..a8ac6a7 100644 --- a/framework/init/bootstrap.json +++ b/framework/init/bootstrap.json @@ -14,7 +14,7 @@ {"id":"strategy","title":"Define the strategy","goal":"Choose the first scoped delivery bet.","read":["product/foundation/vision/vision.md","product/foundation/vision/principles.md","product/foundation/vision/north-star.md","product/foundation/strategy/context.md"],"write":["product/foundation/strategy/strategy.md","product/foundation/strategy/personas.md","product/foundation/strategy/metrics.md","product/foundation/strategy/roadmap.md"],"prompt":"Propose strategy, target segments, trade-offs, metrics, roadmap, and delivery levels using the approved Foundation. Ask before making material scope decisions. Stop before approval.","gate":"Approve Strategy before modeling delivery domains.","next":"First domain"} ], "rules":["Read the nearest context.md before changing an artifact.","Write drafts only after showing the proposed change.","Never change status manually or create approval records.","Record assumptions, gaps, and decisions separately."], - "after":"Read the pinned examples/events reference, then create one Domain -> User Goal -> Feature -> Use Case walking skeleton. Do not create a workspace until the feature is stable and registered." + "after":"Complete and approve the Product Landscape, Engineering System, and shared Design System before modeling the complete domain map. Then create Domain -> User Goal -> Feature -> Use Case; do not create a Specification or workspace until the pre-Specification baselines are approved." }, "existing-product": { "title": "Document an operating product", @@ -26,7 +26,7 @@ {"id":"strategy","title":"Define future direction","goal":"Turn the current baseline into explicit future bets.","read":["product/foundation/product-baseline.md","product/foundation/strategy/context.md","existing decisions and metrics"],"write":["product/foundation/strategy/strategy.md","product/foundation/strategy/personas.md","product/foundation/strategy/metrics.md","product/foundation/strategy/roadmap.md"],"prompt":"Read the approved Product Baseline and propose future bets, trade-offs, segments, metrics, and roadmap. Ask before inferring strategic intent from implementation evidence. Stop before approval.","gate":"Approve Strategy before delivery modeling.","next":"First domain"} ], "rules":["Separate observed current state from future intent.","Cite repository or operational evidence.","Do not rewrite product history or approval records.","Escalate to full Foundation when audience, value, or direction is uncertain."], - "after":"Read the pinned examples/events reference and model the first Domain -> User Goal -> Feature -> Use Case slice." + "after":"Complete and approve the Product Landscape, Engineering System, and shared Design System before modeling the complete domain map and the first Domain -> User Goal -> Feature -> Use Case slice." }, "existing-documents": { "title": "Bring existing documents into the framework", @@ -52,7 +52,7 @@ {"id":"walking-skeleton","title":"Model the bounded delivery slice","goal":"Create one Domain -> User Goal -> Feature -> Use Case chain that fits the brief.","read":["approved product/foundation/feature-brief.md","pinned examples/events/ reference","relevant context.md files"],"write":["product/domains//... draft artifacts"],"prompt":"Use the approved Feature Brief and the examples/events reference. Propose the smallest domain, goal, feature, and use case chain that delivers the brief. Preserve parent and non-ownership boundaries. Stop before approval.","gate":"Approve the relevant artifacts through their normal owners and parents.","next":"Specification"} ], "rules":["Keep the scope bounded to the Feature Brief.","Do not invent product-wide strategy.","Do not create a workspace before the Feature Brief gate.","Do not skip the walking skeleton."], - "after":"Continue through Use Case, Specification, Design when applicable, planning, tasks, and delivery gates." + "after":"Complete and approve the Product Landscape, Engineering System, and shared Design System before creating a Specification; then continue through Design when applicable, planning, tasks, and delivery gates." }, "existing-implementation": { "title": "Understand an existing codebase", @@ -64,7 +64,7 @@ {"id":"foundation","title":"Recover product intent carefully","goal":"Use the assessment as evidence for the full Foundation sequence.","read":["approved implementation-assessment.md","product/foundation/*/context.md","relevant decisions"],"write":["foundation/problem/problem.md","foundation/vision/*","foundation/strategy/*"],"prompt":"Propose Problem, Vision, Principles, North Star, and Strategy one artifact at a time. Ask questions wherever implementation evidence does not prove user intent. Preserve the distinction between observed behavior and approved product direction.","gate":"Approve each Foundation artifact individually.","next":"First domain"} ], "rules":["Code is evidence, not approved product scope.","Do not change application code during bootstrap.","Separate observed behavior from inferred intent.","Require the full Foundation before delivery work."], - "after":"Read the examples/events reference and continue through the first domain walking skeleton." + "after":"Complete and approve the Product Landscape, Engineering System, and shared Design System before domain modeling, then read the examples/events reference and continue through the first domain walking skeleton." }, "audit-only": { "title": "Inspect without changing the product", diff --git a/framework/init/catalog.json b/framework/init/catalog.json index 0985d3b..33bb087 100644 --- a/framework/init/catalog.json +++ b/framework/init/catalog.json @@ -14,8 +14,8 @@ "delivery-validation": {"asset_sets": [], "artifact_types": ["tests", "qa-evidence", "security-review", "audit"]}, "delivery-execution": {"asset_sets": [], "artifact_types": ["execution-graph", "taskset", "task"]}, "governance": {"asset_sets": ["governance"], "artifact_types": ["decision", "release"]}, - "knowledge": {"asset_sets": ["knowledge"], "artifact_types": []}, - "knowledge-security-baseline": {"asset_sets": ["knowledge-security-baseline"], "artifact_types": ["security-baseline"]} + "knowledge": {"asset_sets": ["knowledge"], "artifact_types": ["product-landscape"]}, + "knowledge-security-baseline": {"asset_sets": ["knowledge-security-baseline"], "artifact_types": ["security-baseline", "product-landscape"]} }, "sets": { "product-core": [ @@ -37,7 +37,8 @@ { "source": "starter/product/knowledge", "target": "knowledge" } ], "knowledge-security-baseline": [ - { "source": "starter/product/knowledge/conventions/security-baseline.md", "target": "knowledge/conventions/security-baseline.md" } + { "source": "starter/product/knowledge/conventions/security-baseline.md", "target": "knowledge/conventions/security-baseline.md" }, + { "source": "starter/product/knowledge/assessments/product-landscape.md", "target": "knowledge/assessments/product-landscape.md" } ], "delivery-domains": [ { "source": "starter/product/domains", "target": "domains" } diff --git a/framework/init/contracts/audit-only.json b/framework/init/contracts/audit-only.json index 04796a5..946b076 100644 --- a/framework/init/contracts/audit-only.json +++ b/framework/init/contracts/audit-only.json @@ -1,12 +1,12 @@ { "schema_version": 1, "id": "audit-only", - "asset_sets": ["product-core", "knowledge-security-baseline", "governance"], + "asset_sets": ["product-core", "knowledge-security-baseline", "delivery-design", "delivery-engineering", "governance"], "directories": [], "files": [], "patches": [], "registry": { - "modules": ["product-core", "knowledge-security-baseline", "governance"], + "modules": ["product-core", "knowledge-security-baseline", "delivery-design", "delivery-engineering", "governance"], "exclude_types": ["problem", "vision", "product-principles", "north-star", "strategy", "domain", "user-goal", "feature", "use-case"] }, "bootstrap_profile": "audit-only", diff --git a/framework/init/contracts/existing-documents.json b/framework/init/contracts/existing-documents.json index 59b7e51..9f4d88a 100644 --- a/framework/init/contracts/existing-documents.json +++ b/framework/init/contracts/existing-documents.json @@ -1,12 +1,12 @@ { "schema_version": 1, "id": "existing-documents", - "asset_sets": ["product-core", "knowledge", "governance"], + "asset_sets": ["product-core", "knowledge", "delivery-design", "delivery-engineering", "governance"], "directories": ["knowledge/imports/sources", "knowledge/imports/runs"], "files": [], "patches": [], "registry": { - "modules": ["product-core", "knowledge", "governance"], + "modules": ["product-core", "knowledge", "delivery-design", "delivery-engineering", "governance"], "exclude_types": ["problem", "vision", "product-principles", "north-star", "strategy", "domain", "user-goal", "feature", "use-case"] }, "bootstrap_profile": "existing-documents", diff --git a/framework/init/contracts/existing-feature.json b/framework/init/contracts/existing-feature.json index 20cc01a..76dd344 100644 --- a/framework/init/contracts/existing-feature.json +++ b/framework/init/contracts/existing-feature.json @@ -1,7 +1,7 @@ { "schema_version": 1, "id": "existing-feature", - "asset_sets": ["product-core", "knowledge", "delivery-domains", "governance"], + "asset_sets": ["product-core", "knowledge", "delivery-domains", "delivery-design", "delivery-engineering", "governance"], "directories": [], "files": [ { @@ -23,7 +23,7 @@ } ], "registry": { - "modules": ["product-core", "foundation-feature", "knowledge", "delivery-domain", "governance"], + "modules": ["product-core", "foundation-feature", "knowledge", "delivery-domain", "delivery-design", "delivery-engineering", "governance"], "required_artifacts": [{"type": "feature-brief", "path": "foundation/feature-brief.md"}], "exclude_types": ["problem", "vision", "product-principles", "north-star", "strategy"], "prepend_artifacts": [ diff --git a/framework/install.md b/framework/install.md index 4789543..ec09388 100644 --- a/framework/install.md +++ b/framework/install.md @@ -59,6 +59,8 @@ spec-framework init ../my-product --agents codex,cursor,claude --yes Use `--starting-point existing-documents` with `--source-dir` or `--sources` to bootstrap from existing product material. This creates a scalable analysis-only import run with paged inventory and review chunks; use `--import-max-files`, `--import-max-total-bytes`, `--import-max-file-bytes`, and `--import-chunk-size` to set the explicit bootstrap budget. It never treats source prose as approved product truth. +`init` also detects common implementation roots beside `product/`. Use `--code-roots ` to declare a root explicitly, for example `--code-roots web:web,api:api`. It records the result in the product manifest and creates `knowledge/assessments/product-landscape.md`. The Product Landscape, Engineering System, and shared Design System are mandatory pre-Specification baselines for new products; legacy manifests remain compatible until upgraded into this policy. + For a large document set, create a bounded scalable run with `import create`, inspect it with `import status`, and resume one bounded chunk at a time. Draft materialization remains a separate explicit command after sources/chunks and mappings have been reviewed: `spec-framework import materialize --run IMPORT-001 --approved-by "Product Owner" --yes`. The generated `product/BOOTSTRAP.md` is rendered from the starting-point map in `framework/init/bootstrap.json`; each step names the user goal, agent reading set, writable draft paths, prompt, gate, and next handoff. For imports it also pins the active run id. `spec-framework work` remains blocked until that latest run records explicit materialization approval and at least one materialized draft path. This gate does not approve the resulting product artifacts. diff --git a/framework/skills/design-system/SKILL.md b/framework/skills/design-system/SKILL.md index 3a05105..5a04427 100644 --- a/framework/skills/design-system/SKILL.md +++ b/framework/skills/design-system/SKILL.md @@ -37,7 +37,7 @@ Follow the shared [Discovery And Challenge contract](../discovery-and-challenge. Use scripts/invoke-cli.ps1 on Windows or scripts/invoke-cli.sh on macOS/Linux for the CLI operation in this skill's reviewed scope. The wrapper never adds --yes or an approver identity. ## Workflow -1. Confirm whether origin is `generate`, `evolve`, or `adopt`; never promote a reference to canonical authority implicitly. +1. Confirm whether origin is `generate`, `evolve`, or `adopt`; never promote a reference to canonical authority implicitly. When there is no interface source, establish explicit accessibility and experience hypotheses before Specifications rather than omitting the shared baseline. 2. Inventory foundations, sources, tokens, themes, components, patterns, consumers, and implementation links. 3. Create or update `context.md` and `design-system.md` using canonical templates. 4. Maintain tool-independent tokens in primitive, semantic, and component layers; resolve aliases and reject cycles. diff --git a/framework/skills/domain-architect/SKILL.md b/framework/skills/domain-architect/SKILL.md index b436f8f..34e5ecb 100644 --- a/framework/skills/domain-architect/SKILL.md +++ b/framework/skills/domain-architect/SKILL.md @@ -35,7 +35,7 @@ domain.md files; domain context.md files; ubiquitous language; invariants; comma Follow the shared [Discovery And Challenge contract](../discovery-and-challenge.md) before substantive creation or material revision. ## Workflow -1. Read the parent context and confirm the artifact status. +1. Read the parent context, the approved Product Landscape, Engineering System, and Design System, then confirm the artifact status. Stop when any pre-Specification baseline is incomplete. 2. Identify missing information, assumptions, conflicts, and dependencies. 3. Partition responsibilities into coherent business areas; do not use the product name, a UI navigation section, or a catch-all domain as the boundary. 4. Propose the artifact or revision using the matching template, including Owns, Does Not Own, and cross-domain contracts. diff --git a/framework/skills/engineering-system/SKILL.md b/framework/skills/engineering-system/SKILL.md index 60abdc7..9f4dbd9 100644 --- a/framework/skills/engineering-system/SKILL.md +++ b/framework/skills/engineering-system/SKILL.md @@ -39,7 +39,7 @@ Follow the shared [Discovery And Challenge contract](../discovery-and-challenge. Use scripts/invoke-cli.ps1 on Windows or scripts/invoke-cli.sh on macOS/Linux for the CLI operation in this skill's reviewed scope. The wrapper never adds --yes or an approver identity. ## Workflow -1. Inspect the real product code, tests, configuration, environments, operations evidence, and existing engineering documents. +1. Inspect the real product code, tests, configuration, environments, operations evidence, and existing engineering documents. If no code exists, create explicit hypothesis contracts, pending decisions, and intended constraints rather than pretending evidence exists. 2. Define the covered product and repository boundaries and choose `generate`, `evolve`, or `adopt`. 3. Inventory modules, ownership, data, integrations, standards, quality attributes, test strategy, gates, environments, test data, runbooks, and consumers with evidence paths. 4. Maintain `engineering/quality/quality-system.md` and `quality-system.yaml` together with the quality model, test strategy, and fitness functions. Keep commands canonical in `knowledge/conventions/gates.md`. diff --git a/framework/skills/framework-guide/SKILL.md b/framework/skills/framework-guide/SKILL.md index 0723cbc..4dd1e6f 100644 --- a/framework/skills/framework-guide/SKILL.md +++ b/framework/skills/framework-guide/SKILL.md @@ -60,7 +60,7 @@ Intent summary; discovered state; recommended command or specialist; mutation pr | Install without an existing CLI | Download and inspect `scripts/install.ps1` or `scripts/install.sh`, then run the chosen bootstrap; installation does not initialize a product, and piping a remote script remains an explicit user choice | | Update or remove the installed CLI | Use `spec-framework update [--check | --version ]` or preview `spec-framework uninstall [--purge]`; mutations require `--yes` and never remove product repositories | | Start a product interactively | `spec-framework init ` | -| Start a product headlessly | `spec-framework init --agents --yes` | +| Start a product headlessly | `spec-framework init --agents --yes`; declare known sibling implementation roots with `--code-roots web:web,api:api` | | Bring existing documents | `init --starting-point existing-documents`, review the latest run, then `import materialize`; resulting artifacts remain draft | | Adopt a code-first operating product | `init --starting-point existing-product`, approve `foundation/product-baseline.md`, then approve Strategy | | Deliver one bounded existing feature | `init --starting-point existing-feature`, complete and individually approve `foundation/feature-brief.md`, then `work` | @@ -100,11 +100,13 @@ When the intent concerns a decision or ADR, first run `spec-framework decisions 6. For `existing-feature`, route through the registered Feature Brief instead of the full product Foundation. Escalate to Problem, Vision, Product Principles, North Star, and Strategy when the requested work reveals broad or uncertain product direction. 7. For `existing-implementation`, route through the registered Implementation Assessment before full Foundation. Treat code, tests, configuration, and history as evidence rather than approved product intent. 8. For `existing-product`, consolidate evidenced current state in Product Baseline and keep future Strategy separate. Escalate to full Foundation when current audience, value, or direction is uncertain. -9. For `audit-only`, use terminal-output inspection commands without write flags. Do not create reports, registry changes, approvals, workspaces, migrations, or delivery state; request an explicit starting-point transition before product work. -10. Require explicit human identity and confirmation for approval commands. For batch approval, preview the exact artifact list and hashes with `approve-batch` before applying `--yes`; ask the human which scope to approve and never convert conversational agreement into unrelated product approval records. -11. Route artifact authorship to the owner skill named by `guide` or `dashboard`; do not generate the artifact yourself. -12. After a command, report exit status, changed artifacts, blockers, and the next safe command. Re-read mechanical state instead of assuming success. -13. Stop on stale parents, missing approvals, ambiguous scope, unsafe remote/destructive action, missing gate configuration, or conflicts requiring a decision. +9. Before any delivery Domain, Goal, Feature, Use Case, or Specification, read `knowledge/assessments/product-landscape.md`, `engineering/engineering-system.md`, and `design/system/design-system.md`. For code roots declared in the manifest, inventory every root comprehensively and separate observed evidence from inferred intent. For no-code products, establish these as explicit draft hypotheses and identify the intended stack and official scaffold command before proposing implementation creation. +10. Keep implementation projects as semantic sibling roots beside `product/` (`web/`, `api/`, `worker/`, `mobile/`, `infrastructure/`, or `library/`); never place application code inside `product/`. +11. For `audit-only`, use terminal-output inspection commands without write flags. Do not create reports, registry changes, approvals, workspaces, migrations, or delivery state; request an explicit starting-point transition before product work. +12. Require explicit human identity and confirmation for approval commands. For batch approval, preview the exact artifact list and hashes with `approve-batch` before applying `--yes`; ask the human which scope to approve and never convert conversational agreement into unrelated product approval records. +13. Route artifact authorship to the owner skill named by `guide` or `dashboard`; do not generate the artifact yourself. +14. After a command, report exit status, changed artifacts, blockers, and the next safe command. Re-read mechanical state instead of assuming success. +15. Stop on stale parents, missing approvals, ambiguous scope, unsafe remote/destructive action, missing gate configuration, or conflicts requiring a decision. ## Runtime and repository cleanliness diff --git a/framework/skills/product-orchestrator/SKILL.md b/framework/skills/product-orchestrator/SKILL.md index 64976ce..4bf5acc 100644 --- a/framework/skills/product-orchestrator/SKILL.md +++ b/framework/skills/product-orchestrator/SKILL.md @@ -19,7 +19,7 @@ Orchestrator. Controls workflow, gates, handoffs, and approval checkpoints. It s - Approved decisions are discovered through the active product root's `.product/decisions.json`; resolve each registered `path` from its declared domain root. ## Default sequence -Problem Discovery -> Vision -> Strategy -> Domain Architect -> User Goal -> Roadmap alignment +Problem Discovery -> Vision -> Strategy -> Product Landscape -> Knowledge, Engineering System, and Design System baselines -> Domain Architect -> User Goal -> Roadmap alignment For a repository whose canonical manifest declares `starting_point: existing-feature`, replace that sequence with Feature Brief -> explicit individual approval -> existing Feature selection -> workspace. Escalate to the default sequence when the feature cannot be bounded without product-wide decisions. @@ -41,6 +41,9 @@ Follow the shared [Discovery And Challenge contract](../discovery-and-challenge. 7. Do not require or synthesize full Foundation artifacts for a bounded `existing-feature` route; own `foundation/feature-brief.md` as the proportional entry contract. 8. For `existing-implementation`, own `knowledge/assessments/implementation-assessment.md`, do not modify application code during assessment, and retain the full Foundation sequence before workspace creation. 9. For `existing-product`, own `foundation/product-baseline.md`, keep uncertain intent visible, and promote to the full Foundation sequence when the baseline cannot establish audience and delivered value confidently. +10. Before Domain modeling, inventory every declared code root comprehensively in `knowledge/assessments/product-landscape.md`; cover modules, user surfaces, data, integrations, business rules, tests, configuration, design assets, and operational constraints. Do not collapse a broad product into one feature-sized domain. +11. Establish `knowledge/`, `engineering/`, and `design/system/` as evidence-backed baselines when code exists, or as explicit draft hypotheses when it does not. Their approval is required before a Specification. +12. Keep implementation repositories as semantic siblings of `product/`; when code must be created, route stack selection and official scaffold-command verification before any implementation action. ## Outputs foundation artifacts; domain map; initial goal catalog; roadmap with Delivery Levels and Priorities; open decisions; next recommended feature slices. diff --git a/internal/cli/app.go b/internal/cli/app.go index ae8e049..0fb8607 100644 --- a/internal/cli/app.go +++ b/internal/cli/app.go @@ -712,6 +712,7 @@ func (app App) runInit(args []string, stdout, stderr io.Writer) int { agentsValue := flags.String("agents", "", "comma-separated agents") startingPoint := flags.String("starting-point", "new-product", "new-product, existing-product, existing-documents, existing-feature, existing-implementation, or audit-only") sourcesValue := flags.String("sources", "", "comma-separated source files or directories for existing-documents") + codeRootsValue := flags.String("code-roots", "", "comma-separated implementation roots as path:role (for example web:web,api:api)") sourceDir := flags.String("source-dir", "", "source directory for existing-documents") importMaxFiles := flags.Int("import-max-files", 500, "maximum files for existing-documents import") importMaxTotal := flags.String("import-max-total-bytes", "200MB", "maximum copied bytes for existing-documents import") @@ -771,6 +772,11 @@ func (app App) runInit(args []string, stdout, stderr io.Writer) int { return 2 } sources := splitCSV(*sourcesValue) + codeRoots, err := parseCodeRoots(*codeRootsValue) + if err != nil { + fmt.Fprintln(stderr, err) + return 2 + } if strings.TrimSpace(*sourceDir) != "" { sources = append(sources, *sourceDir) } @@ -784,7 +790,7 @@ func (app App) runInit(args []string, stdout, stderr io.Writer) int { fmt.Fprintln(stderr, err) return 2 } - result, err := install.Init(install.Options{Target: *target, Version: app.version, Agents: agents, StartingPoint: point, Sources: sources, ImportOptions: sourceimport.CreateOptions{MaxFiles: *importMaxFiles, MaxTotalBytes: importTotal, MaxFileBytes: importFile, ChunkSize: *importChunkSize, BinaryPolicy: *importBinaryPolicy}, Force: *force}) + result, err := install.Init(install.Options{Target: *target, Version: app.version, Agents: agents, StartingPoint: point, Sources: sources, CodeRoots: codeRoots, ImportOptions: sourceimport.CreateOptions{MaxFiles: *importMaxFiles, MaxTotalBytes: importTotal, MaxFileBytes: importFile, ChunkSize: *importChunkSize, BinaryPolicy: *importBinaryPolicy}, Force: *force}) if err != nil { fmt.Fprintln(stderr, err) return 1 @@ -793,6 +799,9 @@ func (app App) runInit(args []string, stdout, stderr io.Writer) int { if result.ImportID != "" { fmt.Fprintf(stdout, "- Import inventory: product/knowledge/imports/runs/%s\n", result.ImportID) } + if len(codeRoots) > 0 { + fmt.Fprintln(stdout, "- Declared code roots: product/knowledge/assessments/product-landscape.md") + } if *installImpeccable { fmt.Fprintln(stdout, "[1/3] Resolving Impeccable version...") resolved, resolveErr := adapters.ResolveVersion("impeccable", *impeccableVersion) @@ -877,6 +886,18 @@ func splitCSV(value string) []string { return out } +func parseCodeRoots(value string) ([]runtimeassets.CodeRoot, error) { + var roots []runtimeassets.CodeRoot + for _, item := range splitCSV(value) { + parts := strings.SplitN(item, ":", 2) + if len(parts) != 2 || strings.TrimSpace(parts[0]) == "" || strings.TrimSpace(parts[1]) == "" { + return nil, fmt.Errorf("invalid --code-roots entry %q; use path:role", item) + } + roots = append(roots, runtimeassets.CodeRoot{Path: strings.TrimSpace(parts[0]), Role: strings.TrimSpace(parts[1])}) + } + return roots, nil +} + func (app App) runUpgrade(args []string, stdout, stderr io.Writer) int { flags := flag.NewFlagSet("upgrade", flag.ContinueOnError) flags.SetOutput(stderr) diff --git a/internal/install/code_discovery.go b/internal/install/code_discovery.go new file mode 100644 index 0000000..d0743bb --- /dev/null +++ b/internal/install/code_discovery.go @@ -0,0 +1,101 @@ +package install + +import ( + "fmt" + "os" + "path/filepath" + "sort" + "strings" + + "github.com/JonatasFreireDev/spec-framework/internal/runtimeassets" +) + +var codeMarkers = map[string]string{ + "package.json": "web", + "go.mod": "service", + "Cargo.toml": "service", + "pom.xml": "service", + "build.gradle": "service", + "build.gradle.kts": "service", +} + +// discoverCodeRoots inventories immediate repository siblings. Explicit roots +// supplement discovery and let an adopter choose a semantic role such as api, +// web, worker, mobile, infrastructure, or library. +func discoverCodeRoots(target string, explicit []runtimeassets.CodeRoot) ([]runtimeassets.CodeRoot, error) { + byPath := map[string]runtimeassets.CodeRoot{} + for _, root := range explicit { + path := filepath.ToSlash(strings.Trim(strings.TrimSpace(root.Path), "/")) + role := strings.ToLower(strings.TrimSpace(root.Role)) + if path == "" || role == "" || path == "product" || strings.HasPrefix(path, "../") || filepath.IsAbs(path) { + return nil, fmt.Errorf("invalid code root %q; use a repository-relative sibling path with a role", root.Path) + } + byPath[path] = runtimeassets.CodeRoot{Path: path, Role: role} + } + entries, err := os.ReadDir(target) + if err != nil { + return nil, err + } + for _, entry := range entries { + if !entry.IsDir() || strings.HasPrefix(entry.Name(), ".") || entry.Name() == "product" || entry.Name() == "node_modules" || entry.Name() == "vendor" { + continue + } + candidate := filepath.Join(target, entry.Name()) + role := detectCodeRole(candidate) + if role != "" { + path := filepath.ToSlash(entry.Name()) + if _, exists := byPath[path]; !exists { + byPath[path] = runtimeassets.CodeRoot{Path: path, Role: role} + } + } + } + roots := make([]runtimeassets.CodeRoot, 0, len(byPath)) + for _, root := range byPath { + roots = append(roots, root) + } + sort.Slice(roots, func(i, j int) bool { return roots[i].Path < roots[j].Path }) + return roots, nil +} + +func detectCodeRole(root string) string { + for marker, role := range codeMarkers { + if _, err := os.Stat(filepath.Join(root, marker)); err == nil { + return role + } + } + if matches, _ := filepath.Glob(filepath.Join(root, "*.sln")); len(matches) > 0 { + return "service" + } + if matches, _ := filepath.Glob(filepath.Join(root, "*.csproj")); len(matches) > 0 { + return "service" + } + if matches, _ := filepath.Glob(filepath.Join(root, "*.py")); len(matches) > 0 { + return "service" + } + return "" +} + +func writeCodeDiscovery(productRoot string, roots []runtimeassets.CodeRoot) error { + path := filepath.Join(productRoot, "knowledge", "assessments", "product-landscape.md") + if _, err := os.Stat(path); err != nil { + return err + } + var b strings.Builder + b.WriteString("# Product Landscape\n\n## Snapshot\n\n| Field | Value |\n| --- | --- |\n| Status | `draft` |\n| Evidence mode | `") + if len(roots) == 0 { + b.WriteString("hypothesis") + } else { + b.WriteString("observed-code") + } + b.WriteString("` |\n\n## Code Roots\n\n") + if len(roots) == 0 { + b.WriteString("No implementation root was detected. Define the intended product surface, language, framework, and official scaffold command before creating code.\n") + } else { + b.WriteString("| Path | Role | Discovery status |\n| --- | --- | --- |\n") + for _, root := range roots { + fmt.Fprintf(&b, "| `%s/` | `%s` | `pending comprehensive inventory` |\n", root.Path, root.Role) + } + } + b.WriteString("\n## Required Inventory\n\nBefore domain modeling, inspect every declared code root for modules, routes or UI surfaces, data models, integrations, business rules, tests, configuration, design assets, and operational constraints. Record evidence paths and unresolved ownership boundaries; do not reduce a broad codebase to a single feature slice.\n\n## Domain Map\n\nPending comprehensive inventory. List all candidate domains, ownership boundaries, cross-domain workflows, and uncovered areas before creating a delivery Domain, Goal, Feature, or Use Case.\n") + return writeFile(path, []byte(b.String()), 0644) +} diff --git a/internal/install/contracts_test.go b/internal/install/contracts_test.go index dce79a6..b442abc 100644 --- a/internal/install/contracts_test.go +++ b/internal/install/contracts_test.go @@ -64,18 +64,18 @@ func TestStartingPointMaterializationProfiles(t *testing.T) { }, "existing-feature": { present: []string{"foundation/feature-brief.md", "domains/_template-domain/context.md"}, - absent: []string{"foundation/problem/problem.md", "foundation/strategy/strategy.md", "design/system/context.md", "engineering/context.md"}, + absent: []string{"foundation/problem/problem.md", "foundation/strategy/strategy.md"}, }, "existing-documents": { present: []string{"context.md"}, - absent: []string{"foundation/README.md", "foundation/problem/problem.md", "domains/_template-domain/context.md", "design/system/context.md", "engineering/context.md"}, + absent: []string{"foundation/README.md", "foundation/problem/problem.md", "domains/_template-domain/context.md"}, }, "existing-implementation": { present: []string{"knowledge/assessments/implementation-assessment.md", "foundation/problem/problem.md", "domains/_template-domain/context.md"}, }, "audit-only": { present: []string{"audits/README.md", "knowledge/conventions/security-baseline.md"}, - absent: []string{"foundation/README.md", "foundation/problem/problem.md", "knowledge/conventions/gates.md", "domains/_template-domain/context.md", "design/system/context.md", "engineering/context.md"}, + absent: []string{"foundation/README.md", "foundation/problem/problem.md", "domains/_template-domain/context.md"}, }, } for point, expected := range tests { diff --git a/internal/install/install.go b/internal/install/install.go index 79247c7..987b614 100644 --- a/internal/install/install.go +++ b/internal/install/install.go @@ -27,12 +27,14 @@ const ( var agentRoots = map[Agent]string{Codex: "codex", Cursor: "cursor", Claude: "claude"} type Options struct { - Target, Version string - Agents []Agent - StartingPoint string - Sources []string - ImportOptions sourceimport.CreateOptions - Force bool + Target, Version string + Agents []Agent + StartingPoint string + Sources []string + CodeRoots []runtimeassets.CodeRoot + EnableBaselinePolicy bool + ImportOptions sourceimport.CreateOptions + Force bool } type Result struct { Target, SpecRoot string @@ -127,7 +129,14 @@ func Init(opts Options) (Result, error) { if err := writeInitializationPlan(filepath.Join(stagingRoot, "product"), plan); err != nil { return Result{}, err } - result, err := Upgrade(Options{Target: stagingRoot, Version: opts.Version, Agents: opts.Agents, StartingPoint: point, Force: true}) + codeRoots, err := discoverCodeRoots(target, opts.CodeRoots) + if err != nil { + return Result{}, err + } + if err := writeCodeDiscovery(filepath.Join(stagingRoot, "product"), codeRoots); err != nil { + return Result{}, err + } + result, err := Upgrade(Options{Target: stagingRoot, Version: opts.Version, Agents: opts.Agents, StartingPoint: point, CodeRoots: codeRoots, EnableBaselinePolicy: true, Force: true}) if err != nil { return Result{}, err } @@ -196,12 +205,24 @@ func Upgrade(opts Options) (Result, error) { if err != nil { return Result{}, err } + codeRoots := opts.CodeRoots + if len(codeRoots) == 0 { + codeRoots = installedCodeRoots(productManifest) + } + baselinePolicy := installedBaselinePolicy(productManifest) + if opts.EnableBaselinePolicy { + baselinePolicy = map[string]any{"pre_specification": "required"} + } manifest := map[string]any{ "schema_version": 3, "framework": "spec-framework", "version": version, "product_root": ".", "agents": agents, "starting_point": point, + "code_roots": codeRoots, "activation": map[string]any{"mode": "manifest-only"}, "runtime": map[string]any{"source": "user-cache", "channel": "stable"}, } + if len(baselinePolicy) > 0 { + manifest["baseline_policy"] = baselinePolicy + } if err := writeJSON(productManifest, manifest); err != nil { return Result{}, err } @@ -217,6 +238,34 @@ func Upgrade(opts Options) (Result, error) { return Result{Target: target, SpecRoot: spec, SkillCount: 0, StartingPoint: point}, nil } +func installedCodeRoots(path string) []runtimeassets.CodeRoot { + data, err := os.ReadFile(path) + if err != nil { + return nil + } + var value struct { + CodeRoots []runtimeassets.CodeRoot `json:"code_roots"` + } + if json.Unmarshal(data, &value) != nil { + return nil + } + return value.CodeRoots +} + +func installedBaselinePolicy(path string) map[string]any { + data, err := os.ReadFile(path) + if err != nil { + return nil + } + var value struct { + BaselinePolicy map[string]any `json:"baseline_policy"` + } + if json.Unmarshal(data, &value) != nil { + return nil + } + return value.BaselinePolicy +} + func installedStartingPoint(path string) string { data, err := os.ReadFile(path) if err != nil { diff --git a/internal/install/install_test.go b/internal/install/install_test.go index a077614..73e36a2 100644 --- a/internal/install/install_test.go +++ b/internal/install/install_test.go @@ -47,6 +47,66 @@ func TestInitShipsLeanReadmeSurfaceAndUpgradePreservesAdopterReadmes(t *testing. } } +func TestInitDiscoversSiblingCodeRootsAndPreservesThemOnUpgrade(t *testing.T) { + t.Setenv("SPEC_FRAMEWORK_CACHE", filepath.Join(t.TempDir(), "cache")) + t.Setenv("SPEC_FRAMEWORK_AGENT_HOME", filepath.Join(t.TempDir(), "agents")) + target := filepath.Join(t.TempDir(), "product-repo") + if err := os.MkdirAll(filepath.Join(target, "web"), 0755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(target, "web", "package.json"), []byte(`{"name":"web"}`), 0644); err != nil { + t.Fatal(err) + } + if _, err := Init(Options{Target: target, Version: "test", Agents: []Agent{Codex}}); err != nil { + t.Fatal(err) + } + manifest, err := os.ReadFile(filepath.Join(target, "product", ".product", "framework.json")) + if err != nil || !strings.Contains(string(manifest), `"path": "web"`) || !strings.Contains(string(manifest), `"role": "web"`) { + t.Fatalf("code root missing from manifest: %s %v", manifest, err) + } + landscape, err := os.ReadFile(filepath.Join(target, "product", "knowledge", "assessments", "product-landscape.md")) + if err != nil || !strings.Contains(string(landscape), "`web/`") || !strings.Contains(string(landscape), "pending comprehensive inventory") { + t.Fatalf("landscape missing discovered root: %s %v", landscape, err) + } + if _, err := Upgrade(Options{Target: target, Version: "test", Agents: []Agent{Codex}}); err != nil { + t.Fatal(err) + } + manifest, err = os.ReadFile(filepath.Join(target, "product", ".product", "framework.json")) + if err != nil || !strings.Contains(string(manifest), `"path": "web"`) { + t.Fatalf("upgrade discarded code roots: %s %v", manifest, err) + } +} + +func TestUpgradeDoesNotOptLegacyProductsIntoBaselinePolicy(t *testing.T) { + t.Setenv("SPEC_FRAMEWORK_CACHE", filepath.Join(t.TempDir(), "cache")) + t.Setenv("SPEC_FRAMEWORK_AGENT_HOME", filepath.Join(t.TempDir(), "agents")) + target := filepath.Join(t.TempDir(), "product-repo") + if _, err := Init(Options{Target: target, Version: "test", Agents: []Agent{Codex}}); err != nil { + t.Fatal(err) + } + manifestPath := filepath.Join(target, "product", ".product", "framework.json") + data, err := os.ReadFile(manifestPath) + if err != nil { + t.Fatal(err) + } + var manifest map[string]any + if err := json.Unmarshal(data, &manifest); err != nil { + t.Fatal(err) + } + delete(manifest, "baseline_policy") + updated, _ := json.Marshal(manifest) + if err := os.WriteFile(manifestPath, updated, 0644); err != nil { + t.Fatal(err) + } + if _, err := Upgrade(Options{Target: target, Version: "test", Agents: []Agent{Codex}}); err != nil { + t.Fatal(err) + } + data, err = os.ReadFile(manifestPath) + if err != nil || strings.Contains(string(data), "baseline_policy") { + t.Fatalf("upgrade opted legacy product into policy: %s %v", data, err) + } +} + func TestInitExistingFeatureActivatesOnlyFeatureBriefFoundation(t *testing.T) { t.Setenv("SPEC_FRAMEWORK_CACHE", filepath.Join(t.TempDir(), "cache")) t.Setenv("SPEC_FRAMEWORK_AGENT_HOME", filepath.Join(t.TempDir(), "agents")) diff --git a/internal/install/testdata/entrypoints.golden.json b/internal/install/testdata/entrypoints.golden.json index 1401d3d..ee135f5 100644 --- a/internal/install/testdata/entrypoints.golden.json +++ b/internal/install/testdata/entrypoints.golden.json @@ -1,36 +1,36 @@ { "new-product": { - "artifact_ids": ["PROBLEM-TBD", "VISION-TBD", "PRINCIPLES-TBD", "NORTH-STAR-TBD", "STRATEGY-TBD", "DOMAIN-TEMPLATE", "GOAL-TEMPLATE", "FT-TEMPLATE", "UC-TEMPLATE"], + "artifact_ids": ["PROBLEM-TBD", "VISION-TBD", "PRINCIPLES-TBD", "NORTH-STAR-TBD", "STRATEGY-TBD", "LANDSCAPE-TBD", "ENGSYS-001", "DSYS-001", "DOMAIN-TEMPLATE", "GOAL-TEMPLATE", "FT-TEMPLATE", "UC-TEMPLATE"], "special_files": [], "directories": [], "actions": [] }, "existing-product": { - "artifact_ids": ["PRODUCT-BASELINE-TBD", "STRATEGY-TBD", "DOMAIN-TEMPLATE", "GOAL-TEMPLATE", "FT-TEMPLATE", "UC-TEMPLATE"], + "artifact_ids": ["PRODUCT-BASELINE-TBD", "STRATEGY-TBD", "LANDSCAPE-TBD", "ENGSYS-001", "DSYS-001", "DOMAIN-TEMPLATE", "GOAL-TEMPLATE", "FT-TEMPLATE", "UC-TEMPLATE"], "special_files": ["foundation/product-baseline.md"], "directories": [], "actions": [] }, "existing-documents": { - "artifact_ids": [], + "artifact_ids": ["LANDSCAPE-TBD", "ENGSYS-001", "DSYS-001"], "special_files": [], "directories": ["knowledge/imports/runs", "knowledge/imports/sources"], "actions": ["create-import-run"] }, "existing-feature": { - "artifact_ids": ["FEATURE-BRIEF-TBD", "DOMAIN-TEMPLATE", "GOAL-TEMPLATE", "FT-TEMPLATE", "UC-TEMPLATE"], + "artifact_ids": ["FEATURE-BRIEF-TBD", "LANDSCAPE-TBD", "ENGSYS-001", "DSYS-001", "DOMAIN-TEMPLATE", "GOAL-TEMPLATE", "FT-TEMPLATE", "UC-TEMPLATE"], "special_files": ["foundation/feature-brief.md"], "directories": [], "actions": [] }, "existing-implementation": { - "artifact_ids": ["IMPL-ASSESS-TBD", "PROBLEM-TBD", "VISION-TBD", "PRINCIPLES-TBD", "NORTH-STAR-TBD", "STRATEGY-TBD", "DOMAIN-TEMPLATE", "GOAL-TEMPLATE", "FT-TEMPLATE", "UC-TEMPLATE"], + "artifact_ids": ["IMPL-ASSESS-TBD", "PROBLEM-TBD", "VISION-TBD", "PRINCIPLES-TBD", "NORTH-STAR-TBD", "STRATEGY-TBD", "LANDSCAPE-TBD", "ENGSYS-001", "DSYS-001", "DOMAIN-TEMPLATE", "GOAL-TEMPLATE", "FT-TEMPLATE", "UC-TEMPLATE"], "special_files": ["knowledge/assessments/implementation-assessment.md"], "directories": [], "actions": [] }, "audit-only": { - "artifact_ids": [], + "artifact_ids": ["LANDSCAPE-TBD", "ENGSYS-001", "DSYS-001"], "special_files": [], "directories": [], "actions": [] diff --git a/internal/runtimeassets/runtime.go b/internal/runtimeassets/runtime.go index 18a50b6..1404725 100644 --- a/internal/runtimeassets/runtime.go +++ b/internal/runtimeassets/runtime.go @@ -12,13 +12,17 @@ import ( ) type Manifest struct { - SchemaVersion int `json:"schema_version"` - Framework string `json:"framework"` - Version string `json:"version"` - ProductRoot string `json:"product_root"` - StartingPoint string `json:"starting_point"` - Agents []string `json:"agents"` - Activation struct { + SchemaVersion int `json:"schema_version"` + Framework string `json:"framework"` + Version string `json:"version"` + ProductRoot string `json:"product_root"` + StartingPoint string `json:"starting_point"` + Agents []string `json:"agents"` + CodeRoots []CodeRoot `json:"code_roots,omitempty"` + BaselinePolicy struct { + PreSpecification string `json:"pre_specification,omitempty"` + } `json:"baseline_policy,omitempty"` + Activation struct { Mode string `json:"mode"` } `json:"activation"` Runtime struct { @@ -27,6 +31,13 @@ type Manifest struct { } `json:"runtime"` } +// CodeRoot is an implementation area located alongside product/. It records +// the semantic role rather than coupling the framework to one language. +type CodeRoot struct { + Path string `json:"path"` + Role string `json:"role"` +} + func CacheRoot() (string, error) { if override := strings.TrimSpace(os.Getenv("SPEC_FRAMEWORK_CACHE")); override != "" { return filepath.Abs(override) diff --git a/internal/validator/pre_specification_test.go b/internal/validator/pre_specification_test.go new file mode 100644 index 0000000..847de26 --- /dev/null +++ b/internal/validator/pre_specification_test.go @@ -0,0 +1,41 @@ +package validator + +import ( + "context" + "os" + "path/filepath" + "testing" +) + +func TestPreSpecificationBaselineBlocksRealSpecification(t *testing.T) { + root := t.TempDir() + for _, path := range []string{".product", "knowledge/assessments", "engineering", "design/system", "domains/live"} { + if err := os.MkdirAll(filepath.Join(root, path), 0755); err != nil { + t.Fatal(err) + } + } + write := func(path, data string) { + if err := os.WriteFile(filepath.Join(root, path), []byte(data), 0644); err != nil { + t.Fatal(err) + } + } + write(".product/framework.json", `{"baseline_policy":{"pre_specification":"required"}}`) + write("knowledge/assessments/product-landscape.md", "# Landscape\n") + write("engineering/engineering-system.md", "# Engineering\n") + write("design/system/design-system.md", "# Design\n") + write("domains/live/specification.md", "# Specification\n") + write(".product/artifacts.json", `{"artifacts":[{"id":"SPEC-1","type":"specification","status":"draft","path":"domains/live/specification.md"},{"id":"LANDSCAPE","type":"product-landscape","status":"draft","path":"knowledge/assessments/product-landscape.md"},{"id":"ENG","type":"engineering-system","status":"draft","path":"engineering/engineering-system.md"},{"id":"DS","type":"design-system","status":"draft","path":"design/system/design-system.md"}]}`) + result, err := Validate(context.Background(), root, ".") + if err != nil { + t.Fatal(err) + } + found := 0 + for _, diagnostic := range result.Diagnostics { + if diagnostic.Check == "pre-specification-baseline" { + found++ + } + } + if found != 3 { + t.Fatalf("baseline diagnostics=%d, want 3: %#v", found, result.Diagnostics) + } +} diff --git a/internal/validator/rules.go b/internal/validator/rules.go index 4d48f9b..ee944bd 100644 --- a/internal/validator/rules.go +++ b/internal/validator/rules.go @@ -1403,6 +1403,53 @@ func validateRegistryAndApprovalGates(s Snapshot) []Diagnostic { return out } +// validatePreSpecificationBaselines applies only to products initialized with +// the current explicit policy. Older products remain compatible until they opt +// in, while new products must establish their knowledge, engineering, and +// shared Design System before authoring a real Specification. +func validatePreSpecificationBaselines(s Snapshot) []Diagnostic { + manifest, ok := s.JSON[".product/framework.json"].(map[string]any) + if !ok { + return nil + } + policy, _ := manifest["baseline_policy"].(map[string]any) + if strings.ToLower(firstString(policy["pre_specification"], "")) != "required" { + return nil + } + var hasSpecification bool + for _, item := range artifactList(s) { + kind := strings.ReplaceAll(firstString(item["type"], ""), "_", "-") + path := firstString(item["path"], "") + if (kind == "specification" || kind == "specification-contract") && !strings.Contains(path, "_template-") { + hasSpecification = true + break + } + } + if !hasSpecification { + return nil + } + required := map[string]string{ + "product-landscape": "knowledge/assessments/product-landscape.md", + "engineering-system": "engineering/engineering-system.md", + "design-system": "design/system/design-system.md", + } + items := artifactList(s) + byType := map[string]map[string]any{} + for _, item := range items { + byType[strings.ReplaceAll(firstString(item["type"], ""), "_", "-")] = item + } + var out []Diagnostic + for kind, expectedPath := range required { + item := byType[kind] + status := firstString(item["status"], "missing") + path := firstString(item["path"], expectedPath) + if item == nil || !feeds(status) { + out = append(out, Diagnostic{Error, "pre-specification-baseline", path, kind + " must be approved before a Specification is authored.", "Complete the comprehensive Product Landscape, Engineering System, and Design System baselines, then approve each one."}) + } + } + return out +} + func configuredRequiredArtifacts(s Snapshot) []string { registry, ok := s.JSON[".product/artifacts.json"].(map[string]any) if !ok { diff --git a/internal/validator/validator.go b/internal/validator/validator.go index d688e50..9518195 100644 --- a/internal/validator/validator.go +++ b/internal/validator/validator.go @@ -225,6 +225,7 @@ func validate(ctx context.Context, root, frameworkRoot string, strict bool, cand d = append(d, validateSkillReferences(snap)...) d = append(d, validateDeliveryAndRigor(snap)...) d = append(d, validateRegistryAndApprovalGates(snap)...) + d = append(d, validatePreSpecificationBaselines(snap)...) d = append(d, validateImportRuns(snap)...) d = append(d, validateDeliveryClosure(snap)...) d = append(d, validateSkillDiscoveryContracts(snap)...) @@ -865,6 +866,9 @@ func validateMarkdownLinks(s Snapshot) []Diagnostic { } candidate := filepath.Clean(filepath.Join(s.Root, filepath.Dir(filepath.FromSlash(rel)), filepath.FromSlash(target))) if _, err := os.Stat(candidate); err != nil { + if relocatedHistoricalTemplate(s, target) { + continue + } out = append(out, Diagnostic{Error, "links", rel, "Broken Markdown link: " + target, "Create the target or update the link."}) } else if fragment != "" { targetText, readErr := os.ReadFile(candidate) @@ -877,6 +881,22 @@ func validateMarkdownLinks(s Snapshot) []Diagnostic { return out } +// Historical approved decisions may cite the pre-normalization template tree. +// Keep those citations valid without rewriting human-approved evidence records. +func relocatedHistoricalTemplate(s Snapshot, target string) bool { + legacy := map[string]string{ + "approval-record-template.json": "framework/skills/product-historian/assets/approval-record-template.json", + "derivation-record-template.json": "framework/skills/documentation-writer/assets/derivation-record-template.json", + } + for name, current := range legacy { + if strings.HasSuffix(filepath.ToSlash(target), "/framework/template/"+name) { + _, err := os.Stat(filepath.Join(s.FrameworkRoot, filepath.FromSlash(current))) + return err == nil + } + } + return false +} + var markdownHeading = regexp.MustCompile(`(?m)^#{1,6}\s+(.+?)\s*#*\s*$`) func markdownAnchors(text string) map[string]bool { diff --git a/starter/product/.product/artifacts.json b/starter/product/.product/artifacts.json index 9cd51ef..94e777e 100644 --- a/starter/product/.product/artifacts.json +++ b/starter/product/.product/artifacts.json @@ -35,6 +35,27 @@ "path": "foundation/strategy/strategy.md", "parentIds": ["VISION-TBD", "PRINCIPLES-TBD", "NORTH-STAR-TBD"] }, + { + "id": "LANDSCAPE-TBD", + "type": "product-landscape", + "status": "draft", + "path": "knowledge/assessments/product-landscape.md", + "parentIds": [] + }, + { + "id": "ENGSYS-001", + "type": "engineering-system", + "status": "draft", + "path": "engineering/engineering-system.md", + "parentIds": ["LANDSCAPE-TBD"] + }, + { + "id": "DSYS-001", + "type": "design-system", + "status": "draft", + "path": "design/system/design-system.md", + "parentIds": ["LANDSCAPE-TBD"] + }, { "id": "DOMAIN-TEMPLATE", "type": "domain", diff --git a/starter/product/design/system/design-system.md b/starter/product/design/system/design-system.md index fc29984..1d800ce 100644 --- a/starter/product/design/system/design-system.md +++ b/starter/product/design/system/design-system.md @@ -9,10 +9,11 @@ | Version | `0.1.0` | | Origin | `generate` | | Owner skill | Design System | +| Baseline role | `pre-specification` | ## Purpose And Principles -Draft shared foundations for use-case Design. Product-specific visual decisions remain open. +Draft shared foundations for use-case Design. Product-specific visual decisions remain open. When no interface exists, document explicit hypotheses and accessibility principles before any Specification is created. ## Foundations, Components And Patterns diff --git a/starter/product/engineering/engineering-system.md b/starter/product/engineering/engineering-system.md index 683ebe5..7a33e9b 100644 --- a/starter/product/engineering/engineering-system.md +++ b/starter/product/engineering/engineering-system.md @@ -8,12 +8,13 @@ | Status | `draft` | | Version | `0.1.0` | | Origin mode | `generate` | +| Baseline role | `pre-specification` | | Mechanical catalog | [engineering-system.yaml](engineering-system.yaml) | | Owner skill | `engineering-system` | ## Scope -Not configured. Inspect the adopter's real repository before defining system boundaries. +Draft contract. Inspect the adopter's real repository before defining system boundaries. When no code exists, record explicit hypotheses, intended runtime boundaries, quality constraints, and decisions pending human approval before any Specification is created. ## Architecture diff --git a/starter/product/knowledge/README.md b/starter/product/knowledge/README.md index 79e4600..76ff0bb 100644 --- a/starter/product/knowledge/README.md +++ b/starter/product/knowledge/README.md @@ -7,6 +7,7 @@ Store durable knowledge owned by this product, including business rules, convent - Product and cross-cutting decisions belong in `decisions/`; design decisions belong in `../design/decisions/`; engineering decisions belong in `../engineering/decisions/`. Every record is indexed in `.product/decisions.json` with its `domain` and `path`. - Product gate commands and security conventions belong in `conventions/`. - Existing source material and reviewed mappings belong in `imports/`. +- `assessments/product-landscape.md` is the complete code and product-boundary inventory. Keep it explicit about observed evidence versus hypotheses; finish it before delivery-domain modeling. - Create other knowledge directories only when a real artifact needs them. Framework maintenance history remains in the framework repository's Git history. diff --git a/starter/product/knowledge/assessments/product-landscape.md b/starter/product/knowledge/assessments/product-landscape.md new file mode 100644 index 0000000..4de28e4 --- /dev/null +++ b/starter/product/knowledge/assessments/product-landscape.md @@ -0,0 +1,21 @@ +# Product Landscape + +## Snapshot + +| Field | Value | +| --- | --- | +| Status | `draft` | +| Evidence mode | `hypothesis` | +| Owner skill | `product-orchestrator` | + +## Code Roots + +No implementation root has been declared or detected. + +## Required Inventory + +Before domain modeling, inspect every declared code root for modules, routes or UI surfaces, data models, integrations, business rules, tests, configuration, design assets, and operational constraints. Record evidence paths and unresolved ownership boundaries; do not reduce a broad codebase to a single feature slice. + +## Domain Map + +Pending comprehensive inventory. List all candidate domains, ownership boundaries, cross-domain workflows, and uncovered areas before creating a delivery Domain, Goal, Feature, or Use Case. From ec0eb69e85f3bd60a725b0040d9d230a74167e5f Mon Sep 17 00:00:00 2001 From: JonatasFreireDev Date: Sat, 18 Jul 2026 04:20:06 -0300 Subject: [PATCH 3/9] feat(skills): add code baseline inventory scripts --- framework/skills/design-system/SKILL.md | 2 ++ .../scripts/inventory-design-evidence.ps1 | 15 +++++++++ .../scripts/inventory-design-evidence.sh | 15 +++++++++ framework/skills/engineering-system/SKILL.md | 2 ++ .../inventory-engineering-evidence.ps1 | 16 +++++++++ .../scripts/inventory-engineering-evidence.sh | 16 +++++++++ framework/skills/framework-guide/SKILL.md | 2 ++ .../scripts/inspect-code-roots.ps1 | 28 ++++++++++++++++ .../scripts/inspect-code-roots.sh | 33 +++++++++++++++++++ .../skills/product-orchestrator/SKILL.md | 2 ++ .../scripts/inventory-product-landscape.ps1 | 9 +++++ .../scripts/inventory-product-landscape.sh | 9 +++++ internal/runtimeassets/runtime_test.go | 2 +- 13 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 framework/skills/design-system/scripts/inventory-design-evidence.ps1 create mode 100644 framework/skills/design-system/scripts/inventory-design-evidence.sh create mode 100644 framework/skills/engineering-system/scripts/inventory-engineering-evidence.ps1 create mode 100644 framework/skills/engineering-system/scripts/inventory-engineering-evidence.sh create mode 100644 framework/skills/framework-guide/scripts/inspect-code-roots.ps1 create mode 100644 framework/skills/framework-guide/scripts/inspect-code-roots.sh create mode 100644 framework/skills/product-orchestrator/scripts/inventory-product-landscape.ps1 create mode 100644 framework/skills/product-orchestrator/scripts/inventory-product-landscape.sh diff --git a/framework/skills/design-system/SKILL.md b/framework/skills/design-system/SKILL.md index 5a04427..9469c67 100644 --- a/framework/skills/design-system/SKILL.md +++ b/framework/skills/design-system/SKILL.md @@ -36,6 +36,8 @@ Follow the shared [Discovery And Challenge contract](../discovery-and-challenge. Use scripts/invoke-cli.ps1 on Windows or scripts/invoke-cli.sh on macOS/Linux for the CLI operation in this skill's reviewed scope. The wrapper never adds --yes or an approver identity. +Use `scripts/inventory-design-evidence.ps1` on Windows or `scripts/inventory-design-evidence.sh` on macOS/Linux before authoring the shared baseline. It inventories interface and design-system evidence and can call `design-system validate`; it never changes product or code files. + ## Workflow 1. Confirm whether origin is `generate`, `evolve`, or `adopt`; never promote a reference to canonical authority implicitly. When there is no interface source, establish explicit accessibility and experience hypotheses before Specifications rather than omitting the shared baseline. 2. Inventory foundations, sources, tokens, themes, components, patterns, consumers, and implementation links. diff --git a/framework/skills/design-system/scripts/inventory-design-evidence.ps1 b/framework/skills/design-system/scripts/inventory-design-evidence.ps1 new file mode 100644 index 0000000..60a4c6b --- /dev/null +++ b/framework/skills/design-system/scripts/inventory-design-evidence.ps1 @@ -0,0 +1,15 @@ +[CmdletBinding()] +param([string]$RepositoryRoot = ".", [string]$ProductRoot = "product", [switch]$Validate) + +$ErrorActionPreference = "Stop" +$repo = (Resolve-Path $RepositoryRoot).Path +$manifest = Get-Content -Raw (Join-Path $repo (Join-Path $ProductRoot ".product/framework.json")) | ConvertFrom-Json +Write-Output "# Design evidence inventory" +foreach ($root in $manifest.code_roots) { + $path = Join-Path $repo $root.path + if (-not (Test-Path $path)) { continue } + Get-ChildItem -LiteralPath $path -Force -File -Recurse -ErrorAction SilentlyContinue | + Where-Object { $_.Name -in @("package.json", "tailwind.config.js", "tailwind.config.ts") -or $_.Extension -in @(".css", ".scss", ".sass", ".svg", ".fig") } | + ForEach-Object { Write-Output ("evidence: " + $_.FullName.Substring($repo.Length + 1).Replace("\", "/")) } +} +if ($Validate) { & spec-framework design-system validate --product-root (Join-Path $repo $ProductRoot); exit $LASTEXITCODE } diff --git a/framework/skills/design-system/scripts/inventory-design-evidence.sh b/framework/skills/design-system/scripts/inventory-design-evidence.sh new file mode 100644 index 0000000..7113a4f --- /dev/null +++ b/framework/skills/design-system/scripts/inventory-design-evidence.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -euo pipefail +repository_root="."; product_root="product"; validate=false +while [[ $# -gt 0 ]]; do case "$1" in --repository-root) repository_root="$2"; shift 2;; --product-root) product_root="$2"; shift 2;; --validate) validate=true; shift;; *) echo "Usage: $0 [--repository-root ] [--product-root ] [--validate]" >&2; exit 2;; esac; done +repo="$(cd "$repository_root" && pwd)"; manifest="$repo/$product_root/.product/framework.json" +[[ -f "$manifest" ]] || { echo "Spec Framework manifest not found: $manifest" >&2; exit 1; } +echo "# Design evidence inventory" +roots="$(awk -F '"' '/^[[:space:]]*"path"[[:space:]]*:/ { path=$4 } /^[[:space:]]*"role"[[:space:]]*:/ { if (path != "") { print path "\t" $4; path="" } }' "$manifest")" +while IFS=$'\t' read -r path role; do + [[ -n "$path" ]] || continue; root="$repo/$path"; [[ -d "$root" ]] || continue + while IFS= read -r file; do echo "evidence: ${file#"$repo/"}"; done < <(find "$root" -type f \( -name package.json -o -name tailwind.config.js -o -name tailwind.config.ts -o -name '*.css' -o -name '*.scss' -o -name '*.sass' -o -name '*.svg' -o -name '*.fig' \) -print) +done <<< "$roots" +if $validate; then + spec-framework design-system validate --product-root "$repo/$product_root" +fi diff --git a/framework/skills/engineering-system/SKILL.md b/framework/skills/engineering-system/SKILL.md index 9f4dbd9..a28a684 100644 --- a/framework/skills/engineering-system/SKILL.md +++ b/framework/skills/engineering-system/SKILL.md @@ -38,6 +38,8 @@ Follow the shared [Discovery And Challenge contract](../discovery-and-challenge. Use scripts/invoke-cli.ps1 on Windows or scripts/invoke-cli.sh on macOS/Linux for the CLI operation in this skill's reviewed scope. The wrapper never adds --yes or an approver identity. +Use `scripts/inventory-engineering-evidence.ps1` on Windows or `scripts/inventory-engineering-evidence.sh` on macOS/Linux before authoring the shared baseline. It inventories deterministic engineering evidence and can call `engineering-system validate`; it never changes product or code files. + ## Workflow 1. Inspect the real product code, tests, configuration, environments, operations evidence, and existing engineering documents. If no code exists, create explicit hypothesis contracts, pending decisions, and intended constraints rather than pretending evidence exists. 2. Define the covered product and repository boundaries and choose `generate`, `evolve`, or `adopt`. diff --git a/framework/skills/engineering-system/scripts/inventory-engineering-evidence.ps1 b/framework/skills/engineering-system/scripts/inventory-engineering-evidence.ps1 new file mode 100644 index 0000000..aa2ec83 --- /dev/null +++ b/framework/skills/engineering-system/scripts/inventory-engineering-evidence.ps1 @@ -0,0 +1,16 @@ +[CmdletBinding()] +param([string]$RepositoryRoot = ".", [string]$ProductRoot = "product", [switch]$Validate) + +$ErrorActionPreference = "Stop" +$repo = (Resolve-Path $RepositoryRoot).Path +$manifest = Get-Content -Raw (Join-Path $repo (Join-Path $ProductRoot ".product/framework.json")) | ConvertFrom-Json +Write-Output "# Engineering evidence inventory" +foreach ($root in $manifest.code_roots) { + $path = Join-Path $repo $root.path + Write-Output "`n## $($root.path) ($($root.role))" + if (-not (Test-Path $path)) { Write-Output "MISSING"; continue } + Get-ChildItem -LiteralPath $path -Force -File -Recurse -ErrorAction SilentlyContinue | + Where-Object { $_.Name -match '^(Dockerfile|docker-compose.*|Makefile|README\.md|.*\.ya?ml)$' -or $_.FullName -match '(test|spec|\.github|\.gitlab)' } | + ForEach-Object { Write-Output ("evidence: " + $_.FullName.Substring($repo.Length + 1).Replace("\", "/")) } +} +if ($Validate) { & spec-framework engineering-system validate --product-root (Join-Path $repo $ProductRoot); exit $LASTEXITCODE } diff --git a/framework/skills/engineering-system/scripts/inventory-engineering-evidence.sh b/framework/skills/engineering-system/scripts/inventory-engineering-evidence.sh new file mode 100644 index 0000000..099705e --- /dev/null +++ b/framework/skills/engineering-system/scripts/inventory-engineering-evidence.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -euo pipefail +repository_root="."; product_root="product"; validate=false +while [[ $# -gt 0 ]]; do case "$1" in --repository-root) repository_root="$2"; shift 2;; --product-root) product_root="$2"; shift 2;; --validate) validate=true; shift;; *) echo "Usage: $0 [--repository-root ] [--product-root ] [--validate]" >&2; exit 2;; esac; done +repo="$(cd "$repository_root" && pwd)"; manifest="$repo/$product_root/.product/framework.json" +[[ -f "$manifest" ]] || { echo "Spec Framework manifest not found: $manifest" >&2; exit 1; } +echo "# Engineering evidence inventory" +roots="$(awk -F '"' '/^[[:space:]]*"path"[[:space:]]*:/ { path=$4 } /^[[:space:]]*"role"[[:space:]]*:/ { if (path != "") { print path "\t" $4; path="" } }' "$manifest")" +while IFS=$'\t' read -r path role; do + [[ -n "$path" ]] || continue; root="$repo/$path"; echo; echo "## $path ($role)" + [[ -d "$root" ]] || { echo "MISSING"; continue; } + while IFS= read -r file; do echo "evidence: ${file#"$repo/"}"; done < <(find "$root" -type f \( -name Dockerfile -o -name 'docker-compose*' -o -name Makefile -o -name README.md -o -name '*.yml' -o -name '*.yaml' -o -iname '*test*' -o -iname '*spec*' \) -print) +done <<< "$roots" +if $validate; then + spec-framework engineering-system validate --product-root "$repo/$product_root" +fi diff --git a/framework/skills/framework-guide/SKILL.md b/framework/skills/framework-guide/SKILL.md index 4dd1e6f..ef510c5 100644 --- a/framework/skills/framework-guide/SKILL.md +++ b/framework/skills/framework-guide/SKILL.md @@ -91,6 +91,8 @@ Follow the shared [Discovery And Challenge contract](../discovery-and-challenge. For a deterministic workspace snapshot, run `scripts/inspect-workspace.ps1` on Windows or `scripts/inspect-workspace.sh` on macOS/Linux instead of reconstructing the `guide`, `status`, and `dashboard` sequence. +For a declared implementation layout, run `scripts/inspect-code-roots.ps1` on Windows or `scripts/inspect-code-roots.sh` on macOS/Linux. The script inventories deterministic code-root evidence and optionally calls `spec-framework validate`; interpret its output before routing domain work. + When the intent concerns a decision or ADR, first run `spec-framework decisions check --product-root product --json`. Use the indexed `path` and reported domain as the routing evidence. `--strict` is the CI gate; `--fix-links` requires a reviewed preview followed by `--yes` and never creates approval records. 1. Restate the goal in one sentence and determine whether the request is explanation, inspection, planning, local mutation, approval, remote mutation, or release. 2. Apply the activation and dispatch boundaries. If active, discover the repository root, product root, pinned version, agents, and starting point from the canonical manifest; do not ask for information the CLI or repository can provide safely. diff --git a/framework/skills/framework-guide/scripts/inspect-code-roots.ps1 b/framework/skills/framework-guide/scripts/inspect-code-roots.ps1 new file mode 100644 index 0000000..1cb0402 --- /dev/null +++ b/framework/skills/framework-guide/scripts/inspect-code-roots.ps1 @@ -0,0 +1,28 @@ +[CmdletBinding()] +param( + [string]$RepositoryRoot = ".", + [string]$ProductRoot = "product", + [switch]$Validate +) + +$ErrorActionPreference = "Stop" +$repo = (Resolve-Path $RepositoryRoot).Path +$manifestPath = Join-Path $repo (Join-Path $ProductRoot ".product/framework.json") +if (-not (Test-Path $manifestPath)) { throw "Spec Framework manifest not found: $manifestPath" } +$manifest = Get-Content -Raw $manifestPath | ConvertFrom-Json + +Write-Output "# Code-root inventory" +Write-Output "Repository: $repo" +if (-not $manifest.code_roots -or $manifest.code_roots.Count -eq 0) { + Write-Output "No declared code roots. Confirm the intended stack and official scaffold command before creating implementation." +} else { + foreach ($root in $manifest.code_roots) { + $path = Join-Path $repo $root.path + Write-Output "`n## $($root.path) ($($root.role))" + if (-not (Test-Path $path)) { Write-Output "MISSING: declared root does not exist"; continue } + Get-ChildItem -LiteralPath $path -Force -File -Recurse -ErrorAction SilentlyContinue | + Where-Object { $_.Name -in @("package.json", "go.mod", "Cargo.toml", "pom.xml", "build.gradle", "build.gradle.kts", "Dockerfile") -or $_.Extension -in @(".csproj", ".sln") } | + ForEach-Object { Write-Output ("evidence: " + $_.FullName.Substring($repo.Length + 1).Replace("\", "/")) } + } +} +if ($Validate) { & spec-framework validate --product-root (Join-Path $repo $ProductRoot); exit $LASTEXITCODE } diff --git a/framework/skills/framework-guide/scripts/inspect-code-roots.sh b/framework/skills/framework-guide/scripts/inspect-code-roots.sh new file mode 100644 index 0000000..c2c9482 --- /dev/null +++ b/framework/skills/framework-guide/scripts/inspect-code-roots.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +set -euo pipefail + +repository_root="." +product_root="product" +validate=false +while [[ $# -gt 0 ]]; do + case "$1" in + --repository-root) repository_root="$2"; shift 2 ;; + --product-root) product_root="$2"; shift 2 ;; + --validate) validate=true; shift ;; + *) echo "Usage: $0 [--repository-root ] [--product-root ] [--validate]" >&2; exit 2 ;; + esac +done +repo="$(cd "$repository_root" && pwd)" +manifest="$repo/$product_root/.product/framework.json" +[[ -f "$manifest" ]] || { echo "Spec Framework manifest not found: $manifest" >&2; exit 1; } + +echo "# Code-root inventory" +echo "Repository: $repo" +roots="$(awk -F '"' '/^[[:space:]]*"path"[[:space:]]*:/ { path=$4 } /^[[:space:]]*"role"[[:space:]]*:/ { if (path != "") { print path "\t" $4; path="" } }' "$manifest")" +if [[ -z "$roots" ]]; then + echo "No declared code roots. Confirm the intended stack and official scaffold command before creating implementation." +else + while IFS=$'\t' read -r path role; do + root="$repo/$path" + echo + echo "## $path ($role)" + [[ -d "$root" ]] || { echo "MISSING: declared root does not exist"; continue; } + while IFS= read -r file; do echo "evidence: ${file#"$repo/"}"; done < <(find "$root" -type f \( -name package.json -o -name go.mod -o -name Cargo.toml -o -name pom.xml -o -name build.gradle -o -name build.gradle.kts -o -name Dockerfile -o -name '*.csproj' -o -name '*.sln' \) -print) + done <<< "$roots" +fi +if $validate; then spec-framework validate --product-root "$repo/$product_root"; fi diff --git a/framework/skills/product-orchestrator/SKILL.md b/framework/skills/product-orchestrator/SKILL.md index 4bf5acc..0c6063b 100644 --- a/framework/skills/product-orchestrator/SKILL.md +++ b/framework/skills/product-orchestrator/SKILL.md @@ -31,6 +31,8 @@ For `starting_point: existing-product`, use Product Baseline -> explicit individ Follow the shared [Discovery And Challenge contract](../discovery-and-challenge.md) when eliciting foundation choices or resolving a blocking route. +Use `scripts/inventory-product-landscape.ps1` on Windows or `scripts/inventory-product-landscape.sh` on macOS/Linux to collect the declared code-root evidence and invoke validation when requested. The script is read-only; the agent must still map the full product landscape and distinguish evidence from inference. + ## Operating rules 1. Identify the current artifact status before routing work. 2. Route work to the smallest specialist skill that owns the next artifact. diff --git a/framework/skills/product-orchestrator/scripts/inventory-product-landscape.ps1 b/framework/skills/product-orchestrator/scripts/inventory-product-landscape.ps1 new file mode 100644 index 0000000..260ca2e --- /dev/null +++ b/framework/skills/product-orchestrator/scripts/inventory-product-landscape.ps1 @@ -0,0 +1,9 @@ +[CmdletBinding()] +param([string]$RepositoryRoot = ".", [string]$ProductRoot = "product", [switch]$Validate) + +$ErrorActionPreference = "Stop" +$script = Join-Path $PSScriptRoot "../../framework-guide/scripts/inspect-code-roots.ps1" +& $script -RepositoryRoot $RepositoryRoot -ProductRoot $ProductRoot +Write-Output "`n## Required coverage" +Write-Output "Map every discovered module, user surface, data boundary, integration, business rule, test, configuration, design asset, and operational constraint into product/knowledge/assessments/product-landscape.md." +if ($Validate) { & spec-framework validate --product-root (Join-Path (Resolve-Path $RepositoryRoot) $ProductRoot); exit $LASTEXITCODE } diff --git a/framework/skills/product-orchestrator/scripts/inventory-product-landscape.sh b/framework/skills/product-orchestrator/scripts/inventory-product-landscape.sh new file mode 100644 index 0000000..33df646 --- /dev/null +++ b/framework/skills/product-orchestrator/scripts/inventory-product-landscape.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -euo pipefail +repository_root="." +product_root="product" +validate="" +while [[ $# -gt 0 ]]; do case "$1" in --repository-root) repository_root="$2"; shift 2;; --product-root) product_root="$2"; shift 2;; --validate) validate="--validate"; shift;; *) echo "Usage: $0 [--repository-root ] [--product-root ] [--validate]" >&2; exit 2;; esac; done +"$(dirname "$0")/../../framework-guide/scripts/inspect-code-roots.sh" --repository-root "$repository_root" --product-root "$product_root" +printf '\n## Required coverage\nMap every discovered module, user surface, data boundary, integration, business rule, test, configuration, design asset, and operational constraint into product/knowledge/assessments/product-landscape.md.\n' +[[ -z "$validate" ]] || spec-framework validate --product-root "$repository_root/$product_root" diff --git a/internal/runtimeassets/runtime_test.go b/internal/runtimeassets/runtime_test.go index 506950a..566d3c2 100644 --- a/internal/runtimeassets/runtime_test.go +++ b/internal/runtimeassets/runtime_test.go @@ -38,7 +38,7 @@ func TestEnsureMaterializesVersionedAssets(t *testing.T) { if err != nil { t.Fatal(err) } - for _, name := range []string{"FRAMEWORK.md", "AGENTS.framework.md", "docs/execution-runtime.md", "docs/engineering-systems.md", "docs/lifecycle-and-approvals.md", "docs/artifact-registry-modules.md", "init/schema.json", "init/catalog.json", "init/bootstrap.json", "init/contracts/new-product.json", "skills/code-runner/SKILL.md", "skills/framework-guide/agents/openai.yaml", "skills/framework-guide/scripts/inspect-workspace.ps1", "skills/framework-guide/scripts/inspect-workspace.sh", "skills/artifact-importer/scripts/record-review-and-validate.ps1", "skills/documentation-writer/scripts/validate-artifacts.sh", "skills/discovery-and-challenge.md", "skills/problem-discovery/assets/interview-note-template.md", "skills/problem-discovery/assets/research-summary-template.md", "skills/specification/assets/specification-template.md", "examples/events/domains/events/domain.md", ".complete"} { + for _, name := range []string{"FRAMEWORK.md", "AGENTS.framework.md", "docs/execution-runtime.md", "docs/engineering-systems.md", "docs/lifecycle-and-approvals.md", "docs/artifact-registry-modules.md", "init/schema.json", "init/catalog.json", "init/bootstrap.json", "init/contracts/new-product.json", "skills/code-runner/SKILL.md", "skills/framework-guide/agents/openai.yaml", "skills/framework-guide/scripts/inspect-workspace.ps1", "skills/framework-guide/scripts/inspect-workspace.sh", "skills/framework-guide/scripts/inspect-code-roots.ps1", "skills/product-orchestrator/scripts/inventory-product-landscape.sh", "skills/engineering-system/scripts/inventory-engineering-evidence.ps1", "skills/design-system/scripts/inventory-design-evidence.sh", "skills/artifact-importer/scripts/record-review-and-validate.ps1", "skills/documentation-writer/scripts/validate-artifacts.sh", "skills/discovery-and-challenge.md", "skills/problem-discovery/assets/interview-note-template.md", "skills/problem-discovery/assets/research-summary-template.md", "skills/specification/assets/specification-template.md", "examples/events/domains/events/domain.md", ".complete"} { if _, err := os.Stat(filepath.Join(root, filepath.FromSlash(name))); err != nil { t.Fatal(err) } From ed2c82c9f3c50dfd4c3f9f2379009a7913aee836 Mon Sep 17 00:00:00 2001 From: JonatasFreireDev Date: Sat, 18 Jul 2026 04:31:54 -0300 Subject: [PATCH 4/9] feat(skills): add runtime grill-me guidance --- framework/skills/README.md | 2 +- framework/skills/grill-me/SKILL.md | 52 ++++++++++++++++++++ framework/skills/grill-me/agents/openai.yaml | 4 ++ internal/runtimeassets/runtime_test.go | 2 +- 4 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 framework/skills/grill-me/SKILL.md create mode 100644 framework/skills/grill-me/agents/openai.yaml diff --git a/framework/skills/README.md b/framework/skills/README.md index 0bd62c6..dff7759 100644 --- a/framework/skills/README.md +++ b/framework/skills/README.md @@ -33,7 +33,7 @@ Use a skill-owned `scripts/` resource only for a deterministic, repeatable seque - `/agents/openai.yaml`: optional UI metadata for discovery surfaces. - Specialist skills: problem, vision, strategy, domain, goal, journey, feature, use case, Design System, UX/UI, specification, Engineering System, technical discovery, engineering proposal, engineering review, implementation planning, graph, task, code runner, bug fixer, QA, code review, security review, threat modeler, commit crafter, PR finalizer, audit, documentation, history, artifact import, and subagent return review. - Orchestrator skills: product, domain evolution, existing product import, new feature, audit, evolution, documentation, release, delivery, execution scheduling, integration, and dispatch. -- Guidance skill: Framework Guide translates human goals into current CLI state, the smallest safe command, and the correct specialist or approval handoff without authoring artifacts. +- Guidance skills: Framework Guide translates human goals into current CLI state, the smallest safe command, and the correct specialist or approval handoff without authoring artifacts. `grill-me` stress-tests a plan or proposal one decision at a time before it reaches its owning skill. Runtime v2 also includes the `command-planner` and `command-executor` operational skills. The planner owns immutable argv-based plans; the executor is restricted to local R0/R1 plans. diff --git a/framework/skills/grill-me/SKILL.md b/framework/skills/grill-me/SKILL.md new file mode 100644 index 0000000..4efb627 --- /dev/null +++ b/framework/skills/grill-me/SKILL.md @@ -0,0 +1,52 @@ +--- +name: grill-me +description: "Interview a person rigorously about a product plan, design, scope, architecture choice, or delivery proposal until important decision branches are resolved. Use when they ask to be grilled, stress-test an idea, expose assumptions, or challenge a plan before it becomes a governed framework artifact." +--- + +# Grill Me + +## Role + +Challenge a proposed direction through one focused question at a time. Discover repository evidence before asking, recommend an answer with trade-offs, and retain unresolved decisions as explicit blockers. This is a guidance skill: it does not author canonical artifacts, grant approvals, run mutations, or replace the owner skill selected by the runtime. + +## Runtime reading + +- Read the framework root's `FRAMEWORK.md`. +- When an active product exists, validate `product/.product/framework.json` and inspect `BOOTSTRAP.md`, relevant `context.md` files, approved decisions, and current `guide`, `status`, or `dashboard` output before the first question. +- Read the smallest relevant plan, Design, Specification, Engineering Proposal, or other named artifact. +- For definition or planning work, follow [Discovery And Challenge](../discovery-and-challenge.md). + +## Interview loop + +1. Restate the proposal, target outcome, and decision boundary. Separate stated facts, repository evidence, assumptions, and unknowns. +2. Explore the repository or runtime first whenever a question can be answered mechanically. Do not ask the person for discoverable state. +3. Select the highest-impact unresolved branch: user value, scope, ownership, dependency, data, security, accessibility, operability, reversibility, validation, approval, or delivery. +4. Ask exactly one focused question. Explain why it matters, give two or three concrete options when a choice is meaningful, and recommend one with its trade-off. +5. Incorporate the answer, expose dependent questions, and repeat. Do not move to a lower-impact branch while a higher-impact blocker remains unresolved. +6. Challenge contradictions with evidence and state the likely consequence. Offer a safer alternative rather than silently accepting a risky premise. +7. Stop when the decision tree is sufficiently resolved for the next owner, or immediately when an unanswered blocker prevents a safe recommendation. + +## Guardrails + +- Never invent repository facts, product decisions, approvals, evidence, or CLI results. +- Do not ask multiple questions in one turn unless the harness structured-question capability is used for a tightly coupled set of at most three choices. +- Do not finalize, approve, or edit a governed artifact. Route the resolved brief to its owning skill and preserve formal approval gates. +- When no active framework manifest exists, stay in general planning mode; do not claim framework status or instruct mutation. +- Treat a human answer as decision input, not as an approval record. + +## Handoff + +Return a concise decision brief: + +```text +Proposal: +Resolved decisions: +Evidence used: +Recommended direction: +Rejected alternatives: +Assumptions retained: +Open blockers: +Next owner and artifact: +``` + +If a blocker remains, name the human decision needed. Otherwise route to `framework-guide` for current-state validation and then to the owning specialist. diff --git a/framework/skills/grill-me/agents/openai.yaml b/framework/skills/grill-me/agents/openai.yaml new file mode 100644 index 0000000..13ebd91 --- /dev/null +++ b/framework/skills/grill-me/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Grill Me" + short_description: "Stress-test product plans and decisions" + default_prompt: "Use $grill-me to challenge this plan one decision at a time." diff --git a/internal/runtimeassets/runtime_test.go b/internal/runtimeassets/runtime_test.go index 566d3c2..8bbb58d 100644 --- a/internal/runtimeassets/runtime_test.go +++ b/internal/runtimeassets/runtime_test.go @@ -38,7 +38,7 @@ func TestEnsureMaterializesVersionedAssets(t *testing.T) { if err != nil { t.Fatal(err) } - for _, name := range []string{"FRAMEWORK.md", "AGENTS.framework.md", "docs/execution-runtime.md", "docs/engineering-systems.md", "docs/lifecycle-and-approvals.md", "docs/artifact-registry-modules.md", "init/schema.json", "init/catalog.json", "init/bootstrap.json", "init/contracts/new-product.json", "skills/code-runner/SKILL.md", "skills/framework-guide/agents/openai.yaml", "skills/framework-guide/scripts/inspect-workspace.ps1", "skills/framework-guide/scripts/inspect-workspace.sh", "skills/framework-guide/scripts/inspect-code-roots.ps1", "skills/product-orchestrator/scripts/inventory-product-landscape.sh", "skills/engineering-system/scripts/inventory-engineering-evidence.ps1", "skills/design-system/scripts/inventory-design-evidence.sh", "skills/artifact-importer/scripts/record-review-and-validate.ps1", "skills/documentation-writer/scripts/validate-artifacts.sh", "skills/discovery-and-challenge.md", "skills/problem-discovery/assets/interview-note-template.md", "skills/problem-discovery/assets/research-summary-template.md", "skills/specification/assets/specification-template.md", "examples/events/domains/events/domain.md", ".complete"} { + for _, name := range []string{"FRAMEWORK.md", "AGENTS.framework.md", "docs/execution-runtime.md", "docs/engineering-systems.md", "docs/lifecycle-and-approvals.md", "docs/artifact-registry-modules.md", "init/schema.json", "init/catalog.json", "init/bootstrap.json", "init/contracts/new-product.json", "skills/code-runner/SKILL.md", "skills/grill-me/SKILL.md", "skills/grill-me/agents/openai.yaml", "skills/framework-guide/agents/openai.yaml", "skills/framework-guide/scripts/inspect-workspace.ps1", "skills/framework-guide/scripts/inspect-workspace.sh", "skills/framework-guide/scripts/inspect-code-roots.ps1", "skills/product-orchestrator/scripts/inventory-product-landscape.sh", "skills/engineering-system/scripts/inventory-engineering-evidence.ps1", "skills/design-system/scripts/inventory-design-evidence.sh", "skills/artifact-importer/scripts/record-review-and-validate.ps1", "skills/documentation-writer/scripts/validate-artifacts.sh", "skills/discovery-and-challenge.md", "skills/problem-discovery/assets/interview-note-template.md", "skills/problem-discovery/assets/research-summary-template.md", "skills/specification/assets/specification-template.md", "examples/events/domains/events/domain.md", ".complete"} { if _, err := os.Stat(filepath.Join(root, filepath.FromSlash(name))); err != nil { t.Fatal(err) } From 6b2371feb108b37d407c5521fe555e42b2170c99 Mon Sep 17 00:00:00 2001 From: JonatasFreireDev Date: Sat, 18 Jul 2026 09:53:19 -0300 Subject: [PATCH 5/9] fix(server): expose product landscape type --- internal/projectserver/api.go | 2 +- internal/projectserver/server_test.go | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/internal/projectserver/api.go b/internal/projectserver/api.go index 7c0a471..f23af50 100644 --- a/internal/projectserver/api.go +++ b/internal/projectserver/api.go @@ -387,7 +387,7 @@ func readGitState(root string) gitState { return out } func allTypeConfigs() []artifactTypeConfig { - keys := []string{"problem", "vision", "product-principles", "north-star", "strategy", "feature-brief", "product-baseline", "implementation-assessment", "domain", "user-goal", "feature", "use-case", "design", "design-system", "engineering-system", "engineering-proposal", "engineering-review", "tests", "qa-evidence", "security-review", "audit", "execution-graph", "taskset", "task", "decision", "release", "security-baseline"} + keys := []string{"problem", "vision", "product-principles", "north-star", "strategy", "feature-brief", "product-baseline", "implementation-assessment", "product-landscape", "domain", "user-goal", "feature", "use-case", "design", "design-system", "engineering-system", "engineering-proposal", "engineering-review", "tests", "qa-evidence", "security-review", "audit", "execution-graph", "taskset", "task", "decision", "release", "security-baseline"} out := make([]artifactTypeConfig, 0, len(keys)) for _, key := range keys { out = append(out, typeConfig(key)) diff --git a/internal/projectserver/server_test.go b/internal/projectserver/server_test.go index 2cf3bce..b86431e 100644 --- a/internal/projectserver/server_test.go +++ b/internal/projectserver/server_test.go @@ -141,6 +141,7 @@ func TestProjectViewIncludesTypeRelationsWorktreeAndChanges(t *testing.T) { registry := workflow.Registry{Artifacts: []workflow.Artifact{ {ID: "FEATURE-1", Type: "feature", Status: "approved", Path: "domains/payments/feature.md"}, {ID: "GRAPH-1", Type: "execution-graph", Status: "draft", Path: "domains/payments/execution-graph.json", ParentIDs: []string{"FEATURE-1"}}, + {ID: "LANDSCAPE-1", Type: "product-landscape", Status: "approved", Path: "knowledge/assessments/product-landscape.md"}, }} data, _ := json.Marshal(registry) if err := os.WriteFile(filepath.Join(root, ".product", "artifacts.json"), data, 0o644); err != nil { @@ -149,12 +150,18 @@ func TestProjectViewIncludesTypeRelationsWorktreeAndChanges(t *testing.T) { if err := os.MkdirAll(filepath.Join(root, "domains", "payments"), 0o755); err != nil { t.Fatal(err) } + if err := os.MkdirAll(filepath.Join(root, "knowledge", "assessments"), 0o755); err != nil { + t.Fatal(err) + } if err := os.WriteFile(filepath.Join(root, "domains", "payments", "feature.md"), []byte("# Pagamentos\n"), 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(root, "domains", "payments", "execution-graph.json"), []byte(`{"nodes":[]}`), 0o644); err != nil { t.Fatal(err) } + if err := os.WriteFile(filepath.Join(root, "knowledge", "assessments", "product-landscape.md"), []byte("# Product Landscape\n"), 0o644); err != nil { + t.Fatal(err) + } if err := os.WriteFile(filepath.Join(root, "notes.md"), []byte("não registrado"), 0o644); err != nil { t.Fatal(err) } @@ -181,9 +188,19 @@ func TestProjectViewIncludesTypeRelationsWorktreeAndChanges(t *testing.T) { for _, artifact := range view.Artifacts { byID[artifact.ID] = artifact } - if len(view.Artifacts) != 2 || byID["GRAPH-1"].View.Renderer != "graph" { + if len(view.Artifacts) != 3 || byID["GRAPH-1"].View.Renderer != "graph" || byID["LANDSCAPE-1"].View.Renderer != "markdown" { t.Fatalf("artifacts=%+v", view.Artifacts) } + foundLandscapeType := false + for _, config := range view.Types { + if config.Type == "product-landscape" && config.Renderer == "markdown" { + foundLandscapeType = true + break + } + } + if !foundLandscapeType { + t.Fatalf("type catalog does not include product-landscape: %+v", view.Types) + } if len(byID["FEATURE-1"].Children) != 1 || byID["FEATURE-1"].Children[0] != "GRAPH-1" { t.Fatalf("relations=%+v", byID["FEATURE-1"]) } From 6895845bd12cedf1c5f0b100a87933f649db92f3 Mon Sep 17 00:00:00 2001 From: JonatasFreireDev Date: Sat, 18 Jul 2026 11:19:35 -0300 Subject: [PATCH 6/9] docs: add portal review guide --- docs/assets/portal-folder-review.png | Bin 0 -> 89261 bytes docs/assets/portal-review.png | Bin 0 -> 77379 bytes docs/index.html | 82 +++++++++++++++------------ 3 files changed, 47 insertions(+), 35 deletions(-) create mode 100644 docs/assets/portal-folder-review.png create mode 100644 docs/assets/portal-review.png diff --git a/docs/assets/portal-folder-review.png b/docs/assets/portal-folder-review.png new file mode 100644 index 0000000000000000000000000000000000000000..746994e2bca9bd0a9f7300d3fbbc577485d9f2aa GIT binary patch literal 89261 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 JKJY(g{}b4eN|OKp literal 0 HcmV?d00001 diff --git a/docs/assets/portal-review.png b/docs/assets/portal-review.png new file mode 100644 index 0000000000000000000000000000000000000000..7337d3b443a9060924052cda1cd3bd8260514e31 GIT binary patch literal 77379 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

^O@d;q8*>4oeT!s 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 literal 0 HcmV?d00001 diff --git a/docs/index.html b/docs/index.html index a865b90..fe2d4a3 100644 --- a/docs/index.html +++ b/docs/index.html @@ -3,7 +3,7 @@ - + Spec Framework — Guia @@ -553,8 +553,10 @@ letter-spacing:.03em; } .level p{margin:6px 0 0;font-size:13px;color:var(--ink-soft);} +.portal-shot{margin:28px 0;border:1px solid var(--line);background:var(--bg-raised);} +.portal-shot img{display:block;width:100%;height:auto;} +.portal-shot figcaption{padding:10px 14px;font-size:13px;color:var(--ink-soft);border-top:1px solid var(--line);} -/* ---------- skills catalog ---------- */ .cat-block{margin-bottom:34px;} .cat-block:last-child{margin-bottom:0;} .cat-block > h3{ @@ -573,27 +575,6 @@ margin:-4px 0 14px; max-width:70ch; } -.skill-grid{ - display:grid; - grid-template-columns:repeat(auto-fill,minmax(260px,1fr)); - gap:12px; -} -.skill{ - border:1px solid var(--line); - border-radius:var(--radius); - padding:13px 15px; - background:var(--bg-raised); -} -.skill .name{ - font-weight:600; - font-size:14px; - margin-bottom:4px; -} -.skill .desc{ - font-size:13px; - color:var(--ink-soft); -} - /* ---------- design page ---------- */ .dims-grid{ display:grid; @@ -975,8 +956,8 @@ } @media (prefers-reduced-motion:no-preference){ - .pillar,.skill,.level,.rung{transition:border-color .15s ease;} - .pillar:hover,.skill:hover,.level:hover{border-color:var(--accent);} + .pillar,.level,.rung{transition:border-color .15s ease;} + .pillar:hover,.level:hover{border-color:var(--accent);} } @@ -991,7 +972,6 @@ Gates Execução - Skills Design Engenharia Entrega @@ -1000,6 +980,7 @@ Começar Exemplo Guia + Portal CLI @@ -1059,11 +1040,6 @@

Documentação como infraestrutura de engenharia

Execução

-

3. Leia o Bootstrap e peça ajuda ao agente

+

3. Bases antes das specs

+

Antes de criar uma Specification, o framework estabelece o Product Landscape e os contratos compartilhados de knowledge/, engineering/ e design/system/.

+
+
Com código existentemapeia raízes, módulos, regras, integrações, dados, testes, configurações e ativos de design; observação e inferência ficam separadas.
+
Sem códigocria os mesmos contratos como hipóteses revisáveis, com decisões pendentes de aprovação humana.
+
Depois da baseo produto segue para Domain → Goal → Feature → Use Case → Specification sem inventar arquitetura, qualidade ou experiência local.
+
+
+ +
+

4. Leia o Bootstrap e peça ajuda ao agente

Depois do init, o arquivo product/BOOTSTRAP.md explica o caminho escolhido para o seu produto. Leia esse arquivo antes de preencher documentos ou executar outros comandos.

O Bootstrap mostrao que o seu ponto de partida significa, qual é o próximo passo, quais arquivos o agente deve ler e qual gate depende da sua decisão.
@@ -1352,7 +1339,7 @@

Regras que evitam roadmap por sensação

-
+
@@ -1992,6 +1979,31 @@

Exemplo de uso

+
+
+

Portal local de revisão

+
A visão visual da documentação que vive no seu próprio projeto.
+
+

Abra a árvore de documentação no navegador para ver o que está pendente, bloqueado, aprovado ou recusado. Selecione um documento para ler o conteúdo, conferir detalhes e dependências e tomar a decisão permitida.

+

Revisão de um arquivo

+
+ Portal local com árvore de documentação, itens pendentes de decisão, conteúdo do artefato e painel lateral de revisão. +
O portal local apresenta contexto, conteúdo e a decisão permitida no mesmo fluxo.
+
+

Revisão de uma pasta

+
+ Portal local com a pasta domains selecionada, tabela de documentos, status, seleção em lote e painel de revisão em lote. +
Ao selecionar uma pasta, a revisão em lote mostra os documentos, sua elegibilidade e as decisões disponíveis.
+
+
+

Navegação contextual

Árvore de pastas, busca, filtros e dependências levam diretamente ao documento relevante.

+

Decisão governada

Aprovação e recusa respeitam elegibilidade, status e o nome localmente salvo de quem revisa.

+

Revisão em lote

Pastas exibem documentos selecionáveis para aprovar ou recusar em conjunto quando a regra permitir.

+
+
spec-framework dashboard
+

O portal roda localmente e lê apenas o diretório product/. Ele não envia sua documentação para um serviço externo.

+
+

Referência de comandos — CLI

@@ -2154,7 +2166,7 @@

Referência de comandos — CLI

var titles = { inicio:'Início', comecar:'Primeiros passos', oque:'O que é', - escada:'A escada de entrega', skills:'As skills', design:'Fluxo de Design', + escada:'A escada de entrega', design:'Fluxo de Design', portal:'Portal local de revisão', engenharia:'Fluxo de Engenharia', gates:'Gates de aprovação', entrega:'Da task à entrega', exemplo:'Exemplo ponta a ponta', guia:'O Framework Guide', cli:'Referência de comandos' }; From fa9407321fd2cea6ab2c3fde7c1cacda6b7ab70e Mon Sep 17 00:00:00 2001 From: JonatasFreireDev Date: Sat, 18 Jul 2026 12:01:57 -0300 Subject: [PATCH 7/9] feat(engineering): add scalable catalogs and standards --- FRAMEWORK.md | 6 +- README.md | 2 +- assets.go | 2 +- docs/artifact-registry-modules.md | 2 +- docs/engineering-catalog-and-standards.md | 153 ++++++++ docs/engineering-systems.md | 4 + framework/README.md | 2 +- framework/install.md | 2 +- framework/skills/README.md | 1 + .../references/template-authoring.md | 2 +- .../skills/engineering-proposal/SKILL.md | 1 + framework/skills/engineering-review/SKILL.md | 1 + framework/skills/engineering-system/SKILL.md | 25 +- .../assets/engineering-system-template.md | 11 + .../assets/engineering-system-template.yaml | 16 + .../assets/evidence-inventory-template.md | 5 + .../assets/operations-catalog-template.yaml | 4 + .../assets/standard-exception-template.yaml | 11 + .../assets/standard-profile-template.yaml | 6 + .../assets/standard-template.md | 19 + .../assets/standard-template.yaml | 21 ++ .../assets/standards-catalog-template.yaml | 5 + .../assets/technical-catalog-template.yaml | 16 + .../assets/technical-entity-template.md | 32 ++ .../assets/technical-entity-template.yaml | 19 + .../inventory-engineering-evidence.ps1 | 5 +- .../scripts/inventory-engineering-evidence.sh | 3 +- framework/skills/technical-discovery/SKILL.md | 1 + framework/validators/README.md | 2 +- internal/engineeringsystem/system.go | 352 ++++++++++++++++++ internal/engineeringsystem/system_test.go | 65 ++++ internal/install/install_test.go | 36 +- internal/runtimeassets/runtime.go | 21 +- internal/runtimeassets/runtime_test.go | 3 + starter/README.md | 2 +- starter/product/README.md | 2 +- starter/product/engineering/README.md | 3 + .../engineering/architecture/topology.yaml | 6 + starter/product/engineering/catalog/README.md | 9 + .../product/engineering/catalog/catalog.yaml | 10 + starter/product/engineering/context.md | 5 + .../product/engineering/engineering-system.md | 4 + .../engineering/engineering-system.yaml | 16 + .../product/engineering/evidence/README.md | 4 + .../product/engineering/evidence/inventory.md | 4 + .../product/engineering/operations/README.md | 4 + .../engineering/operations/operations.yaml | 4 + .../product/engineering/standards/README.md | 5 + .../standards/profiles/product-default.yaml | 6 + .../engineering/standards/standards.yaml | 5 + 50 files changed, 911 insertions(+), 34 deletions(-) create mode 100644 docs/engineering-catalog-and-standards.md create mode 100644 framework/skills/engineering-system/assets/evidence-inventory-template.md create mode 100644 framework/skills/engineering-system/assets/operations-catalog-template.yaml create mode 100644 framework/skills/engineering-system/assets/standard-exception-template.yaml create mode 100644 framework/skills/engineering-system/assets/standard-profile-template.yaml create mode 100644 framework/skills/engineering-system/assets/standard-template.md create mode 100644 framework/skills/engineering-system/assets/standard-template.yaml create mode 100644 framework/skills/engineering-system/assets/standards-catalog-template.yaml create mode 100644 framework/skills/engineering-system/assets/technical-catalog-template.yaml create mode 100644 framework/skills/engineering-system/assets/technical-entity-template.md create mode 100644 framework/skills/engineering-system/assets/technical-entity-template.yaml create mode 100644 starter/product/engineering/architecture/topology.yaml create mode 100644 starter/product/engineering/catalog/README.md create mode 100644 starter/product/engineering/catalog/catalog.yaml create mode 100644 starter/product/engineering/evidence/README.md create mode 100644 starter/product/engineering/evidence/inventory.md create mode 100644 starter/product/engineering/operations/README.md create mode 100644 starter/product/engineering/operations/operations.yaml create mode 100644 starter/product/engineering/standards/README.md create mode 100644 starter/product/engineering/standards/profiles/product-default.yaml create mode 100644 starter/product/engineering/standards/standards.yaml diff --git a/FRAMEWORK.md b/FRAMEWORK.md index 9a524c0..23d95cc 100644 --- a/FRAMEWORK.md +++ b/FRAMEWORK.md @@ -144,7 +144,9 @@ The Design System is an optional shared product artifact for products with recur ### Engineering System -The optional Engineering System versions stable architecture, module and data ownership, integrations, standards, quality attributes, operations, and evidence under `engineering/`. Its detailed versioning, maturity, hashing, migration, and approval contract lives in [`docs/engineering-systems.md`](docs/engineering-systems.md). Specification and approved `DEC-*` records remain authoritative when contracts conflict. +The optional Engineering System versions stable architecture, module and data ownership, integrations, standards, quality attributes, operations, and evidence under `engineering/`. It models systems, applications, components, repositories, interfaces, data stores, and deployments as independently identified graph entities, so one product may span monorepos, polyrepos, shared components, and multiple deployables without encoding those relations in folder position. Its detailed contracts live in [`docs/engineering-systems.md`](docs/engineering-systems.md) and [`docs/engineering-catalog-and-standards.md`](docs/engineering-catalog-and-standards.md). Specification and approved `DEC-*` records remain authoritative when contracts conflict. + +Engineering standards are versioned, verifiable rules composed through profiles and applied by entity type, capability, or explicit assignment. A more specific contract may add constraints but cannot silently weaken an inherited required standard. Divergence requires a scoped, governed exception or approved decision. ### Engineering Quality System @@ -215,7 +217,7 @@ An adopter repository owns one framework root, `product/`. Its stable ownership | `knowledge/` | Product rules, conventions, cross-cutting decisions, imports, and durable evidence. | | `domains/` | Domain → User Goal → Feature → Use Case hierarchy and delivery artifacts. | | `design/` | Shared Design System and product-owned design sources. | -| `engineering/` | Versioned Engineering System, quality contracts, operations, and evidence. | +| `engineering/` | Versioned Engineering System, technical entity graph, standards, quality contracts, operations, and evidence. | | `audits/` | Readiness, consistency, QA, security, and release findings. | | `releases/` | Product release records when materialized. | diff --git a/README.md b/README.md index 2c29bb5..edbda50 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ spec-framework uninstall --purge --yes | **Engineering flow** | Technical Discovery → Engineering Proposal → independent Engineering Review → Implementation Plan. | | **Execution Graph** | Complete vertical task contracts as a DAG with explicit `writeScope`; parallel work never overlaps write paths. | | **Approval gates** | `draft → proposed → approved → in_progress → implemented → validated → released`, each transition mechanically checked (content hashes, same-diff QA + Code Review, staleness detection). | -| **Shared systems** | Optional versioned Design System (`design/system/`) and Engineering System (`engineering/`) — pinned per delivery, never self-approving. | +| **Shared systems** | Optional versioned Design System (`design/system/`) and scalable Engineering System (`engineering/`) with graph catalogs, standards, quality, operations, and evidence — pinned per delivery, never self-approving. | ## CLI At a Glance diff --git a/assets.go b/assets.go index b6ce4de..360dea1 100644 --- a/assets.go +++ b/assets.go @@ -6,5 +6,5 @@ import "embed" // Init composes product-owned starter assets through versioned contracts; method // assets are materialized in the user cache so the released CLI works offline. // -//go:embed FRAMEWORK.md all:starter all:examples/events docs/artifact-registry-modules.md docs/execution-runtime.md docs/engineering-systems.md docs/lifecycle-and-approvals.md framework/AGENTS.framework.md framework/delivery-closure.md framework/extensions framework/init framework/skills +//go:embed FRAMEWORK.md all:starter all:examples/events docs/artifact-registry-modules.md docs/execution-runtime.md docs/engineering-systems.md docs/engineering-catalog-and-standards.md docs/lifecycle-and-approvals.md framework/AGENTS.framework.md framework/delivery-closure.md framework/extensions framework/init framework/skills var Assets embed.FS diff --git a/docs/artifact-registry-modules.md b/docs/artifact-registry-modules.md index 35ab9d7..21fdbbe 100644 --- a/docs/artifact-registry-modules.md +++ b/docs/artifact-registry-modules.md @@ -40,7 +40,7 @@ The approval engine remains generic. Adapters are only for side effects or compo | --- | --- | | `generic` | Update document, registry, and approval record | | `foundation-context` | Synchronize the canonical document status with its `context.md` | -| `engineering-system` | Synchronize context, YAML, Quality System, and composite hash | +| `engineering-system` | Synchronize context, YAML, technical entity graph, standards, operations, Quality System, evidence, and composite hash | | `design-system` | Validate tokens/catalog when applicable | | `feature-brief` | Validate `targetFeature` and the selected feature | diff --git a/docs/engineering-catalog-and-standards.md b/docs/engineering-catalog-and-standards.md new file mode 100644 index 0000000..0b3ef40 --- /dev/null +++ b/docs/engineering-catalog-and-standards.md @@ -0,0 +1,153 @@ +# Engineering Catalog And Standards + +## Purpose + +This contract defines how `engineering/` grows from a product baseline into a +multi-system, multi-application, and multi-repository technical knowledge graph. +It also records the implementation plan for adopting that model across the +framework. + +The Engineering System must not assume that one product equals one repository +or one deployable application. Repositories, systems, applications, components, +interfaces, data stores, and deployments are independent entities connected by +stable identifiers. + +## Canonical Structure + +```text +engineering/ +├── context.md +├── engineering-system.md +├── engineering-system.yaml +├── architecture/ +│ ├── system-context.md +│ ├── modules.md +│ └── topology.yaml +├── catalog/ +│ ├── catalog.yaml +│ ├── systems/ +│ ├── applications/ +│ ├── components/ +│ ├── repositories/ +│ ├── data-stores/ +│ ├── interfaces/ +│ └── deployments/ +├── standards/ +│ ├── standards.yaml +│ ├── profiles/ +│ ├── catalog/ +│ └── exceptions/ +├── quality/ +├── operations/ +│ ├── operations.yaml +│ ├── environments/ +│ ├── deployments/ +│ └── runbooks/ +├── evidence/ +│ └── inventory.md +└── decisions/ +``` + +Only the root catalogs and README contracts are initialized. Entity records, +profiles, standards, exceptions, environments, and runbooks are materialized +on demand from evidence or explicit hypotheses; empty placeholder forests are +not required. + +## Entity Graph + +| Entity | Responsibility | +| --- | --- | +| System | Technical capability composed from one or more applications or components. | +| Application | Executable or user-facing unit delivered as a whole. | +| Component | Service, module, package, worker, frontend, or reusable library. | +| Repository | Physical source-control boundary containing one or more components. | +| Interface | API, event, queue, file, webhook, or other cross-component contract. | +| Data store | Database, cache, bucket, index, or other persistent source. | +| Deployment | Mapping from components and artifacts to an environment and release strategy. | + +Relations live in mechanical catalogs and use stable IDs. Folder position is +for navigation and never determines ownership or containment by itself. This +supports monorepos, polyrepos, shared components, multiple deployables per +repository, and applications assembled from several repositories. + +Each relation declares a stable `REL-*` ID, an extensible relation `type`, and +existing `source` and `target` entity IDs. Evidence may be attached when the +relation is observed rather than hypothetical. The framework validates graph +integrity without imposing a closed relation vocabulary. + +## Contract Inheritance + +```text +Engineering System +→ System +→ Application +→ Component +→ delivery-specific Engineering Proposal +``` + +A more specific contract may add constraints. It may not silently weaken or +replace an inherited contract. A divergence requires a governed exception, +deviation, or approved `DEC-*` record. + +## Standards System + +`engineering/standards/` is a versioned catalog of verifiable technical +rules. Standards are independent entities grouped into profiles and selected +by entity type, capability, or explicit assignment. + +Canonical categories are architecture, code, API, events, data, dependencies, +security, observability, testing, and delivery. A standard declares: + +- stable ID, semantic version, status, category, and obligation level; +- applicable entity types and capabilities; +- individually identifiable rules; +- verification methods and required evidence; +- exception policy and compatibility notes. + +Obligation levels are `required`, `recommended`, `experimental`, and +`deprecated`. Required standards block a governed gate unless conformity or +an open, approved, unexpired, in-scope `STDEX-*` exception is recorded. + +Profiles compose standards for common shapes such as web applications, HTTP +APIs, workers, event consumers, shared libraries, and product defaults. +Consumers pin profile and standard versions. Profiles may extend other +profiles, but cycles are invalid. + +Standards define technical rules. The Quality System defines quality policy and +coverage. The security baseline defines threats, trust, and governed controls. +Standards reference these contracts rather than duplicating them. + +## Evidence And Maturity + +Every maturity above `baseline` requires resolvable evidence. Evidence may +reference repository paths, tests, CI, commands, runtime observations, or +approved external sources. `engineering/evidence/inventory.md` indexes the +evidence used by catalogs without copying volatile output into contracts. + +The Engineering System composite approval hash covers every file under +`engineering/`. Adding or changing a catalog, profile, standard, exception, +entity, operation, or evidence contract makes existing approval stale. + +## Compatibility And Migration + +The model is additive. Existing adopters with only `engineering-system.md`, +`engineering-system.yaml`, architecture, and quality remain valid. Upgrade +must not create or overwrite adopter-owned entity records. New starter +repositories receive empty root catalogs; existing products opt into them by a +previewable migration or normal skill-driven evolution followed by approval. + +## Implementation Plan + +1. Make this document and `FRAMEWORK.md` the method-level contract. +2. Expand the Engineering System skill workflow, outputs, checklist, and owned + assets for catalogs, standards, operations, and evidence. +3. Add human and mechanical templates for technical entities, standards, + profiles, exceptions, and root catalogs. +4. Expand the starter with root catalogs and navigational READMEs only. +5. Add the catalog, standards, operations, and evidence areas to the starter + Engineering System YAML and human summary. +6. Validate catalog schema versions, safe relative paths, unique IDs, standard + obligation levels, and referenced records without rejecting legacy products. +7. Update embedded assets, runtime documentation, starter guidance, and tests. +8. Verify init, upgrade preservation, full Go gates, fixture validation, and + distribution boundaries before release. diff --git a/docs/engineering-systems.md b/docs/engineering-systems.md index 6f917ad..025e7cd 100644 --- a/docs/engineering-systems.md +++ b/docs/engineering-systems.md @@ -6,6 +6,10 @@ This document contains the detailed contract for the shared Engineering System a The Engineering System versions stable architecture, module and data ownership, integrations, standards, quality attributes, operations, and evidence under `engineering/`. `engineering-system.md` is the human contract and `engineering-system.yaml` is the mechanical catalog. Origin is `generate`, `evolve`, or `adopt`; maturity records evidence and never grants approval. +Its scalable entity and standards model is defined in [`engineering-catalog-and-standards.md`](engineering-catalog-and-standards.md). Systems, applications, components, repositories, interfaces, data stores, and deployments are independent graph entities. Root catalogs are initialized once; entity records are created only from evidence or explicit hypotheses. + +The standards system composes versioned rules through profiles. Consumers pin applicable profiles and standards. A narrower scope may add constraints but may not silently weaken inherited contracts. Exceptions require a stable ID, exact scope, owner, rationale, residual risk, mitigation, expiry or review date, re-entry gate, status, and approval where required. + Engineering System approval hashes its complete contract surface deterministically. A change to an approved shared contract makes its approval stale and requires human re-approval. Specification and approved product decisions remain authoritative when contracts conflict. ## Engineering Quality System diff --git a/framework/README.md b/framework/README.md index 07aaec6..d0caa52 100644 --- a/framework/README.md +++ b/framework/README.md @@ -38,6 +38,6 @@ Use `spec-framework init` to copy `starter/product/`, cache embedded framework a Use `spec-framework upgrade` to refresh the external runtime and manifest without overwriting adopter-owned product content. -The runtime also ships the shared operational contracts [`docs/execution-runtime.md`](../docs/execution-runtime.md), [`docs/engineering-systems.md`](../docs/engineering-systems.md), and [`docs/lifecycle-and-approvals.md`](../docs/lifecycle-and-approvals.md). Skills and orchestrators reference these contracts instead of duplicating their cross-cutting rules. +The runtime also ships the shared operational contracts [`docs/execution-runtime.md`](../docs/execution-runtime.md), [`docs/engineering-systems.md`](../docs/engineering-systems.md), [`docs/engineering-catalog-and-standards.md`](../docs/engineering-catalog-and-standards.md), and [`docs/lifecycle-and-approvals.md`](../docs/lifecycle-and-approvals.md). Skills and orchestrators reference these contracts instead of duplicating their cross-cutting rules. For framework development, use `go run ./cmd/spec-framework`; adopters use the precompiled release binary. diff --git a/framework/install.md b/framework/install.md index ec09388..eff893c 100644 --- a/framework/install.md +++ b/framework/install.md @@ -83,4 +83,4 @@ spec-framework decisions check --product-root product --strict Use `--json` for CI annotations or `--fix-links --yes` only after reviewing the preview. The command never changes approval records, moves decision files, or rewrites ambiguous references. -The pinned runtime also materializes the shared contracts [`execution-runtime.md`](../docs/execution-runtime.md), [`engineering-systems.md`](../docs/engineering-systems.md), and [`lifecycle-and-approvals.md`](../docs/lifecycle-and-approvals.md) alongside `FRAMEWORK.md` and `AGENTS.framework.md`. Skills and orchestrators use these documents for cross-cutting runtime, engineering-system, and lifecycle rules. +The pinned runtime also materializes the shared contracts [`execution-runtime.md`](../docs/execution-runtime.md), [`engineering-systems.md`](../docs/engineering-systems.md), [`engineering-catalog-and-standards.md`](../docs/engineering-catalog-and-standards.md), and [`lifecycle-and-approvals.md`](../docs/lifecycle-and-approvals.md) alongside `FRAMEWORK.md` and `AGENTS.framework.md`. Skills and orchestrators use these documents for cross-cutting runtime, engineering-system, catalog, standards, and lifecycle rules. diff --git a/framework/skills/README.md b/framework/skills/README.md index dff7759..e82a513 100644 --- a/framework/skills/README.md +++ b/framework/skills/README.md @@ -43,6 +43,7 @@ Skills and orchestrators must read the shared contract that matches their respon - [`execution-runtime.md`](../../docs/execution-runtime.md): workspaces, leases, graph scheduling, command plans, and integration. - [`engineering-systems.md`](../../docs/engineering-systems.md): Engineering System and Engineering Quality System versioning, migration, evidence, and approval boundaries. +- [`engineering-catalog-and-standards.md`](../../docs/engineering-catalog-and-standards.md): scalable technical entity graph, standards profiles, inheritance, exceptions, operations catalogs, and evidence contracts. - [`lifecycle-and-approvals.md`](../../docs/lifecycle-and-approvals.md): lifecycle states, approval records, staleness, authority, and failure routing. Use `framework-guide` as the default conversational entry point when no verified specialist route exists. A direct specialist route requires current CLI guidance or an explicit human request that names both the specialist and concrete scope. Persisted handoffs/checkpoints must first be revalidated with `dashboard`, `status`, `next`, or `guide`; a skill name without scope is only a hint. Framework Guide activates product operations only from a valid `product/.product/framework.json`, routes bootstrap/init before activation when explicitly requested, resolves pinned specialist contracts with `spec-framework skill path`, reads CLI state first, and routes governed runtime execution back through Command Planner and Command Executor. diff --git a/framework/skills/documentation-writer/references/template-authoring.md b/framework/skills/documentation-writer/references/template-authoring.md index 57794dc..29eaa55 100644 --- a/framework/skills/documentation-writer/references/template-authoring.md +++ b/framework/skills/documentation-writer/references/template-authoring.md @@ -20,7 +20,7 @@ Use the generating skill's own templates whenever a new canonical artifact is cr - `domain-template.md`, `goal-template.md`, `feature-template.md`, `use-case-template.md`: product hierarchy artifacts. - `journey-template.md`: user journey artifact. - `specification-template.md`, `design-template.md`, `design-system-template.md`, `design-component-template.md`, `design-pattern-template.md`, `engineering-system-template.md`, `engineering-proposal-template.md`, `engineering-review-template.md`, `implementation-plan-template.md`: planning, shared Design and Engineering contracts, and independent technical review. -- `engineering-system-template.yaml`, `quality-system-template.md`, `quality-system-template.yaml`, `quality-model-template.md`, `test-strategy-template.md`, `fitness-functions-template.yaml`: mechanical Engineering System catalog and shared product quality contracts. +- Engineering System assets include root and entity catalogs, standards, profiles, exceptions, operations and evidence templates, plus `quality-system-template.md`, `quality-system-template.yaml`, `quality-model-template.md`, `test-strategy-template.md`, and `fitness-functions-template.yaml` for shared product quality contracts. - `execution-graph-template.json`, `tasks-template.md`, `tests-template.md`: executable planning artifacts. - `qa-evidence-template.md`, `security-review-template.md`, `security-baseline-template.md`, `threat-register-template.md`: validation evidence, security gate, and proactive threat modeling artifacts. - `analytics-template.md`, `audit-template.md`, `readiness-report-template.md`: validation and measurement artifacts. diff --git a/framework/skills/engineering-proposal/SKILL.md b/framework/skills/engineering-proposal/SKILL.md index a0ba70f..4916b86 100644 --- a/framework/skills/engineering-proposal/SKILL.md +++ b/framework/skills/engineering-proposal/SKILL.md @@ -25,6 +25,7 @@ Approved Specification; approved Design or `Not applicable`; approved Technical ## Required reading - [`engineering-systems.md`](../../docs/engineering-systems.md) for the pinned Engineering System and Quality System contracts. +- [`engineering-catalog-and-standards.md`](../../docs/engineering-catalog-and-standards.md) for applicable entity, profile, standard, exception, and operations contracts. - the framework root's `FRAMEWORK.md` - Relevant parent context.md files. - This skill owns its generation resources: `assets/engineering-proposal-template.md`. diff --git a/framework/skills/engineering-review/SKILL.md b/framework/skills/engineering-review/SKILL.md index 360bc41..1f100ed 100644 --- a/framework/skills/engineering-review/SKILL.md +++ b/framework/skills/engineering-review/SKILL.md @@ -25,6 +25,7 @@ Approved Specification; Design; approved Technical Discovery; `engineering-propo ## Required reading - [`engineering-systems.md`](../../docs/engineering-systems.md) for shared contract versioning, hashes, and review boundaries. +- [`engineering-catalog-and-standards.md`](../../docs/engineering-catalog-and-standards.md) for entity graph, standards inheritance, exceptions, and operations conformance. - the framework root's `FRAMEWORK.md` - Relevant parent context.md files. - This skill owns its generation resources: `assets/engineering-review-template.md`. diff --git a/framework/skills/engineering-system/SKILL.md b/framework/skills/engineering-system/SKILL.md index a28a684..59c7418 100644 --- a/framework/skills/engineering-system/SKILL.md +++ b/framework/skills/engineering-system/SKILL.md @@ -9,7 +9,7 @@ description: "Engineering System Skill. Use when an agent needs to create, adopt Planning ## Responsibility -Own the shared `engineering/engineering-system.md` and `engineering-system.yaml` contracts. Maintain stable product engineering knowledge without making product decisions, planning a specific delivery, or writing application code. +Own the shared `engineering/engineering-system.md` and `engineering-system.yaml` contracts, technical entity graph, standards, operations catalogs, quality policy, and evidence index. Maintain stable product engineering knowledge without making product decisions, planning a specific delivery, or writing application code. ## Operating modes - create: establish the first evidence-backed Engineering System. @@ -22,13 +22,14 @@ Own the shared `engineering/engineering-system.md` and `engineering-system.yaml` Product context; real code and test tree; deployment and operations configuration; approved decisions; conventions; runbooks; dependency evidence. ## Outputs -`engineering/engineering-system.md`; `engineering/engineering-system.yaml`; architecture, standards, quality, runbook, and evidence links; decision candidates; version and compatibility notes. +`engineering/engineering-system.md`; `engineering/engineering-system.yaml`; root technical catalog; on-demand system, application, component, repository, interface, data-store, and deployment records; standards catalog, profiles, standards, and exceptions; architecture, quality, operations, runbook, and evidence links; decision candidates; version and compatibility notes. ## Required reading - [`engineering-systems.md`](../../docs/engineering-systems.md) for shared Engineering System and Quality System versioning, migration, and approval boundaries. +- [`engineering-catalog-and-standards.md`](../../docs/engineering-catalog-and-standards.md) for the scalable entity graph, standards inheritance, exceptions, and materialization boundary. - the framework root's `FRAMEWORK.md` - Relevant parent context.md files. -- This skill owns its generation resources: the engineering-system, quality-system, quality-model, test-strategy, and fitness-function resources in `assets/`. +- This skill owns every generation resource in `assets/`: Engineering System, technical catalog and entity, standards catalog, standard, profile, exception, quality system and model, test strategy, fitness functions, operations catalog, and evidence inventory. - Approved decisions are discovered through the active product root's `.product/decisions.json`; resolve each registered `path` from its declared domain root (`knowledge/decisions/`, `design/decisions/`, or `engineering/decisions/`). ## Discovery and challenge @@ -43,12 +44,14 @@ Use `scripts/inventory-engineering-evidence.ps1` on Windows or `scripts/inventor ## Workflow 1. Inspect the real product code, tests, configuration, environments, operations evidence, and existing engineering documents. If no code exists, create explicit hypothesis contracts, pending decisions, and intended constraints rather than pretending evidence exists. 2. Define the covered product and repository boundaries and choose `generate`, `evolve`, or `adopt`. -3. Inventory modules, ownership, data, integrations, standards, quality attributes, test strategy, gates, environments, test data, runbooks, and consumers with evidence paths. -4. Maintain `engineering/quality/quality-system.md` and `quality-system.yaml` together with the quality model, test strategy, and fitness functions. Keep commands canonical in `knowledge/conventions/gates.md`. -5. Declare maturity per area only when its required evidence exists; maturity never implies approval. -6. Link approved `DEC-*` records for governed choices and record candidates for missing structural, data, security, or operational decisions. -7. Update the human contract and mechanical catalogs together, applying semantic versioning and compatibility notes. -8. Stop before inventing architecture, editing application code, or creating approval records. +3. Inventory systems, applications, components, repositories, ownership, data stores, interfaces, deployments, standards, quality attributes, environments, runbooks, and consumers with evidence paths. Model stable IDs and graph relations instead of inferring them from folders. +4. Initialize only root catalogs. Materialize entity records, standards, profiles, exceptions, environments, deployments, and runbooks on demand from evidence or explicit hypotheses. +5. Compose standards through profiles selected by entity type, capability, or explicit assignment. A narrower contract may add constraints but requires a governed exception or decision to weaken an inherited required rule. +6. Maintain `engineering/quality/quality-system.md` and `quality-system.yaml` together with the quality model, test strategy, and fitness functions. Keep commands canonical in `knowledge/conventions/gates.md`. +7. Declare maturity per area only when its required evidence exists; maturity never implies approval. +8. Link approved `DEC-*` records for governed choices and record candidates for missing structural, data, security, standards, or operational decisions. +9. Update the human contract and every affected mechanical catalog together, applying semantic versioning and compatibility notes. +10. Stop before inventing architecture, editing application code, or creating approval records. ## Quality checklist - [ ] Preserves traceability to affected artifacts. @@ -60,6 +63,10 @@ Use `scripts/inventory-engineering-evidence.ps1` on Windows or `scripts/inventor - [ ] Quality exceptions identify scope, owner, rationale, residual risk, mitigation, expiry or review date, re-entry gate, and status. - [ ] Only open, unexpired, in-scope exceptions are passed to delivery consumers. - [ ] Stable knowledge is not duplicated into delivery proposals. +- [ ] Repository, application, component, and deployment relations use stable IDs and support monorepo and polyrepo shapes. +- [ ] Standards declare version, obligation level, applicability, verification, evidence, and exception policy. +- [ ] Profiles do not contain cycles and consumers cannot silently weaken inherited required standards. +- [ ] Optional records are materialized only when evidence or explicit hypotheses require them. - [ ] Detects gaps, conflicts, and dependencies. - [ ] Records or requests decisions for meaningful changes. - [ ] Leaves a clear handoff for the next skill or orchestrator. diff --git a/framework/skills/engineering-system/assets/engineering-system-template.md b/framework/skills/engineering-system/assets/engineering-system-template.md index f22a355..dad9d25 100644 --- a/framework/skills/engineering-system/assets/engineering-system-template.md +++ b/framework/skills/engineering-system/assets/engineering-system-template.md @@ -37,6 +37,7 @@ | --- | --- | --- | --- | | System context | [architecture/system-context.md](architecture/system-context.md) | `[code/config/decision path]` | `baseline | mapped | governed | verified | operated` | | Modules | [architecture/modules.md](architecture/modules.md) | `[code/test path]` | `baseline | mapped | governed | verified | operated` | +| Technical catalog | [catalog/catalog.yaml](catalog/catalog.yaml) | `[catalog/entity evidence]` | `baseline | mapped | governed | verified | operated` | | Data ownership | `[path or Not configured]` | `[evidence]` | `[maturity]` | | Integrations | `[path or Not configured]` | `[evidence]` | `[maturity]` | | Deployment | `[path or Not configured]` | `[evidence]` | `[maturity]` | @@ -51,6 +52,10 @@ | Fitness functions | `[quality/fitness-functions.yaml or Not configured]` | `[command/evidence]` | | Engineering standards | `[standards path or Not configured]` | `[gate/evidence]` | +Standards are composed through versioned profiles and applied by entity type, +capability, or explicit assignment. Required inherited rules may be weakened +only through a scoped, governed exception or approved decision. + ## Decisions And Deviations | Decision or deviation | Scope | Status | Evidence | @@ -64,6 +69,12 @@ | Deploy and rollback | `[path or Not configured]` | `[evidence]` | `[maturity]` | | Incident response | `[path or Not configured]` | `[evidence]` | `[maturity]` | +## Technical Entity Graph + +| Entity | Stable ID | Repositories or components | Standards profiles | Evidence | +| --- | --- | --- | --- | --- | +| `[system/application/component/etc.]` | `[ID]` | `[IDs]` | `[PROFILE-*]` | `[path]` | + ## Consumers | Use case or system | Pinned version | Deviations | diff --git a/framework/skills/engineering-system/assets/engineering-system-template.yaml b/framework/skills/engineering-system/assets/engineering-system-template.yaml index 7f9bd0b..14b66a1 100644 --- a/framework/skills/engineering-system/assets/engineering-system-template.yaml +++ b/framework/skills/engineering-system/assets/engineering-system-template.yaml @@ -13,10 +13,26 @@ areas: contract: architecture/modules.md maturity: baseline evidence: [] + technical_catalog: + contract: catalog/catalog.yaml + maturity: baseline + evidence: [] + standards: + contract: standards/standards.yaml + maturity: baseline + evidence: [] quality: contract: quality/quality-system.md maturity: baseline evidence: [] + operations: + contract: operations/operations.yaml + maturity: baseline + evidence: [] + evidence: + contract: evidence/inventory.md + maturity: baseline + evidence: [] decisions: [] standards: [] fitness_functions: [] diff --git a/framework/skills/engineering-system/assets/evidence-inventory-template.md b/framework/skills/engineering-system/assets/evidence-inventory-template.md new file mode 100644 index 0000000..59f03ec --- /dev/null +++ b/framework/skills/engineering-system/assets/evidence-inventory-template.md @@ -0,0 +1,5 @@ +# Engineering Evidence Inventory + +| Evidence ID | Scope | Kind | Source | Observed on | Notes | +| --- | --- | --- | --- | --- | --- | +| `EVID-001` | `[entity or area ID]` | `[code | test | config | CI | runtime | external]` | `[path or URL]` | `YYYY-MM-DD` | [what this proves] | diff --git a/framework/skills/engineering-system/assets/operations-catalog-template.yaml b/framework/skills/engineering-system/assets/operations-catalog-template.yaml new file mode 100644 index 0000000..220e4b1 --- /dev/null +++ b/framework/skills/engineering-system/assets/operations-catalog-template.yaml @@ -0,0 +1,4 @@ +schema_version: 1 +environments: {} +deployments: {} +runbooks: {} diff --git a/framework/skills/engineering-system/assets/standard-exception-template.yaml b/framework/skills/engineering-system/assets/standard-exception-template.yaml new file mode 100644 index 0000000..763ca71 --- /dev/null +++ b/framework/skills/engineering-system/assets/standard-exception-template.yaml @@ -0,0 +1,11 @@ +schema_version: 1 +id: STDEX-001 +standard: STD-CATEGORY-001 +scope: [] +owner: Unassigned +rationale: "[why the standard cannot currently be met]" +residual_risk: "[remaining risk]" +mitigation: "[temporary mitigation]" +expires_on: YYYY-MM-DD +reentry_gate: "[condition required to conform]" +status: open diff --git a/framework/skills/engineering-system/assets/standard-profile-template.yaml b/framework/skills/engineering-system/assets/standard-profile-template.yaml new file mode 100644 index 0000000..026e0fd --- /dev/null +++ b/framework/skills/engineering-system/assets/standard-profile-template.yaml @@ -0,0 +1,6 @@ +schema_version: 1 +id: PROFILE-NAME +version: 1.0.0 +status: draft +extends: [] +standards: [] diff --git a/framework/skills/engineering-system/assets/standard-template.md b/framework/skills/engineering-system/assets/standard-template.md new file mode 100644 index 0000000..485456c --- /dev/null +++ b/framework/skills/engineering-system/assets/standard-template.md @@ -0,0 +1,19 @@ +# [STD-*] — [Standard title] + +## Objective + +[Technical outcome protected by this standard.] + +## Applicability + +[Entity types, capabilities, and explicit exclusions.] + +## Rules + +| Rule ID | Requirement | Verification | +| --- | --- | --- | +| `[STD-*-R01]` | [verifiable requirement] | [test, schema, review, command, or evidence] | + +## Compatibility And Exceptions + +[Compatibility impact and whether governed exceptions are permitted.] diff --git a/framework/skills/engineering-system/assets/standard-template.yaml b/framework/skills/engineering-system/assets/standard-template.yaml new file mode 100644 index 0000000..a4ea564 --- /dev/null +++ b/framework/skills/engineering-system/assets/standard-template.yaml @@ -0,0 +1,21 @@ +schema_version: 1 +id: STD-CATEGORY-001 +title: "[title]" +version: 1.0.0 +status: draft +category: "[architecture | code | api | events | data | dependencies | security | observability | testing | delivery]" +level: "[required | recommended | experimental | deprecated]" +applies_to: + entity_types: [] + capabilities: [] +rules: + - id: STD-CATEGORY-001-R01 + requirement: "[verifiable requirement]" + verification: + - "[test, schema, review, command, or evidence]" +evidence: + required: [] +exceptions: + allowed: true + approval_required: true + maximum_duration_days: 90 diff --git a/framework/skills/engineering-system/assets/standards-catalog-template.yaml b/framework/skills/engineering-system/assets/standards-catalog-template.yaml new file mode 100644 index 0000000..0a91076 --- /dev/null +++ b/framework/skills/engineering-system/assets/standards-catalog-template.yaml @@ -0,0 +1,5 @@ +schema_version: 1 +profiles: + PROFILE-PRODUCT-DEFAULT: profiles/product-default.yaml +standards: {} +exceptions: {} diff --git a/framework/skills/engineering-system/assets/technical-catalog-template.yaml b/framework/skills/engineering-system/assets/technical-catalog-template.yaml new file mode 100644 index 0000000..92896e3 --- /dev/null +++ b/framework/skills/engineering-system/assets/technical-catalog-template.yaml @@ -0,0 +1,16 @@ +schema_version: 1 +entities: + systems: {} + applications: {} + components: {} + repositories: {} + data_stores: {} + interfaces: {} + deployments: {} +relations: [] +# Example relation materialized on demand: +# - id: REL-APP-COMPONENT-001 +# type: contains +# source: APP-001 +# target: CMP-001 +# evidence: [] diff --git a/framework/skills/engineering-system/assets/technical-entity-template.md b/framework/skills/engineering-system/assets/technical-entity-template.md new file mode 100644 index 0000000..2aef00a --- /dev/null +++ b/framework/skills/engineering-system/assets/technical-entity-template.md @@ -0,0 +1,32 @@ +# [Entity type]: [name] + +## Snapshot + +| Field | Value | +| --- | --- | +| ID | `[SYS|APP|CMP|REPO|DATA|IFACE|DEPLOY]-*` | +| Type | `system | application | component | repository | data-store | interface | deployment` | +| Status | `draft` | +| Mechanical record | [entity.yaml](entity.yaml) | + +## Responsibility + +[Stable responsibility and explicit boundaries.] + +## Relations + +| Relation | Target ID | Evidence | +| --- | --- | --- | +| `[contains | depends_on | exposes | stores_in | deployed_as]` | `[stable ID]` | `[path]` | + +## Ownership And Evidence + +| Concern | Owner | Evidence | +| --- | --- | --- | +| Technical ownership | `[owner or Unassigned]` | `[path]` | + +## Standards + +| Profile or standard | Version | Exception | +| --- | --- | --- | +| `[PROFILE-* or STD-*]` | `[version]` | `[None or STDEX-*]` | diff --git a/framework/skills/engineering-system/assets/technical-entity-template.yaml b/framework/skills/engineering-system/assets/technical-entity-template.yaml new file mode 100644 index 0000000..c53af3f --- /dev/null +++ b/framework/skills/engineering-system/assets/technical-entity-template.yaml @@ -0,0 +1,19 @@ +schema_version: 1 +id: "[stable entity ID]" +type: "[system | application | component | repository | data-store | interface | deployment]" +status: draft +name: "[name]" +capabilities: [] +repositories: [] +components: [] +depends_on: [] +interfaces: [] +data_stores: [] +deployments: [] +standards: + profiles: [] + additional: [] + exceptions: [] +ownership: + technical: Unassigned +evidence: [] diff --git a/framework/skills/engineering-system/scripts/inventory-engineering-evidence.ps1 b/framework/skills/engineering-system/scripts/inventory-engineering-evidence.ps1 index aa2ec83..fc39f35 100644 --- a/framework/skills/engineering-system/scripts/inventory-engineering-evidence.ps1 +++ b/framework/skills/engineering-system/scripts/inventory-engineering-evidence.ps1 @@ -9,8 +9,11 @@ foreach ($root in $manifest.code_roots) { $path = Join-Path $repo $root.path Write-Output "`n## $($root.path) ($($root.role))" if (-not (Test-Path $path)) { Write-Output "MISSING"; continue } + $repository = Get-ChildItem -LiteralPath $path -Force -File -ErrorAction SilentlyContinue | + Where-Object { $_.Name -in @("go.mod", "package.json", "pyproject.toml", "Cargo.toml", "pom.xml", "build.gradle", "settings.gradle") -or $_.Extension -in @(".sln", ".csproj", ".fsproj") } + foreach ($file in $repository) { Write-Output ("repository-contract: " + $file.FullName.Substring($repo.Length + 1).Replace("\", "/")) } Get-ChildItem -LiteralPath $path -Force -File -Recurse -ErrorAction SilentlyContinue | - Where-Object { $_.Name -match '^(Dockerfile|docker-compose.*|Makefile|README\.md|.*\.ya?ml)$' -or $_.FullName -match '(test|spec|\.github|\.gitlab)' } | + Where-Object { $_.Name -match '^(Dockerfile|docker-compose.*|Makefile|README\.md|go\.mod|package\.json|pyproject\.toml|Cargo\.toml|pom\.xml|build\.gradle|settings\.gradle|.*\.ya?ml|.*\.tf)$' -or $_.Extension -in @(".sln", ".csproj", ".fsproj") -or $_.FullName -match '(test|spec|\.github|\.gitlab|deploy|infra|runbook)' } | ForEach-Object { Write-Output ("evidence: " + $_.FullName.Substring($repo.Length + 1).Replace("\", "/")) } } if ($Validate) { & spec-framework engineering-system validate --product-root (Join-Path $repo $ProductRoot); exit $LASTEXITCODE } diff --git a/framework/skills/engineering-system/scripts/inventory-engineering-evidence.sh b/framework/skills/engineering-system/scripts/inventory-engineering-evidence.sh index 099705e..1b55df5 100644 --- a/framework/skills/engineering-system/scripts/inventory-engineering-evidence.sh +++ b/framework/skills/engineering-system/scripts/inventory-engineering-evidence.sh @@ -9,7 +9,8 @@ roots="$(awk -F '"' '/^[[:space:]]*"path"[[:space:]]*:/ { path=$4 } /^[[:space:] while IFS=$'\t' read -r path role; do [[ -n "$path" ]] || continue; root="$repo/$path"; echo; echo "## $path ($role)" [[ -d "$root" ]] || { echo "MISSING"; continue; } - while IFS= read -r file; do echo "evidence: ${file#"$repo/"}"; done < <(find "$root" -type f \( -name Dockerfile -o -name 'docker-compose*' -o -name Makefile -o -name README.md -o -name '*.yml' -o -name '*.yaml' -o -iname '*test*' -o -iname '*spec*' \) -print) + while IFS= read -r file; do echo "repository-contract: ${file#"$repo/"}"; done < <(find "$root" -maxdepth 1 -type f \( -name go.mod -o -name package.json -o -name pyproject.toml -o -name Cargo.toml -o -name pom.xml -o -name build.gradle -o -name settings.gradle -o -name '*.sln' -o -name '*.csproj' -o -name '*.fsproj' \) -print) + while IFS= read -r file; do echo "evidence: ${file#"$repo/"}"; done < <(find "$root" -type f \( -name Dockerfile -o -name 'docker-compose*' -o -name Makefile -o -name README.md -o -name go.mod -o -name package.json -o -name pyproject.toml -o -name Cargo.toml -o -name pom.xml -o -name build.gradle -o -name settings.gradle -o -name '*.sln' -o -name '*.csproj' -o -name '*.fsproj' -o -name '*.yml' -o -name '*.yaml' -o -name '*.tf' -o -iname '*test*' -o -iname '*spec*' -o -ipath '*deploy*' -o -ipath '*infra*' -o -ipath '*runbook*' \) -print) done <<< "$roots" if $validate; then spec-framework engineering-system validate --product-root "$repo/$product_root" diff --git a/framework/skills/technical-discovery/SKILL.md b/framework/skills/technical-discovery/SKILL.md index 130827b..ca19014 100644 --- a/framework/skills/technical-discovery/SKILL.md +++ b/framework/skills/technical-discovery/SKILL.md @@ -25,6 +25,7 @@ Approved specification contracts; approved design or `Not applicable`; stable en ## Required reading - [`engineering-systems.md`](../../docs/engineering-systems.md) for the stable engineering baseline and migration boundaries. +- [`engineering-catalog-and-standards.md`](../../docs/engineering-catalog-and-standards.md) for technical entity discovery and applicable standards. - the framework root's `FRAMEWORK.md` - Relevant parent context.md files. - This skill owns its generation resources: `assets/technical-discovery-template.md`. diff --git a/framework/validators/README.md b/framework/validators/README.md index b8cad01..d451890 100644 --- a/framework/validators/README.md +++ b/framework/validators/README.md @@ -44,7 +44,7 @@ The validator checks: - decision index paths in `.product/decisions.json`; - visual Design source identity, immutable versions, authority, screen inventories, snapshot paths, origin mode, maturity, fidelity policy, source references, and requirement coverage values; - canonical Design System identity, status, semantic version, token schema, aliases and cycles, plus proposed-or-later Design consumer pins; -- canonical Engineering System identity, semantic version, origin, catalog areas, maturity evidence, contract paths, and structured delivery triggers; +- canonical Engineering System identity, semantic version, origin, catalog areas, technical entity graph, standards profiles and exceptions, operations references, maturity evidence, contract paths, and structured delivery triggers; - Engineering Proposal consumer pins, Engineering Review proposal hashes, and current approval evidence for operational navigation; - decision references against `.product/decisions.json`, including approved delivery dependencies; - visual Mermaid standards for flowcharts; diff --git a/internal/engineeringsystem/system.go b/internal/engineeringsystem/system.go index 153c655..ca325f9 100644 --- a/internal/engineeringsystem/system.go +++ b/internal/engineeringsystem/system.go @@ -33,6 +33,25 @@ var allowedMaturity = map[string]bool{ "operated": true, } +var technicalEntityPrefixes = map[string]string{ + "systems": "SYS-", "applications": "APP-", "components": "CMP-", + "repositories": "REPO-", "data_stores": "DATA-", "interfaces": "IFACE-", "deployments": "DEPLOY-", +} + +var technicalEntityTypes = map[string]string{ + "systems": "system", "applications": "application", "components": "component", + "repositories": "repository", "data_stores": "data-store", "interfaces": "interface", "deployments": "deployment", +} + +var allowedStandardCategories = map[string]bool{ + "architecture": true, "code": true, "api": true, "events": true, "data": true, + "dependencies": true, "security": true, "observability": true, "testing": true, "delivery": true, +} + +var allowedStandardLevels = map[string]bool{ + "required": true, "recommended": true, "experimental": true, "deprecated": true, +} + type Area struct { Name string `json:"name"` Contract string `json:"contract"` @@ -125,6 +144,80 @@ type qualityExceptionRecord struct { Status string `yaml:"status"` } +type technicalCatalogDocument struct { + SchemaVersion int `yaml:"schema_version"` + Entities map[string]map[string]string `yaml:"entities"` + Relations []technicalRelationDocument `yaml:"relations"` +} + +type technicalEntityDocument struct { + SchemaVersion int `yaml:"schema_version"` + ID string `yaml:"id"` + Type string `yaml:"type"` + Status string `yaml:"status"` +} + +type technicalRelationDocument struct { + ID string `yaml:"id"` + Type string `yaml:"type"` + Source string `yaml:"source"` + Target string `yaml:"target"` + Evidence []string `yaml:"evidence"` +} + +type standardsCatalogDocument struct { + SchemaVersion int `yaml:"schema_version"` + Profiles map[string]string `yaml:"profiles"` + Standards map[string]string `yaml:"standards"` + Exceptions map[string]string `yaml:"exceptions"` +} + +type standardProfileDocument struct { + SchemaVersion int `yaml:"schema_version"` + ID string `yaml:"id"` + Version string `yaml:"version"` + Status string `yaml:"status"` + Extends []string `yaml:"extends"` + Standards []string `yaml:"standards"` +} + +type standardDocument struct { + SchemaVersion int `yaml:"schema_version"` + ID string `yaml:"id"` + Version string `yaml:"version"` + Status string `yaml:"status"` + Category string `yaml:"category"` + Level string `yaml:"level"` + Rules []standardRuleDocument `yaml:"rules"` +} + +type standardRuleDocument struct { + ID string `yaml:"id"` + Requirement string `yaml:"requirement"` + Verification []string `yaml:"verification"` +} + +type standardExceptionDocument struct { + SchemaVersion int `yaml:"schema_version"` + ID string `yaml:"id"` + Standard string `yaml:"standard"` + Scope []string `yaml:"scope"` + Owner string `yaml:"owner"` + Rationale string `yaml:"rationale"` + ResidualRisk string `yaml:"residual_risk"` + Mitigation string `yaml:"mitigation"` + ExpiresOn string `yaml:"expires_on"` + ReentryGate string `yaml:"reentry_gate"` + Status string `yaml:"status"` +} + +type operationsCatalogDocument struct { + SchemaVersion int `yaml:"schema_version"` + Environments map[string]string `yaml:"environments"` + Deployments map[string]string `yaml:"deployments"` + Runbooks map[string]string `yaml:"runbooks"` +} + func Inspect(root string) (Inspection, error) { dir := filepath.Join(root, "engineering") contextData, err := os.ReadFile(filepath.Join(dir, "context.md")) @@ -229,12 +322,271 @@ func Inspect(root string) (Inspection, error) { i.Blockers = append(i.Blockers, "quality area contract must be quality/quality-system.md or legacy quality/quality-model.md") } } + if source, exists := catalog.Areas["technical_catalog"]; exists { + i.Blockers = append(i.Blockers, validateTechnicalCatalog(dir, source.Contract)...) + } + if source, exists := catalog.Areas["standards"]; exists { + i.Blockers = append(i.Blockers, validateStandardsCatalog(dir, source.Contract)...) + } + if source, exists := catalog.Areas["operations"]; exists { + i.Blockers = append(i.Blockers, validateOperationsCatalog(dir, source.Contract)...) + } sort.Slice(i.Areas, func(left, right int) bool { return i.Areas[left].Name < i.Areas[right].Name }) i.Blockers = unique(i.Blockers) sort.Strings(i.Blockers) return i, nil } +func validateTechnicalCatalog(engineeringDir, contract string) []string { + var catalog technicalCatalogDocument + path, blockers := loadEngineeringYAML(engineeringDir, contract, "technical catalog", &catalog) + if len(blockers) != 0 { + return blockers + } + if catalog.SchemaVersion != 1 { + blockers = append(blockers, "technical catalog schema_version must be 1") + } + for category := range catalog.Entities { + if technicalEntityPrefixes[category] == "" { + blockers = append(blockers, "technical catalog has unknown entity category "+category) + } + } + entityIDs := map[string]bool{} + for category, prefix := range technicalEntityPrefixes { + for id, reference := range catalog.Entities[category] { + if !regexp.MustCompile(`^[A-Z][A-Z0-9-]+$`).MatchString(id) || !strings.HasPrefix(id, prefix) { + blockers = append(blockers, fmt.Sprintf("technical catalog %s id %s must start with %s", category, id, prefix)) + } + entityIDs[id] = true + entityPath, refBlockers := resolvedCatalogReference(engineeringDir, filepath.Dir(path), reference, "technical entity "+id) + blockers = append(blockers, refBlockers...) + if len(refBlockers) != 0 { + continue + } + var entity technicalEntityDocument + if err := readYAML(entityPath, &entity); err != nil { + blockers = append(blockers, "technical entity "+id+" is invalid YAML") + continue + } + if entity.SchemaVersion != 1 || entity.ID != id || entity.Type != technicalEntityTypes[category] || strings.TrimSpace(entity.Status) == "" { + blockers = append(blockers, "technical entity "+id+" has invalid schema, identity, type, or status") + } + } + } + seenRelations := map[string]bool{} + for _, relation := range catalog.Relations { + if !regexp.MustCompile(`^REL-[A-Z0-9-]+$`).MatchString(relation.ID) || strings.TrimSpace(relation.Type) == "" { + blockers = append(blockers, "technical relation "+relation.ID+" has invalid identity or type") + } + if seenRelations[relation.ID] { + blockers = append(blockers, "technical relation "+relation.ID+" is duplicated") + } + seenRelations[relation.ID] = true + if !entityIDs[relation.Source] || !entityIDs[relation.Target] { + blockers = append(blockers, "technical relation "+relation.ID+" references an unknown source or target") + } + } + return blockers +} + +func validateStandardsCatalog(engineeringDir, contract string) []string { + var catalog standardsCatalogDocument + path, blockers := loadEngineeringYAML(engineeringDir, contract, "standards catalog", &catalog) + if len(blockers) != 0 { + return blockers + } + if catalog.SchemaVersion != 1 { + blockers = append(blockers, "standards catalog schema_version must be 1") + } + base := filepath.Dir(path) + profiles := map[string]standardProfileDocument{} + for id, reference := range catalog.Profiles { + if !strings.HasPrefix(id, "PROFILE-") { + blockers = append(blockers, "standards profile id "+id+" must start with PROFILE-") + } + profilePath, refBlockers := resolvedCatalogReference(engineeringDir, base, reference, "standards profile "+id) + blockers = append(blockers, refBlockers...) + if len(refBlockers) != 0 { + continue + } + var profile standardProfileDocument + if err := readYAML(profilePath, &profile); err != nil { + blockers = append(blockers, "standards profile "+id+" is invalid YAML") + continue + } + if profile.SchemaVersion != 1 || profile.ID != id || !semanticVersion(profile.Version) { + blockers = append(blockers, "standards profile "+id+" has invalid schema, identity, or semantic version") + } + profiles[id] = profile + } + for id, reference := range catalog.Standards { + if !strings.HasPrefix(id, "STD-") { + blockers = append(blockers, "standard id "+id+" must start with STD-") + } + standardPath, refBlockers := resolvedCatalogReference(engineeringDir, base, reference, "standard "+id) + blockers = append(blockers, refBlockers...) + if len(refBlockers) != 0 { + continue + } + var standard standardDocument + if err := readYAML(standardPath, &standard); err != nil { + blockers = append(blockers, "standard "+id+" is invalid YAML") + continue + } + if standard.SchemaVersion != 1 || standard.ID != id || !semanticVersion(standard.Version) { + blockers = append(blockers, "standard "+id+" has invalid schema, identity, or semantic version") + } + if !allowedStandardCategories[standard.Category] { + blockers = append(blockers, "standard "+id+" has invalid category "+standard.Category) + } + if !allowedStandardLevels[standard.Level] { + blockers = append(blockers, "standard "+id+" has invalid obligation level "+standard.Level) + } + if len(standard.Rules) == 0 { + blockers = append(blockers, "standard "+id+" must declare at least one verifiable rule") + } + seenRules := map[string]bool{} + for _, rule := range standard.Rules { + if !strings.HasPrefix(rule.ID, id+"-R") || strings.TrimSpace(rule.Requirement) == "" || len(rule.Verification) == 0 { + blockers = append(blockers, "standard "+id+" has an invalid rule identity, requirement, or verification") + } + if seenRules[rule.ID] { + blockers = append(blockers, "standard "+id+" rule "+rule.ID+" is duplicated") + } + seenRules[rule.ID] = true + } + } + for id, reference := range catalog.Exceptions { + if !strings.HasPrefix(id, "STDEX-") { + blockers = append(blockers, "standard exception id "+id+" must start with STDEX-") + } + exceptionPath, refBlockers := resolvedCatalogReference(engineeringDir, base, reference, "standard exception "+id) + blockers = append(blockers, refBlockers...) + if len(refBlockers) != 0 { + continue + } + var exception standardExceptionDocument + if err := readYAML(exceptionPath, &exception); err != nil { + blockers = append(blockers, "standard exception "+id+" is invalid YAML") + continue + } + if exception.SchemaVersion != 1 || exception.ID != id || catalog.Standards[exception.Standard] == "" { + blockers = append(blockers, "standard exception "+id+" has invalid schema, identity, or standard reference") + } + if len(exception.Scope) == 0 || strings.TrimSpace(exception.Owner) == "" || strings.TrimSpace(exception.Rationale) == "" || strings.TrimSpace(exception.ResidualRisk) == "" || strings.TrimSpace(exception.Mitigation) == "" || strings.TrimSpace(exception.ReentryGate) == "" { + blockers = append(blockers, "standard exception "+id+" lacks scope, owner, rationale, residual risk, mitigation, or re-entry gate") + } + if !oneOf(exception.Status, "open", "closed") { + blockers = append(blockers, "standard exception "+id+" has invalid status "+exception.Status) + } + expires, dateErr := time.Parse("2006-01-02", exception.ExpiresOn) + if dateErr != nil { + blockers = append(blockers, "standard exception "+id+" expires_on must use YYYY-MM-DD") + } else if exception.Status == "open" && !expires.After(time.Now().UTC().Truncate(24*time.Hour)) { + blockers = append(blockers, "standard exception "+id+" is open but expired") + } + } + for id, profile := range profiles { + for _, parent := range profile.Extends { + if _, exists := catalog.Profiles[parent]; !exists { + blockers = append(blockers, "standards profile "+id+" extends unknown profile "+parent) + } + } + for _, standard := range profile.Standards { + if _, exists := catalog.Standards[standard]; !exists { + blockers = append(blockers, "standards profile "+id+" references unknown standard "+standard) + } + } + } + blockers = append(blockers, validateProfileCycles(profiles)...) + return blockers +} + +func validateOperationsCatalog(engineeringDir, contract string) []string { + var catalog operationsCatalogDocument + path, blockers := loadEngineeringYAML(engineeringDir, contract, "operations catalog", &catalog) + if len(blockers) != 0 { + return blockers + } + if catalog.SchemaVersion != 1 { + blockers = append(blockers, "operations catalog schema_version must be 1") + } + for category, entries := range map[string]map[string]string{"environment": catalog.Environments, "deployment": catalog.Deployments, "runbook": catalog.Runbooks} { + for id, reference := range entries { + blockers = append(blockers, validateCatalogReference(engineeringDir, filepath.Dir(path), reference, category+" "+id)...) + } + } + return blockers +} + +func loadEngineeringYAML(engineeringDir, contract, label string, target any) (string, []string) { + path, blockers := resolvedCatalogReference(engineeringDir, engineeringDir, contract, label) + if len(blockers) != 0 { + return path, blockers + } + if err := readYAML(path, target); err != nil { + return path, []string{label + " is invalid YAML"} + } + return path, nil +} + +func validateCatalogReference(engineeringDir, baseDir, reference, label string) []string { + _, blockers := resolvedCatalogReference(engineeringDir, baseDir, reference, label) + return blockers +} + +func resolvedCatalogReference(engineeringDir, baseDir, reference, label string) (string, []string) { + reference = strings.TrimSpace(reference) + path := filepath.Clean(filepath.Join(baseDir, filepath.FromSlash(reference))) + relative, err := filepath.Rel(engineeringDir, path) + if reference == "" || filepath.IsAbs(filepath.FromSlash(reference)) || err != nil || relative == ".." || strings.HasPrefix(relative, ".."+string(filepath.Separator)) { + return path, []string{label + " reference escapes engineering"} + } + info, statErr := os.Stat(path) + if statErr != nil || info.IsDir() { + return path, []string{label + " reference " + reference + " is missing"} + } + return path, nil +} + +func readYAML(path string, target any) error { + data, err := os.ReadFile(path) + if err != nil { + return err + } + return yaml.Unmarshal(data, target) +} + +func semanticVersion(version string) bool { + return regexp.MustCompile(`^[0-9]+\.[0-9]+\.[0-9]+(?:[-+][0-9A-Za-z.-]+)?$`).MatchString(version) +} + +func validateProfileCycles(profiles map[string]standardProfileDocument) []string { + state := map[string]int{} + var blockers []string + var visit func(string) + visit = func(id string) { + if state[id] == 1 { + blockers = append(blockers, "standards profiles contain an inheritance cycle at "+id) + return + } + if state[id] == 2 { + return + } + state[id] = 1 + for _, parent := range profiles[id].Extends { + if _, exists := profiles[parent]; exists { + visit(parent) + } + } + state[id] = 2 + } + for id := range profiles { + visit(id) + } + return blockers +} + func inspectQualityExceptions(engineeringDir string, blockers []string) ([]string, map[string]string, []string) { data, err := os.ReadFile(filepath.Join(engineeringDir, "quality", "quality-system.yaml")) if err != nil { diff --git a/internal/engineeringsystem/system_test.go b/internal/engineeringsystem/system_test.go index 163ab16..dd6a41f 100644 --- a/internal/engineeringsystem/system_test.go +++ b/internal/engineeringsystem/system_test.go @@ -92,6 +92,71 @@ func TestInspectRejectsCatalogIdentityMismatch(t *testing.T) { } } +func TestInspectValidatesTechnicalStandardsAndOperationsCatalogs(t *testing.T) { + root := t.TempDir() + engineering := filepath.Join(root, "engineering") + files := map[string]string{ + "context.md": "---\nid: ENGSYS-TEST-001\nstatus: draft\nversion: 1.0.0\norigin_mode: generate\n---\n", + "engineering-system.md": "| Field | Value |\n| --- | --- |\n| ID | `ENGSYS-TEST-001` |\n| Status | `draft` |\n| Version | `1.0.0` |\n", + "engineering-system.yaml": "schema_version: 1\nid: ENGSYS-TEST-001\nstatus: draft\nversion: 1.0.0\norigin_mode: generate\nscope: product\nareas:\n technical_catalog: {contract: catalog/catalog.yaml, maturity: baseline, evidence: []}\n standards: {contract: standards/standards.yaml, maturity: baseline, evidence: []}\n operations: {contract: operations/operations.yaml, maturity: baseline, evidence: []}\n", + "catalog/catalog.yaml": "schema_version: 1\nentities:\n systems: {SYS-ONE: systems/system.yaml}\n applications: {APP-ONE: applications/app.yaml}\n components: {}\n repositories: {}\n data_stores: {}\n interfaces: {}\n deployments: {}\nrelations:\n - {id: REL-SYSTEM-APP, type: contains, source: SYS-ONE, target: APP-ONE, evidence: []}\n", + "catalog/systems/system.yaml": "schema_version: 1\nid: SYS-ONE\ntype: system\nstatus: draft\n", + "catalog/applications/app.yaml": "schema_version: 1\nid: APP-ONE\ntype: application\nstatus: draft\n", + "standards/standards.yaml": "schema_version: 1\nprofiles: {PROFILE-DEFAULT: profiles/default.yaml}\nstandards: {STD-API-001: catalog/api.yaml}\nexceptions: {STDEX-001: exceptions/api.yaml}\n", + "standards/profiles/default.yaml": "schema_version: 1\nid: PROFILE-DEFAULT\nversion: 1.0.0\nstatus: draft\nextends: []\nstandards: [STD-API-001]\n", + "standards/catalog/api.yaml": "schema_version: 1\nid: STD-API-001\nversion: 1.0.0\nstatus: active\ncategory: api\nlevel: required\nrules:\n - id: STD-API-001-R01\n requirement: APIs publish a contract\n verification: [schema]\n", + "standards/exceptions/api.yaml": "schema_version: 1\nid: STDEX-001\nstandard: STD-API-001\nscope: [APP-ONE]\nowner: team\nrationale: migration\nresidual_risk: accepted\nmitigation: monitor\nexpires_on: 2099-01-01\nreentry_gate: tests pass\nstatus: open\n", + "operations/operations.yaml": "schema_version: 1\nenvironments: {ENV-PROD: environments/prod.yaml}\ndeployments: {}\nrunbooks: {}\n", + "operations/environments/prod.yaml": "schema_version: 1\nid: ENV-PROD\n", + } + for name, body := range files { + path := filepath.Join(engineering, filepath.FromSlash(name)) + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, []byte(body), 0o644); err != nil { + t.Fatal(err) + } + } + inspection, err := Inspect(root) + if err != nil || len(inspection.Blockers) != 0 { + t.Fatalf("inspection=%+v err=%v", inspection, err) + } +} + +func TestInspectRejectsInvalidStandardInheritanceAndMissingCatalogRecord(t *testing.T) { + root := t.TempDir() + engineering := filepath.Join(root, "engineering") + files := map[string]string{ + "context.md": "---\nid: ENGSYS-TEST-001\nstatus: draft\nversion: 1.0.0\norigin_mode: generate\n---\n", + "engineering-system.md": "| Field | Value |\n| --- | --- |\n| ID | `ENGSYS-TEST-001` |\n| Status | `draft` |\n| Version | `1.0.0` |\n", + "engineering-system.yaml": "schema_version: 1\nid: ENGSYS-TEST-001\nstatus: draft\nversion: 1.0.0\norigin_mode: generate\nscope: product\nareas:\n technical_catalog: {contract: catalog/catalog.yaml, maturity: baseline, evidence: []}\n standards: {contract: standards/standards.yaml, maturity: baseline, evidence: []}\n", + "catalog/catalog.yaml": "schema_version: 1\nentities:\n systems: {BAD-ID: systems/missing.yaml}\nrelations:\n - {id: REL-BROKEN, type: depends_on, source: SYS-MISSING, target: APP-MISSING}\n", + "standards/standards.yaml": "schema_version: 1\nprofiles: {PROFILE-A: profiles/a.yaml, PROFILE-B: profiles/b.yaml}\nstandards: {}\nexceptions: {}\n", + "standards/profiles/a.yaml": "schema_version: 1\nid: PROFILE-A\nversion: 1.0.0\nstatus: draft\nextends: [PROFILE-B]\nstandards: []\n", + "standards/profiles/b.yaml": "schema_version: 1\nid: PROFILE-B\nversion: 1.0.0\nstatus: draft\nextends: [PROFILE-A]\nstandards: []\n", + } + for name, body := range files { + path := filepath.Join(engineering, filepath.FromSlash(name)) + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, []byte(body), 0o644); err != nil { + t.Fatal(err) + } + } + inspection, err := Inspect(root) + if err != nil { + t.Fatal(err) + } + joined := strings.Join(inspection.Blockers, "\n") + for _, expected := range []string{"must start with SYS-", "is missing", "unknown source or target", "inheritance cycle"} { + if !strings.Contains(joined, expected) { + t.Fatalf("expected %q in blockers=%v", expected, inspection.Blockers) + } + } +} + func TestMigrateAddsSchemaVersionWithoutChangingOtherFields(t *testing.T) { root := t.TempDir() dir := filepath.Join(root, "engineering") diff --git a/internal/install/install_test.go b/internal/install/install_test.go index 73e36a2..cd66535 100644 --- a/internal/install/install_test.go +++ b/internal/install/install_test.go @@ -25,8 +25,8 @@ func TestInitShipsLeanReadmeSurfaceAndUpgradePreservesAdopterReadmes(t *testing. }); err != nil { t.Fatal(err) } - if readmes != 18 { - t.Fatalf("initialized product has %d READMEs, want 18", readmes) + if readmes != 22 { + t.Fatalf("initialized product has %d READMEs, want 22", readmes) } obsolete := filepath.Join(product, "knowledge", "prompts", "README.md") if _, err := os.Stat(obsolete); !os.IsNotExist(err) { @@ -264,6 +264,38 @@ func TestInitKeepsRuntimeAndDispatchersOutsideRepository(t *testing.T) { } } +func TestInitShipsEngineeringCatalogRootsForEveryAgentTarget(t *testing.T) { + t.Setenv("SPEC_FRAMEWORK_CACHE", filepath.Join(t.TempDir(), "cache")) + t.Setenv("SPEC_FRAMEWORK_AGENT_HOME", filepath.Join(t.TempDir(), "agents")) + target := filepath.Join(t.TempDir(), "product") + if _, err := Init(Options{Target: target, Version: "test", Agents: []Agent{Codex, Cursor, Claude}}); err != nil { + t.Fatal(err) + } + for _, file := range []string{ + "product/engineering/architecture/topology.yaml", + "product/engineering/catalog/catalog.yaml", + "product/engineering/standards/standards.yaml", + "product/engineering/standards/profiles/product-default.yaml", + "product/engineering/operations/operations.yaml", + "product/engineering/evidence/inventory.md", + } { + if _, err := os.Stat(filepath.Join(target, filepath.FromSlash(file))); err != nil { + t.Errorf("missing %s: %v", file, err) + } + } + owned := filepath.Join(target, "product", "engineering", "catalog", "catalog.yaml") + if err := os.WriteFile(owned, []byte("adopter-owned catalog\n"), 0o644); err != nil { + t.Fatal(err) + } + if _, err := Upgrade(Options{Target: target, Version: "test", Agents: []Agent{Codex, Cursor, Claude}}); err != nil { + t.Fatal(err) + } + data, err := os.ReadFile(owned) + if err != nil || string(data) != "adopter-owned catalog\n" { + t.Fatalf("upgrade changed adopter engineering catalog: %q %v", data, err) + } +} + func TestInitFromExistingDocumentsCreatesImportRun(t *testing.T) { t.Setenv("SPEC_FRAMEWORK_CACHE", filepath.Join(t.TempDir(), "cache")) t.Setenv("SPEC_FRAMEWORK_AGENT_HOME", filepath.Join(t.TempDir(), "agents")) diff --git a/internal/runtimeassets/runtime.go b/internal/runtimeassets/runtime.go index 1404725..975334d 100644 --- a/internal/runtimeassets/runtime.go +++ b/internal/runtimeassets/runtime.go @@ -76,16 +76,17 @@ func Ensure(version string) (string, error) { } defer os.RemoveAll(tmp) assets := map[string]string{ - "FRAMEWORK.md": "FRAMEWORK.md", - "docs/artifact-registry-modules.md": "docs/artifact-registry-modules.md", - "docs/execution-runtime.md": "docs/execution-runtime.md", - "docs/engineering-systems.md": "docs/engineering-systems.md", - "docs/lifecycle-and-approvals.md": "docs/lifecycle-and-approvals.md", - "framework/AGENTS.framework.md": "AGENTS.framework.md", - "framework/delivery-closure.md": "delivery-closure.md", - "framework/init": "init", - "framework/skills": "skills", - "examples/events": "examples/events", + "FRAMEWORK.md": "FRAMEWORK.md", + "docs/artifact-registry-modules.md": "docs/artifact-registry-modules.md", + "docs/execution-runtime.md": "docs/execution-runtime.md", + "docs/engineering-systems.md": "docs/engineering-systems.md", + "docs/engineering-catalog-and-standards.md": "docs/engineering-catalog-and-standards.md", + "docs/lifecycle-and-approvals.md": "docs/lifecycle-and-approvals.md", + "framework/AGENTS.framework.md": "AGENTS.framework.md", + "framework/delivery-closure.md": "delivery-closure.md", + "framework/init": "init", + "framework/skills": "skills", + "examples/events": "examples/events", } for source, dest := range assets { if err := copyTree(source, filepath.Join(tmp, dest)); err != nil { diff --git a/internal/runtimeassets/runtime_test.go b/internal/runtimeassets/runtime_test.go index 8bbb58d..9e98ad7 100644 --- a/internal/runtimeassets/runtime_test.go +++ b/internal/runtimeassets/runtime_test.go @@ -43,6 +43,9 @@ func TestEnsureMaterializesVersionedAssets(t *testing.T) { t.Fatal(err) } } + if _, err := os.Stat(filepath.Join(root, "docs", "engineering-catalog-and-standards.md")); err != nil { + t.Fatal(err) + } data, err := os.ReadFile(filepath.Join(root, "AGENTS.framework.md")) if err != nil || !strings.Contains(string(data), "Common Agent Rules") { t.Fatalf("runtime common agent rules missing: %v", err) diff --git a/starter/README.md b/starter/README.md index 740f02c..3605e2a 100644 --- a/starter/README.md +++ b/starter/README.md @@ -24,7 +24,7 @@ Framework assets live in the versioned user cache and are resolved from `product | `product/audits/` | Product validation reports, readiness checks, and security/threat tracking. | | `product/releases/` | Release planning and release evidence. | | `product/design/` | Product design references. | -| `product/engineering/` | Product engineering notes, architecture, runbooks, and product-specific gates. | +| `product/engineering/` | Scalable technical entity graph, architecture, versioned standards, quality, operations, evidence, decisions, and product-specific gates. | ## First Product Steps diff --git a/starter/product/README.md b/starter/product/README.md index 201f68c..e38070d 100644 --- a/starter/product/README.md +++ b/starter/product/README.md @@ -16,7 +16,7 @@ Product-owned specifications, decisions, plans, and evidence live here. Framewor | `domains/` | Domains, goals, features, use cases, and delivery artifacts. | | `knowledge/` | Product rules, decisions, conventions, and imports. | | `design/` | Shared and use-case design sources. | -| `engineering/` | Product Engineering System and operational evidence. | +| `engineering/` | Product Engineering System, technical entity graph, standards, quality, operations, and evidence. | | `audits/` | Readiness, security, QA, and consistency findings. | `BOOTSTRAP.md` and `context.md` are authoritative for sequencing; this README is navigation only. diff --git a/starter/product/engineering/README.md b/starter/product/engineering/README.md index 05a86c3..2d4b09c 100644 --- a/starter/product/engineering/README.md +++ b/starter/product/engineering/README.md @@ -6,6 +6,9 @@ This area owns the versioned Engineering System: architecture, standards, qualit - Read `context.md`, `engineering-system.md`, and `engineering-system.yaml`. - Base maturity claims on real code and operational evidence. +- Model systems, applications, components, repositories, interfaces, data stores, and deployments as stable graph entities under `catalog/`. +- Compose verifiable rules through versioned profiles under `standards/`; never weaken inherited required rules without a governed exception. +- Materialize optional entity, standard, environment, deployment, and runbook records only when evidence or explicit hypotheses require them. - Keep product gates in `../knowledge/conventions/gates.md`. - Inspect with `spec-framework engineering-system inspect` and validate with `spec-framework engineering-system validate`. diff --git a/starter/product/engineering/architecture/topology.yaml b/starter/product/engineering/architecture/topology.yaml new file mode 100644 index 0000000..3042751 --- /dev/null +++ b/starter/product/engineering/architecture/topology.yaml @@ -0,0 +1,6 @@ +schema_version: 1 +systems: [] +applications: [] +components: [] +repositories: [] +relations: [] diff --git a/starter/product/engineering/catalog/README.md b/starter/product/engineering/catalog/README.md new file mode 100644 index 0000000..ddcf801 --- /dev/null +++ b/starter/product/engineering/catalog/README.md @@ -0,0 +1,9 @@ +# Technical Entity Catalog + +`catalog.yaml` indexes systems, applications, components, repositories, data +stores, interfaces, and deployments by stable ID. Create records only from +observed evidence or explicit hypotheses. Folder position does not define graph +relations. + +Each relation uses a stable `REL-*` ID, an extensible type, and existing source +and target entity IDs. The graph contract does not infer relations from paths. diff --git a/starter/product/engineering/catalog/catalog.yaml b/starter/product/engineering/catalog/catalog.yaml new file mode 100644 index 0000000..d0c294a --- /dev/null +++ b/starter/product/engineering/catalog/catalog.yaml @@ -0,0 +1,10 @@ +schema_version: 1 +entities: + systems: {} + applications: {} + components: {} + repositories: {} + data_stores: {} + interfaces: {} + deployments: {} +relations: [] diff --git a/starter/product/engineering/context.md b/starter/product/engineering/context.md index d0a08f7..ed62760 100644 --- a/starter/product/engineering/context.md +++ b/starter/product/engineering/context.md @@ -18,10 +18,15 @@ This placeholder does not describe a real product architecture. Replace it from - [engineering-system.yaml](engineering-system.yaml) - [architecture/system-context.md](architecture/system-context.md) - [architecture/modules.md](architecture/modules.md) +- [architecture/topology.yaml](architecture/topology.yaml) +- [catalog/catalog.yaml](catalog/catalog.yaml) +- [standards/standards.yaml](standards/standards.yaml) - [quality/quality-model.md](quality/quality-model.md) - [quality/quality-system.md](quality/quality-system.md) - [quality/quality-system.yaml](quality/quality-system.yaml) - [quality/test-strategy.md](quality/test-strategy.md) +- [operations/operations.yaml](operations/operations.yaml) +- [evidence/inventory.md](evidence/inventory.md) ## Handoff diff --git a/starter/product/engineering/engineering-system.md b/starter/product/engineering/engineering-system.md index 7a33e9b..dc0f288 100644 --- a/starter/product/engineering/engineering-system.md +++ b/starter/product/engineering/engineering-system.md @@ -22,7 +22,11 @@ Draft contract. Inspect the adopter's real repository before defining system bou | --- | --- | --- | --- | | System context | [architecture/system-context.md](architecture/system-context.md) | Not configured | `baseline` | | Modules | [architecture/modules.md](architecture/modules.md) | Not configured | `baseline` | +| Technical catalog | [catalog/catalog.yaml](catalog/catalog.yaml) | Not configured | `baseline` | +| Standards | [standards/standards.yaml](standards/standards.yaml) | Not configured | `baseline` | | Quality | [quality/quality-system.md](quality/quality-system.md) | Not configured | `baseline` | +| Operations | [operations/operations.yaml](operations/operations.yaml) | Not configured | `baseline` | +| Evidence | [evidence/inventory.md](evidence/inventory.md) | Not configured | `baseline` | No maturity beyond placeholder baseline is claimed. diff --git a/starter/product/engineering/engineering-system.yaml b/starter/product/engineering/engineering-system.yaml index a505fd1..2e504e7 100644 --- a/starter/product/engineering/engineering-system.yaml +++ b/starter/product/engineering/engineering-system.yaml @@ -13,10 +13,26 @@ areas: contract: architecture/modules.md maturity: baseline evidence: [] + technical_catalog: + contract: catalog/catalog.yaml + maturity: baseline + evidence: [] + standards: + contract: standards/standards.yaml + maturity: baseline + evidence: [] quality: contract: quality/quality-system.md maturity: baseline evidence: [] + operations: + contract: operations/operations.yaml + maturity: baseline + evidence: [] + evidence: + contract: evidence/inventory.md + maturity: baseline + evidence: [] decisions: [] standards: [] fitness_functions: [] diff --git a/starter/product/engineering/evidence/README.md b/starter/product/engineering/evidence/README.md new file mode 100644 index 0000000..0184afd --- /dev/null +++ b/starter/product/engineering/evidence/README.md @@ -0,0 +1,4 @@ +# Engineering Evidence + +Index evidence used by maturity claims without copying volatile command or CI +output into stable contracts. diff --git a/starter/product/engineering/evidence/inventory.md b/starter/product/engineering/evidence/inventory.md new file mode 100644 index 0000000..b33a5a4 --- /dev/null +++ b/starter/product/engineering/evidence/inventory.md @@ -0,0 +1,4 @@ +# Engineering Evidence Inventory + +| Evidence ID | Scope | Kind | Source | Observed on | Notes | +| --- | --- | --- | --- | --- | --- | diff --git a/starter/product/engineering/operations/README.md b/starter/product/engineering/operations/README.md new file mode 100644 index 0000000..4808200 --- /dev/null +++ b/starter/product/engineering/operations/README.md @@ -0,0 +1,4 @@ +# Engineering Operations + +Index environments, deployments, and runbooks in `operations.yaml`. Create +records on demand and link them to stable catalog entity IDs. diff --git a/starter/product/engineering/operations/operations.yaml b/starter/product/engineering/operations/operations.yaml new file mode 100644 index 0000000..220e4b1 --- /dev/null +++ b/starter/product/engineering/operations/operations.yaml @@ -0,0 +1,4 @@ +schema_version: 1 +environments: {} +deployments: {} +runbooks: {} diff --git a/starter/product/engineering/standards/README.md b/starter/product/engineering/standards/README.md new file mode 100644 index 0000000..81333ee --- /dev/null +++ b/starter/product/engineering/standards/README.md @@ -0,0 +1,5 @@ +# Engineering Standards + +`standards.yaml` indexes versioned profiles, standards, and governed +exceptions. Standards apply by entity type, capability, or explicit assignment. +Required inherited standards cannot be silently weakened. diff --git a/starter/product/engineering/standards/profiles/product-default.yaml b/starter/product/engineering/standards/profiles/product-default.yaml new file mode 100644 index 0000000..72e9253 --- /dev/null +++ b/starter/product/engineering/standards/profiles/product-default.yaml @@ -0,0 +1,6 @@ +schema_version: 1 +id: PROFILE-PRODUCT-DEFAULT +version: 0.1.0 +status: draft +extends: [] +standards: [] diff --git a/starter/product/engineering/standards/standards.yaml b/starter/product/engineering/standards/standards.yaml new file mode 100644 index 0000000..0a91076 --- /dev/null +++ b/starter/product/engineering/standards/standards.yaml @@ -0,0 +1,5 @@ +schema_version: 1 +profiles: + PROFILE-PRODUCT-DEFAULT: profiles/product-default.yaml +standards: {} +exceptions: {} From ab72ba4d3000fd72a60b04ddeea5bddfe5669244 Mon Sep 17 00:00:00 2001 From: JonatasFreireDev Date: Sat, 18 Jul 2026 12:46:19 -0300 Subject: [PATCH 8/9] feat(engineering): split baseline into specialist skills --- FRAMEWORK.md | 5 +- README.md | 4 +- docs/artifact-registry-modules.md | 4 +- docs/engineering-catalog-and-standards.md | 41 +++++++----- docs/engineering-systems.md | 9 ++- framework/AGENTS.framework.md | 2 + framework/adoption.md | 2 +- framework/init/bootstrap.json | 8 +-- framework/install.md | 2 +- framework/skills/README.md | 6 +- .../references/template-authoring.md | 3 +- .../skills/engineering-evidence/SKILL.md | 60 ++++++++++++++++++ .../engineering-evidence/agents/openai.yaml | 4 ++ .../assets/coverage-map-template.yaml | 4 ++ .../assets/evidence-catalog-template.yaml | 5 ++ .../assets/evidence-gap-report-template.md | 5 ++ .../assets/evidence-inventory-template.md | 0 .../assets/evidence-record-template.yaml | 9 +++ .../assets/maturity-assessment-template.md | 9 +++ .../inventory-engineering-evidence.ps1 | 1 + .../scripts/inventory-engineering-evidence.sh | 4 +- .../skills/engineering-orchestrator/SKILL.md | 63 +++++++++++++++++++ .../agents/openai.yaml | 4 ++ ...engineering-baseline-handoff-template.json | 12 ++++ .../engineering-readiness-report-template.md | 23 +++++++ .../skills/engineering-standards/SKILL.md | 61 ++++++++++++++++++ .../engineering-standards/agents/openai.yaml | 4 ++ .../assets/conformance-report-template.md | 9 +++ .../assets/profile-resolution-template.yaml | 6 ++ .../assets/standard-exception-template.yaml | 0 .../assets/standard-profile-template.yaml | 0 .../assets/standard-template.md | 0 .../assets/standard-template.yaml | 0 .../assets/standards-catalog-template.yaml | 1 + .../assets/standards-overview-template.md | 17 +++++ framework/skills/engineering-system/SKILL.md | 46 +++++++------- .../engineering-system/agents/openai.yaml | 4 +- .../assets/engineering-system-template.md | 20 +++--- .../assets/engineering-system-template.yaml | 7 +++ framework/skills/framework-guide/SKILL.md | 5 +- framework/skills/operations-baseline/SKILL.md | 60 ++++++++++++++++++ .../operations-baseline/agents/openai.yaml | 4 ++ .../assets/deployment-template.yaml | 12 ++++ .../assets/environment-template.yaml | 9 +++ .../assets/operations-catalog-template.yaml | 1 + .../assets/operations-overview-template.md | 15 +++++ .../assets/release-strategy-template.md | 13 ++++ .../assets/runbook-template.md | 17 +++++ .../assets/service-level-template.yaml | 9 +++ .../skills/product-orchestrator/SKILL.md | 4 +- framework/skills/technical-landscape/SKILL.md | 61 ++++++++++++++++++ .../technical-landscape/agents/openai.yaml | 4 ++ .../assets/boundary-assessment-template.md | 17 +++++ .../assets/module-map-template.md | 9 +++ .../assets/system-context-template.md | 15 +++++ .../assets/technical-catalog-template.yaml | 1 + .../assets/technical-entity-template.md | 0 .../assets/technical-entity-template.yaml | 0 .../assets/technical-landscape-template.md | 23 +++++++ .../assets/topology-template.yaml | 7 +++ .../scripts/inventory-technical-landscape.ps1 | 19 ++++++ .../scripts/inventory-technical-landscape.sh | 14 +++++ framework/validators/README.md | 2 +- internal/cli/engineering_system.go | 6 +- internal/engineeringsystem/system.go | 43 ++++++++++--- internal/engineeringsystem/system_test.go | 4 +- internal/install/install_test.go | 17 ++++- internal/runtimeassets/runtime_test.go | 2 +- starter/AGENTS.md | 2 + starter/README.md | 2 +- starter/product/engineering/README.md | 4 +- .../engineering/architecture/README.md | 3 + .../engineering/architecture/topology.yaml | 1 + starter/product/engineering/catalog/README.md | 2 + .../product/engineering/catalog/catalog.yaml | 1 + starter/product/engineering/context.md | 2 +- .../product/engineering/engineering-system.md | 20 +++--- .../engineering/engineering-system.yaml | 7 +++ .../product/engineering/evidence/README.md | 2 + .../product/engineering/evidence/inventory.md | 2 + .../product/engineering/operations/README.md | 2 + .../engineering/operations/operations.yaml | 1 + .../engineering/quality/quality-system.md | 2 +- .../product/engineering/standards/README.md | 2 + .../engineering/standards/standards.yaml | 1 + 85 files changed, 812 insertions(+), 101 deletions(-) create mode 100644 framework/skills/engineering-evidence/SKILL.md create mode 100644 framework/skills/engineering-evidence/agents/openai.yaml create mode 100644 framework/skills/engineering-evidence/assets/coverage-map-template.yaml create mode 100644 framework/skills/engineering-evidence/assets/evidence-catalog-template.yaml create mode 100644 framework/skills/engineering-evidence/assets/evidence-gap-report-template.md rename framework/skills/{engineering-system => engineering-evidence}/assets/evidence-inventory-template.md (100%) create mode 100644 framework/skills/engineering-evidence/assets/evidence-record-template.yaml create mode 100644 framework/skills/engineering-evidence/assets/maturity-assessment-template.md rename framework/skills/{engineering-system => engineering-evidence}/scripts/inventory-engineering-evidence.ps1 (98%) rename framework/skills/{engineering-system => engineering-evidence}/scripts/inventory-engineering-evidence.sh (96%) create mode 100644 framework/skills/engineering-orchestrator/SKILL.md create mode 100644 framework/skills/engineering-orchestrator/agents/openai.yaml create mode 100644 framework/skills/engineering-orchestrator/assets/engineering-baseline-handoff-template.json create mode 100644 framework/skills/engineering-orchestrator/assets/engineering-readiness-report-template.md create mode 100644 framework/skills/engineering-standards/SKILL.md create mode 100644 framework/skills/engineering-standards/agents/openai.yaml create mode 100644 framework/skills/engineering-standards/assets/conformance-report-template.md create mode 100644 framework/skills/engineering-standards/assets/profile-resolution-template.yaml rename framework/skills/{engineering-system => engineering-standards}/assets/standard-exception-template.yaml (100%) rename framework/skills/{engineering-system => engineering-standards}/assets/standard-profile-template.yaml (100%) rename framework/skills/{engineering-system => engineering-standards}/assets/standard-template.md (100%) rename framework/skills/{engineering-system => engineering-standards}/assets/standard-template.yaml (100%) rename framework/skills/{engineering-system => engineering-standards}/assets/standards-catalog-template.yaml (76%) create mode 100644 framework/skills/engineering-standards/assets/standards-overview-template.md create mode 100644 framework/skills/operations-baseline/SKILL.md create mode 100644 framework/skills/operations-baseline/agents/openai.yaml create mode 100644 framework/skills/operations-baseline/assets/deployment-template.yaml create mode 100644 framework/skills/operations-baseline/assets/environment-template.yaml rename framework/skills/{engineering-system => operations-baseline}/assets/operations-catalog-template.yaml (65%) create mode 100644 framework/skills/operations-baseline/assets/operations-overview-template.md create mode 100644 framework/skills/operations-baseline/assets/release-strategy-template.md create mode 100644 framework/skills/operations-baseline/assets/runbook-template.md create mode 100644 framework/skills/operations-baseline/assets/service-level-template.yaml create mode 100644 framework/skills/technical-landscape/SKILL.md create mode 100644 framework/skills/technical-landscape/agents/openai.yaml create mode 100644 framework/skills/technical-landscape/assets/boundary-assessment-template.md create mode 100644 framework/skills/technical-landscape/assets/module-map-template.md create mode 100644 framework/skills/technical-landscape/assets/system-context-template.md rename framework/skills/{engineering-system => technical-landscape}/assets/technical-catalog-template.yaml (90%) rename framework/skills/{engineering-system => technical-landscape}/assets/technical-entity-template.md (100%) rename framework/skills/{engineering-system => technical-landscape}/assets/technical-entity-template.yaml (100%) create mode 100644 framework/skills/technical-landscape/assets/technical-landscape-template.md create mode 100644 framework/skills/technical-landscape/assets/topology-template.yaml create mode 100644 framework/skills/technical-landscape/scripts/inventory-technical-landscape.ps1 create mode 100644 framework/skills/technical-landscape/scripts/inventory-technical-landscape.sh diff --git a/FRAMEWORK.md b/FRAMEWORK.md index 23d95cc..8d70100 100644 --- a/FRAMEWORK.md +++ b/FRAMEWORK.md @@ -146,6 +146,8 @@ The Design System is an optional shared product artifact for products with recur The optional Engineering System versions stable architecture, module and data ownership, integrations, standards, quality attributes, operations, and evidence under `engineering/`. It models systems, applications, components, repositories, interfaces, data stores, and deployments as independently identified graph entities, so one product may span monorepos, polyrepos, shared components, and multiple deployables without encoding those relations in folder position. Its detailed contracts live in [`docs/engineering-systems.md`](docs/engineering-systems.md) and [`docs/engineering-catalog-and-standards.md`](docs/engineering-catalog-and-standards.md). Specification and approved `DEC-*` records remain authoritative when contracts conflict. +`engineering-orchestrator` governs completeness and sequencing across `technical-landscape`, `engineering-standards`, `operations-baseline`, `engineering-evidence`, and final `engineering-system` consolidation. Each specialist owns its canonical contracts; the orchestrator never authors them, and `engineering-system` aggregates and versions them without absorbing their ownership. The baseline stops at human approval before Domain modeling or delivery-specific Technical Discovery. + Engineering standards are versioned, verifiable rules composed through profiles and applied by entity type, capability, or explicit assignment. A more specific contract may add constraints but cannot silently weaken an inherited required standard. Divergence requires a scoped, governed exception or approved decision. ### Engineering Quality System @@ -561,7 +563,7 @@ Definition and planning skills follow the shared Discovery and Challenge contrac | --- | --- | | Foundation | Problem Discovery, Vision, Strategy, Domain Architect, and User Goal define evidenced product direction and boundaries. | | Product Design | Journey, Feature, Use Case, UX/UI, UX Review, and Design System define value slices and verifiable experience. | -| Specification and planning | Specification, Engineering System, Technical Discovery, Engineering Proposal, Engineering Review, Implementation Planner, Execution Graph, and Task Generator turn approved intent into executable contracts. | +| Specification and planning | Specification, Engineering Orchestrator, Technical Landscape, Engineering Standards, Operations Baseline, Engineering Evidence, Engineering System, Technical Discovery, Engineering Proposal, Engineering Review, Implementation Planner, Execution Graph, and Task Generator turn approved intent into executable contracts. | | Engineering and validation | Code Runner implements one approved task; QA, Code Review, and Security Review independently verify it; Threat Modeler maintains proactive security context; Commit Crafter and PR Finalizer prepare verified delivery without merging. | | Audit and evolution | Gap, Conflict, Dependency, Impact, Evolution, Documentation, and Product Historian skills inspect health and route governed change. | @@ -575,6 +577,7 @@ Orchestrators do not create primary artifacts. They control flow, order, gates, | --- | --- | | Framework Guide | Translates intent and current CLI state into the smallest safe route; it does not author artifacts or approve work. | | Product | Coordinates the approved Foundation sequence and stops at each human gate. | +| Engineering | Routes the shared technical landscape, standards, operations, evidence, aggregate Engineering System, and human approval gate without authoring specialist contracts. | | Domain Evolution | Compares evidenced feature candidates and requires explicit human selection before handoff. | | Existing Product Import | Moves sources through inventory, per-source traceability, conflicts, reviewed mappings, explicit materialization, and draft artifacts without treating sources as truth. | | New Feature | Drives an approved feature through Use Cases, Specification, Design, engineering gates, Plan, Graph, and Tasks. | diff --git a/README.md b/README.md index edbda50..8ff6c59 100644 --- a/README.md +++ b/README.md @@ -75,10 +75,10 @@ spec-framework uninstall --purge --yes | --- | --- | | **Specification** | The central contract: flow, UI, APIs, data, permissions, analytics, security, and acceptance — written before any code. | | **Design flow** | Specification → `design.md` (origin `generate`/`evolve`/`adopt`, versioned visual sources) → independent UX Review → human gate. | -| **Engineering flow** | Technical Discovery → Engineering Proposal → independent Engineering Review → Implementation Plan. | +| **Engineering flow** | Shared baseline: Engineering Orchestrator → Technical Landscape → Standards → Operations → Evidence → Engineering System approval. Delivery: Technical Discovery → Engineering Proposal → independent Engineering Review → Implementation Plan. | | **Execution Graph** | Complete vertical task contracts as a DAG with explicit `writeScope`; parallel work never overlaps write paths. | | **Approval gates** | `draft → proposed → approved → in_progress → implemented → validated → released`, each transition mechanically checked (content hashes, same-diff QA + Code Review, staleness detection). | -| **Shared systems** | Optional versioned Design System (`design/system/`) and scalable Engineering System (`engineering/`) with graph catalogs, standards, quality, operations, and evidence — pinned per delivery, never self-approving. | +| **Shared systems** | Optional versioned Design System (`design/system/`) and scalable Engineering System (`engineering/`) whose graph, standards, operations, evidence, aggregate, and quality contracts have explicit specialist owners — pinned per delivery, never self-approving. | ## CLI At a Glance diff --git a/docs/artifact-registry-modules.md b/docs/artifact-registry-modules.md index 21fdbbe..7032ded 100644 --- a/docs/artifact-registry-modules.md +++ b/docs/artifact-registry-modules.md @@ -25,7 +25,7 @@ This document is the canonical maintenance map for composing the initial artifac | `foundation-strategy` | Strategy | O starting point preserva apenas estratégia | | `delivery-domain` | Domain, Goal, Feature, Use Case | O produto está pronto para modelar entrega | | `delivery-design` | Design System, Design, componentes, padrões | O produto possui interface ou sistema visual | -| `delivery-engineering` | Engineering System, Quality System, Engineering Proposal, Review | O produto precisa de contratos técnicos compartilhados | +| `delivery-engineering` | Engineering System aggregate, technical landscape, standards, operations, evidence, Quality System, Engineering Proposal, Review | O produto precisa de contratos técnicos compartilhados | | `delivery-validation` | Tests, QA Evidence, Security Review, Audit | A entrega avançou para validação | | `delivery-execution` | Execution Graph, Task Set, Task | A especificação está pronta para implementação | | `governance` | Decisions, approvals, releases, audits | Sempre que houver governança aplicável | @@ -40,7 +40,7 @@ The approval engine remains generic. Adapters are only for side effects or compo | --- | --- | | `generic` | Update document, registry, and approval record | | `foundation-context` | Synchronize the canonical document status with its `context.md` | -| `engineering-system` | Synchronize context, YAML, technical entity graph, standards, operations, Quality System, evidence, and composite hash | +| `engineering-system` | Synchronize aggregate context, YAML, specialist contract references, Quality System, and composite hash | | `design-system` | Validate tokens/catalog when applicable | | `feature-brief` | Validate `targetFeature` and the selected feature | diff --git a/docs/engineering-catalog-and-standards.md b/docs/engineering-catalog-and-standards.md index 0b3ef40..0f038f1 100644 --- a/docs/engineering-catalog-and-standards.md +++ b/docs/engineering-catalog-and-standards.md @@ -12,6 +12,15 @@ or one deployable application. Repositories, systems, applications, components, interfaces, data stores, and deployments are independent entities connected by stable identifiers. +Ownership is deliberately split. `technical-landscape` owns the graph, +`engineering-standards` owns rules and profiles, `operations-baseline` owns +operational contracts, `engineering-evidence` owns evidence and maturity, and +`engineering-system` owns only the versioned aggregate and Quality System. +`engineering-orchestrator` sequences these owners and stops at human approval. +The aggregate `engineering-system.yaml` declares `owner_skill` for every area, +and specialist root catalogs repeat their owner. Missing owner metadata remains +valid for legacy adopters; once declared, mismatched ownership is a blocker. + ## Canonical Structure ```text @@ -136,18 +145,20 @@ must not create or overwrite adopter-owned entity records. New starter repositories receive empty root catalogs; existing products opt into them by a previewable migration or normal skill-driven evolution followed by approval. -## Implementation Plan - -1. Make this document and `FRAMEWORK.md` the method-level contract. -2. Expand the Engineering System skill workflow, outputs, checklist, and owned - assets for catalogs, standards, operations, and evidence. -3. Add human and mechanical templates for technical entities, standards, - profiles, exceptions, and root catalogs. -4. Expand the starter with root catalogs and navigational READMEs only. -5. Add the catalog, standards, operations, and evidence areas to the starter - Engineering System YAML and human summary. -6. Validate catalog schema versions, safe relative paths, unique IDs, standard - obligation levels, and referenced records without rejecting legacy products. -7. Update embedded assets, runtime documentation, starter guidance, and tests. -8. Verify init, upgrade preservation, full Go gates, fixture validation, and - distribution boundaries before release. +## Skill Flow + +```text +engineering-orchestrator +→ technical-landscape +→ engineering-standards +→ operations-baseline +→ engineering-evidence +→ engineering-system aggregation and validation +→ engineering-orchestrator readiness review +→ human approval +→ domain-architect or technical-discovery +``` + +The orchestrator may revisit only affected specialists during evolution, but it +must revalidate downstream contracts and the final composite hash before asking +for approval. diff --git a/docs/engineering-systems.md b/docs/engineering-systems.md index 025e7cd..b116007 100644 --- a/docs/engineering-systems.md +++ b/docs/engineering-systems.md @@ -10,6 +10,8 @@ Its scalable entity and standards model is defined in [`engineering-catalog-and- The standards system composes versioned rules through profiles. Consumers pin applicable profiles and standards. A narrower scope may add constraints but may not silently weaken inherited contracts. Exceptions require a stable ID, exact scope, owner, rationale, residual risk, mitigation, expiry or review date, re-entry gate, status, and approval where required. +The shared baseline is coordinated by `engineering-orchestrator`. It routes `technical-landscape`, `engineering-standards`, `operations-baseline`, and `engineering-evidence`, then asks `engineering-system` to consolidate the aggregate and Quality System. Specialist contracts remain independently owned even though the Engineering System composite approval hash governs their use together. + Engineering System approval hashes its complete contract surface deterministically. A change to an approved shared contract makes its approval stale and requires human re-approval. Specification and approved product decisions remain authoritative when contracts conflict. ## Engineering Quality System @@ -26,7 +28,12 @@ Legacy products without the new contract remain compatible until they explicitly ## Owning skills -- `engineering-system`: creates, adopts, evolves, validates, and migrates shared engineering contracts. +- `engineering-orchestrator`: controls completeness, sequencing, blockers, and the human approval handoff. +- `technical-landscape`: owns the technical entity graph, topology, entities, relations, and boundary assessments. +- `engineering-standards`: owns standards, profiles, resolutions, conformance, and governed exceptions. +- `operations-baseline`: owns environments, deployments, runbooks, service levels, release, rollback, and continuity contracts. +- `engineering-evidence`: owns evidence inventory, coverage, maturity, gap, and staleness assessments. +- `engineering-system`: consolidates, versions, validates, and migrates the aggregate and Engineering Quality System. - `technical-discovery`: maps delivery requirements to the codebase and stable engineering baseline. - `engineering-proposal`: translates approved delivery contracts into an intended technical solution. - `engineering-review`: independently reviews the proposal and its alignment with the pinned systems. diff --git a/framework/AGENTS.framework.md b/framework/AGENTS.framework.md index aea5ba3..8fc4140 100644 --- a/framework/AGENTS.framework.md +++ b/framework/AGENTS.framework.md @@ -34,6 +34,8 @@ Operational boundaries: When the product declares `product/design/system/`, route shared foundations, tokens, components, patterns, versions, and sources to the Design System skill. Detailed Design System and UX/UI gates remain in `FRAMEWORK.md` and their owning skills. External visual tools are optional adapters; their installation, output, and availability never grant product approval or replace framework contracts. +Route creation or evolution of `product/engineering/` through Engineering Orchestrator. Technical Landscape owns shared architecture and entity graph contracts; Engineering Standards owns standards and profiles; Operations Baseline owns operational contracts; Engineering Evidence owns evidence and maturity assessments; Engineering System owns only aggregate and Quality System consolidation. Do not let one specialist edit another specialist's subtree without an explicit handoff, and stop for human approval of the current composite hash. + ## Required Reading Authority order: diff --git a/framework/adoption.md b/framework/adoption.md index adbda50..e2b943c 100644 --- a/framework/adoption.md +++ b/framework/adoption.md @@ -47,7 +47,7 @@ Keep implementation roots beside `product/`, using semantic roles such as `web/` spec-framework init . --agents codex --starting-point existing-implementation --code-roots web:web,api:api --yes ``` -The resulting `product/knowledge/assessments/product-landscape.md` is the required comprehensive inventory. Complete it, `engineering/engineering-system.md`, and `design/system/design-system.md` before authoring a real Specification. With no code, these documents are explicit hypotheses; first confirm the intended stack and official scaffold command, then create the semantic sibling root rather than placing code under `product/`. +The resulting `product/knowledge/assessments/product-landscape.md` is the required comprehensive inventory. Route the shared engineering baseline through `engineering-orchestrator`, complete `engineering/engineering-system.md` and `design/system/design-system.md`, and obtain their human approvals before authoring a real Specification. With no code, these documents are explicit hypotheses; first confirm the intended stack and official scaffold command, then create the semantic sibling root rather than placing code under `product/`. After the Artifact Importer fills `mapping.json`, review the selected mappings and materialize them explicitly: diff --git a/framework/init/bootstrap.json b/framework/init/bootstrap.json index a8ac6a7..81de273 100644 --- a/framework/init/bootstrap.json +++ b/framework/init/bootstrap.json @@ -14,7 +14,7 @@ {"id":"strategy","title":"Define the strategy","goal":"Choose the first scoped delivery bet.","read":["product/foundation/vision/vision.md","product/foundation/vision/principles.md","product/foundation/vision/north-star.md","product/foundation/strategy/context.md"],"write":["product/foundation/strategy/strategy.md","product/foundation/strategy/personas.md","product/foundation/strategy/metrics.md","product/foundation/strategy/roadmap.md"],"prompt":"Propose strategy, target segments, trade-offs, metrics, roadmap, and delivery levels using the approved Foundation. Ask before making material scope decisions. Stop before approval.","gate":"Approve Strategy before modeling delivery domains.","next":"First domain"} ], "rules":["Read the nearest context.md before changing an artifact.","Write drafts only after showing the proposed change.","Never change status manually or create approval records.","Record assumptions, gaps, and decisions separately."], - "after":"Complete and approve the Product Landscape, Engineering System, and shared Design System before modeling the complete domain map. Then create Domain -> User Goal -> Feature -> Use Case; do not create a Specification or workspace until the pre-Specification baselines are approved." + "after":"Complete and approve the Product Landscape, route the shared engineering baseline through Engineering Orchestrator, and approve the Engineering System and shared Design System before modeling the complete domain map. Then create Domain -> User Goal -> Feature -> Use Case; do not create a Specification or workspace until the pre-Specification baselines are approved." }, "existing-product": { "title": "Document an operating product", @@ -26,7 +26,7 @@ {"id":"strategy","title":"Define future direction","goal":"Turn the current baseline into explicit future bets.","read":["product/foundation/product-baseline.md","product/foundation/strategy/context.md","existing decisions and metrics"],"write":["product/foundation/strategy/strategy.md","product/foundation/strategy/personas.md","product/foundation/strategy/metrics.md","product/foundation/strategy/roadmap.md"],"prompt":"Read the approved Product Baseline and propose future bets, trade-offs, segments, metrics, and roadmap. Ask before inferring strategic intent from implementation evidence. Stop before approval.","gate":"Approve Strategy before delivery modeling.","next":"First domain"} ], "rules":["Separate observed current state from future intent.","Cite repository or operational evidence.","Do not rewrite product history or approval records.","Escalate to full Foundation when audience, value, or direction is uncertain."], - "after":"Complete and approve the Product Landscape, Engineering System, and shared Design System before modeling the complete domain map and the first Domain -> User Goal -> Feature -> Use Case slice." + "after":"Complete and approve the Product Landscape, route the shared engineering baseline through Engineering Orchestrator, and approve the Engineering System and shared Design System before modeling the complete domain map and the first Domain -> User Goal -> Feature -> Use Case slice." }, "existing-documents": { "title": "Bring existing documents into the framework", @@ -52,7 +52,7 @@ {"id":"walking-skeleton","title":"Model the bounded delivery slice","goal":"Create one Domain -> User Goal -> Feature -> Use Case chain that fits the brief.","read":["approved product/foundation/feature-brief.md","pinned examples/events/ reference","relevant context.md files"],"write":["product/domains//... draft artifacts"],"prompt":"Use the approved Feature Brief and the examples/events reference. Propose the smallest domain, goal, feature, and use case chain that delivers the brief. Preserve parent and non-ownership boundaries. Stop before approval.","gate":"Approve the relevant artifacts through their normal owners and parents.","next":"Specification"} ], "rules":["Keep the scope bounded to the Feature Brief.","Do not invent product-wide strategy.","Do not create a workspace before the Feature Brief gate.","Do not skip the walking skeleton."], - "after":"Complete and approve the Product Landscape, Engineering System, and shared Design System before creating a Specification; then continue through Design when applicable, planning, tasks, and delivery gates." + "after":"Complete and approve the Product Landscape, route the shared engineering baseline through Engineering Orchestrator, and approve the Engineering System and shared Design System before creating a Specification; then continue through Design when applicable, planning, tasks, and delivery gates." }, "existing-implementation": { "title": "Understand an existing codebase", @@ -64,7 +64,7 @@ {"id":"foundation","title":"Recover product intent carefully","goal":"Use the assessment as evidence for the full Foundation sequence.","read":["approved implementation-assessment.md","product/foundation/*/context.md","relevant decisions"],"write":["foundation/problem/problem.md","foundation/vision/*","foundation/strategy/*"],"prompt":"Propose Problem, Vision, Principles, North Star, and Strategy one artifact at a time. Ask questions wherever implementation evidence does not prove user intent. Preserve the distinction between observed behavior and approved product direction.","gate":"Approve each Foundation artifact individually.","next":"First domain"} ], "rules":["Code is evidence, not approved product scope.","Do not change application code during bootstrap.","Separate observed behavior from inferred intent.","Require the full Foundation before delivery work."], - "after":"Complete and approve the Product Landscape, Engineering System, and shared Design System before domain modeling, then read the examples/events reference and continue through the first domain walking skeleton." + "after":"Complete and approve the Product Landscape, route the shared engineering baseline through Engineering Orchestrator, and approve the Engineering System and shared Design System before domain modeling, then read the examples/events reference and continue through the first domain walking skeleton." }, "audit-only": { "title": "Inspect without changing the product", diff --git a/framework/install.md b/framework/install.md index eff893c..200febe 100644 --- a/framework/install.md +++ b/framework/install.md @@ -59,7 +59,7 @@ spec-framework init ../my-product --agents codex,cursor,claude --yes Use `--starting-point existing-documents` with `--source-dir` or `--sources` to bootstrap from existing product material. This creates a scalable analysis-only import run with paged inventory and review chunks; use `--import-max-files`, `--import-max-total-bytes`, `--import-max-file-bytes`, and `--import-chunk-size` to set the explicit bootstrap budget. It never treats source prose as approved product truth. -`init` also detects common implementation roots beside `product/`. Use `--code-roots ` to declare a root explicitly, for example `--code-roots web:web,api:api`. It records the result in the product manifest and creates `knowledge/assessments/product-landscape.md`. The Product Landscape, Engineering System, and shared Design System are mandatory pre-Specification baselines for new products; legacy manifests remain compatible until upgraded into this policy. +`init` also detects common implementation roots beside `product/`. Use `--code-roots ` to declare a root explicitly, for example `--code-roots web:web,api:api`. It records the result in the product manifest and creates `knowledge/assessments/product-landscape.md`. The Product Landscape, Engineering System, and shared Design System are mandatory pre-Specification baselines for new products; route Engineering System creation through `engineering-orchestrator` and its specialist owners. Legacy manifests remain compatible until upgraded into this policy. For a large document set, create a bounded scalable run with `import create`, inspect it with `import status`, and resume one bounded chunk at a time. Draft materialization remains a separate explicit command after sources/chunks and mappings have been reviewed: `spec-framework import materialize --run IMPORT-001 --approved-by "Product Owner" --yes`. diff --git a/framework/skills/README.md b/framework/skills/README.md index e82a513..8a4ea91 100644 --- a/framework/skills/README.md +++ b/framework/skills/README.md @@ -31,8 +31,8 @@ Use a skill-owned `scripts/` resource only for a deterministic, repeatable seque - `/SKILL.md`: one skill per folder. - `/agents/openai.yaml`: optional UI metadata for discovery surfaces. -- Specialist skills: problem, vision, strategy, domain, goal, journey, feature, use case, Design System, UX/UI, specification, Engineering System, technical discovery, engineering proposal, engineering review, implementation planning, graph, task, code runner, bug fixer, QA, code review, security review, threat modeler, commit crafter, PR finalizer, audit, documentation, history, artifact import, and subagent return review. -- Orchestrator skills: product, domain evolution, existing product import, new feature, audit, evolution, documentation, release, delivery, execution scheduling, integration, and dispatch. +- Specialist skills: problem, vision, strategy, domain, goal, journey, feature, use case, Design System, UX/UI, specification, Technical Landscape, Engineering Standards, Operations Baseline, Engineering Evidence, Engineering System aggregation and quality, technical discovery, engineering proposal, engineering review, implementation planning, graph, task, code runner, bug fixer, QA, code review, security review, threat modeler, commit crafter, PR finalizer, audit, documentation, history, artifact import, and subagent return review. +- Orchestrator skills: product, engineering, domain evolution, existing product import, new feature, audit, evolution, documentation, release, delivery, execution scheduling, integration, and dispatch. - Guidance skills: Framework Guide translates human goals into current CLI state, the smallest safe command, and the correct specialist or approval handoff without authoring artifacts. `grill-me` stress-tests a plan or proposal one decision at a time before it reaches its owning skill. Runtime v2 also includes the `command-planner` and `command-executor` operational skills. The planner owns immutable argv-based plans; the executor is restricted to local R0/R1 plans. @@ -50,6 +50,8 @@ Use `framework-guide` as the default conversational entry point when no verified Delivery Orchestrator reports through the consolidated dashboard model. Product Historian owns review of guided legacy decision migration; the migration tool only updates `.product/decisions.json` metadata and never edits DEC content or approvals. +Engineering Orchestrator owns shared-baseline sequencing across Technical Landscape, Engineering Standards, Operations Baseline, Engineering Evidence, and Engineering System consolidation. It never authors their contracts or grants approval. + Vision AI owns a three-artifact package with exclusive content boundaries: `vision.md` contains direction and strategic boundaries, `principles.md` contains product decision rules and trade-offs, and `north-star.md` contains the outcome metric and guardrails. The artifacts link to one another instead of duplicating canonical content. Domain Architect AI reads the pinned runtime's `examples/events/` before first-domain modeling. That worked example is the canonical reference for business-area boundaries, non-ownership, cross-domain contracts, and the first Domain -> User Goal -> Feature -> Use Case walking skeleton. diff --git a/framework/skills/documentation-writer/references/template-authoring.md b/framework/skills/documentation-writer/references/template-authoring.md index 29eaa55..5a2d39b 100644 --- a/framework/skills/documentation-writer/references/template-authoring.md +++ b/framework/skills/documentation-writer/references/template-authoring.md @@ -20,7 +20,8 @@ Use the generating skill's own templates whenever a new canonical artifact is cr - `domain-template.md`, `goal-template.md`, `feature-template.md`, `use-case-template.md`: product hierarchy artifacts. - `journey-template.md`: user journey artifact. - `specification-template.md`, `design-template.md`, `design-system-template.md`, `design-component-template.md`, `design-pattern-template.md`, `engineering-system-template.md`, `engineering-proposal-template.md`, `engineering-review-template.md`, `implementation-plan-template.md`: planning, shared Design and Engineering contracts, and independent technical review. -- Engineering System assets include root and entity catalogs, standards, profiles, exceptions, operations and evidence templates, plus `quality-system-template.md`, `quality-system-template.yaml`, `quality-model-template.md`, `test-strategy-template.md`, and `fitness-functions-template.yaml` for shared product quality contracts. +- `technical-landscape/assets/` owns topology, technical catalog, entity, and boundary templates; `engineering-standards/assets/` owns standards, profiles, resolution, exception, and conformance templates; `operations-baseline/assets/` owns operations, environment, deployment, runbook, service-level, release, and rollback templates; `engineering-evidence/assets/` owns evidence, coverage, maturity, gap, and staleness templates. +- `engineering-system/assets/` owns only the aggregate Engineering System and shared product quality contracts: `engineering-system-template.*`, `quality-system-template.*`, `quality-model-template.md`, `test-strategy-template.md`, and `fitness-functions-template.yaml`. - `execution-graph-template.json`, `tasks-template.md`, `tests-template.md`: executable planning artifacts. - `qa-evidence-template.md`, `security-review-template.md`, `security-baseline-template.md`, `threat-register-template.md`: validation evidence, security gate, and proactive threat modeling artifacts. - `analytics-template.md`, `audit-template.md`, `readiness-report-template.md`: validation and measurement artifacts. diff --git a/framework/skills/engineering-evidence/SKILL.md b/framework/skills/engineering-evidence/SKILL.md new file mode 100644 index 0000000..3f57a6d --- /dev/null +++ b/framework/skills/engineering-evidence/SKILL.md @@ -0,0 +1,60 @@ +--- +name: engineering-evidence +description: "Engineering Evidence Skill. Use when an agent needs to inventory, normalize, assess, or audit engineering evidence, maturity claims, coverage, and staleness across the shared Engineering System." +--- + +# Engineering Evidence Skill + +## Layer +Validation + +## Responsibility +Own the engineering evidence inventory, evidence records, coverage maps, maturity assessments, verification-run references, and gap or staleness reports. It does not fabricate evidence, execute application changes, or approve maturity. + +## Operating modes +- create: establish the first evidence inventory and baseline assessments. +- update: refresh evidence references and affected maturity findings. +- audit: detect missing, unresolved, stale, duplicated, or scope-mismatched evidence. +- explain: summarize what current evidence proves and does not prove. + +## Inputs +Technical graph; standards; operations; Quality System; code and tests; CI and runtime references; decisions; current maturity claims. + +## Outputs +`engineering/evidence/inventory.md`; optional mechanical catalog and `EVID-*` records; coverage map; maturity assessments; gap and staleness reports. + +## Required reading +- [`engineering-systems.md`](../../docs/engineering-systems.md) and [`engineering-catalog-and-standards.md`](../../docs/engineering-catalog-and-standards.md). +- the framework root's `FRAMEWORK.md` +- Relevant parent context.md files. +- This skill owns every generation resource in `assets/` and the read-only evidence inventory scripts in `scripts/`. +- Approved decisions are discovered through the active product root's `.product/decisions.json`. + +## Discovery and challenge + +Follow the shared [Discovery And Challenge contract](../discovery-and-challenge.md) when evidence limitations, freshness policy, maturity interpretation, or ownership requires a material choice. + +## Workflow +1. Run the platform-appropriate evidence inventory script and inspect referenced local, CI, runtime, and approved external sources. +2. Normalize each material source into a stable `EVID-*` reference without copying volatile logs into canonical contracts. +3. Map evidence to technical entities, standards, operations, quality areas, decisions, and maturity claims. +4. Evaluate source existence, observation time, freshness policy, scope, and what the evidence actually proves. +5. Mark unsupported or stale claims as findings; never promote maturity or synthesize approval from evidence volume. +6. Update coverage, maturity, gap, and staleness reports and identify the exact owner required to resolve each gap. +7. Run Engineering System validation when requested and return findings to `engineering-orchestrator`. + +## Quality checklist +- [ ] Preserves traceability to affected artifacts. +- [ ] Uses the correct template and naming conventions. +- [ ] Evidence references are safe, resolvable, scoped, and freshness-aware. +- [ ] Observed evidence, inference, and hypothesis remain distinguishable. +- [ ] Every maturity above baseline has sufficient current evidence. +- [ ] Volatile outputs are referenced rather than copied into stable contracts. +- [ ] Detects gaps, conflicts, and dependencies. +- [ ] Records or requests decisions for meaningful changes. +- [ ] Leaves a clear handoff for the next skill or orchestrator. + +## Handoff +Next: `engineering-orchestrator`. + +Pass forward inventory, coverage, maturity findings, stale or missing evidence, source limitations, blockers, and required owners. diff --git a/framework/skills/engineering-evidence/agents/openai.yaml b/framework/skills/engineering-evidence/agents/openai.yaml new file mode 100644 index 0000000..bcdb8ec --- /dev/null +++ b/framework/skills/engineering-evidence/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Engineering Evidence" + short_description: "Assess engineering evidence, maturity, and staleness" + default_prompt: "Use $engineering-evidence to inventory evidence and assess coverage, maturity, gaps, and staleness." diff --git a/framework/skills/engineering-evidence/assets/coverage-map-template.yaml b/framework/skills/engineering-evidence/assets/coverage-map-template.yaml new file mode 100644 index 0000000..63e1558 --- /dev/null +++ b/framework/skills/engineering-evidence/assets/coverage-map-template.yaml @@ -0,0 +1,4 @@ +schema_version: 1 +claims: {} +uncovered: [] +stale: [] diff --git a/framework/skills/engineering-evidence/assets/evidence-catalog-template.yaml b/framework/skills/engineering-evidence/assets/evidence-catalog-template.yaml new file mode 100644 index 0000000..0e7aae9 --- /dev/null +++ b/framework/skills/engineering-evidence/assets/evidence-catalog-template.yaml @@ -0,0 +1,5 @@ +schema_version: 1 +owner_skill: engineering-evidence +records: {} +coverage: {} +verification_runs: {} diff --git a/framework/skills/engineering-evidence/assets/evidence-gap-report-template.md b/framework/skills/engineering-evidence/assets/evidence-gap-report-template.md new file mode 100644 index 0000000..3b3ff36 --- /dev/null +++ b/framework/skills/engineering-evidence/assets/evidence-gap-report-template.md @@ -0,0 +1,5 @@ +# Engineering Evidence Gaps + +| Gap | Affected entity, standard, operation, or maturity | Impact | Required owner | Next evidence | +| --- | --- | --- | --- | --- | +| `[gap]` | `[IDs or area]` | `[impact]` | `[owner]` | `[source or verification needed]` | diff --git a/framework/skills/engineering-system/assets/evidence-inventory-template.md b/framework/skills/engineering-evidence/assets/evidence-inventory-template.md similarity index 100% rename from framework/skills/engineering-system/assets/evidence-inventory-template.md rename to framework/skills/engineering-evidence/assets/evidence-inventory-template.md diff --git a/framework/skills/engineering-evidence/assets/evidence-record-template.yaml b/framework/skills/engineering-evidence/assets/evidence-record-template.yaml new file mode 100644 index 0000000..e051d60 --- /dev/null +++ b/framework/skills/engineering-evidence/assets/evidence-record-template.yaml @@ -0,0 +1,9 @@ +schema_version: 1 +id: EVID-001 +type: "[code | test | config | ci | runtime | external]" +source: "[path, command, CI, URL, or runtime reference]" +supports: [] +observed_at: YYYY-MM-DD +freshness: + maximum_age_days: 30 +limitations: [] diff --git a/framework/skills/engineering-evidence/assets/maturity-assessment-template.md b/framework/skills/engineering-evidence/assets/maturity-assessment-template.md new file mode 100644 index 0000000..4880783 --- /dev/null +++ b/framework/skills/engineering-evidence/assets/maturity-assessment-template.md @@ -0,0 +1,9 @@ +# Engineering Maturity Assessment: [area] + +| Claim | Current maturity | Required evidence | Current evidence | Finding | +| --- | --- | --- | --- | --- | +| `[claim]` | `[baseline | mapped | governed | verified | operated]` | `[requirement]` | `[EVID-* or source]` | `[supported | partial | unsupported | stale]` | + +## Limitations And Handoff + +[What the evidence does not prove and the exact owner required next.] diff --git a/framework/skills/engineering-system/scripts/inventory-engineering-evidence.ps1 b/framework/skills/engineering-evidence/scripts/inventory-engineering-evidence.ps1 similarity index 98% rename from framework/skills/engineering-system/scripts/inventory-engineering-evidence.ps1 rename to framework/skills/engineering-evidence/scripts/inventory-engineering-evidence.ps1 index fc39f35..f8b4c7c 100644 --- a/framework/skills/engineering-system/scripts/inventory-engineering-evidence.ps1 +++ b/framework/skills/engineering-evidence/scripts/inventory-engineering-evidence.ps1 @@ -14,6 +14,7 @@ foreach ($root in $manifest.code_roots) { foreach ($file in $repository) { Write-Output ("repository-contract: " + $file.FullName.Substring($repo.Length + 1).Replace("\", "/")) } Get-ChildItem -LiteralPath $path -Force -File -Recurse -ErrorAction SilentlyContinue | Where-Object { $_.Name -match '^(Dockerfile|docker-compose.*|Makefile|README\.md|go\.mod|package\.json|pyproject\.toml|Cargo\.toml|pom\.xml|build\.gradle|settings\.gradle|.*\.ya?ml|.*\.tf)$' -or $_.Extension -in @(".sln", ".csproj", ".fsproj") -or $_.FullName -match '(test|spec|\.github|\.gitlab|deploy|infra|runbook)' } | + Sort-Object FullName | ForEach-Object { Write-Output ("evidence: " + $_.FullName.Substring($repo.Length + 1).Replace("\", "/")) } } if ($Validate) { & spec-framework engineering-system validate --product-root (Join-Path $repo $ProductRoot); exit $LASTEXITCODE } diff --git a/framework/skills/engineering-system/scripts/inventory-engineering-evidence.sh b/framework/skills/engineering-evidence/scripts/inventory-engineering-evidence.sh similarity index 96% rename from framework/skills/engineering-system/scripts/inventory-engineering-evidence.sh rename to framework/skills/engineering-evidence/scripts/inventory-engineering-evidence.sh index 1b55df5..334cdab 100644 --- a/framework/skills/engineering-system/scripts/inventory-engineering-evidence.sh +++ b/framework/skills/engineering-evidence/scripts/inventory-engineering-evidence.sh @@ -9,8 +9,8 @@ roots="$(awk -F '"' '/^[[:space:]]*"path"[[:space:]]*:/ { path=$4 } /^[[:space:] while IFS=$'\t' read -r path role; do [[ -n "$path" ]] || continue; root="$repo/$path"; echo; echo "## $path ($role)" [[ -d "$root" ]] || { echo "MISSING"; continue; } - while IFS= read -r file; do echo "repository-contract: ${file#"$repo/"}"; done < <(find "$root" -maxdepth 1 -type f \( -name go.mod -o -name package.json -o -name pyproject.toml -o -name Cargo.toml -o -name pom.xml -o -name build.gradle -o -name settings.gradle -o -name '*.sln' -o -name '*.csproj' -o -name '*.fsproj' \) -print) - while IFS= read -r file; do echo "evidence: ${file#"$repo/"}"; done < <(find "$root" -type f \( -name Dockerfile -o -name 'docker-compose*' -o -name Makefile -o -name README.md -o -name go.mod -o -name package.json -o -name pyproject.toml -o -name Cargo.toml -o -name pom.xml -o -name build.gradle -o -name settings.gradle -o -name '*.sln' -o -name '*.csproj' -o -name '*.fsproj' -o -name '*.yml' -o -name '*.yaml' -o -name '*.tf' -o -iname '*test*' -o -iname '*spec*' -o -ipath '*deploy*' -o -ipath '*infra*' -o -ipath '*runbook*' \) -print) + while IFS= read -r file; do echo "repository-contract: ${file#"$repo/"}"; done < <(find "$root" -maxdepth 1 -type f \( -name go.mod -o -name package.json -o -name pyproject.toml -o -name Cargo.toml -o -name pom.xml -o -name build.gradle -o -name settings.gradle -o -name '*.sln' -o -name '*.csproj' -o -name '*.fsproj' \) -print | sort) + while IFS= read -r file; do echo "evidence: ${file#"$repo/"}"; done < <(find "$root" -type f \( -name Dockerfile -o -name 'docker-compose*' -o -name Makefile -o -name README.md -o -name go.mod -o -name package.json -o -name pyproject.toml -o -name Cargo.toml -o -name pom.xml -o -name build.gradle -o -name settings.gradle -o -name '*.sln' -o -name '*.csproj' -o -name '*.fsproj' -o -name '*.yml' -o -name '*.yaml' -o -name '*.tf' -o -iname '*test*' -o -iname '*spec*' -o -ipath '*deploy*' -o -ipath '*infra*' -o -ipath '*runbook*' \) -print | sort) done <<< "$roots" if $validate; then spec-framework engineering-system validate --product-root "$repo/$product_root" diff --git a/framework/skills/engineering-orchestrator/SKILL.md b/framework/skills/engineering-orchestrator/SKILL.md new file mode 100644 index 0000000..5ec83c9 --- /dev/null +++ b/framework/skills/engineering-orchestrator/SKILL.md @@ -0,0 +1,63 @@ +--- +name: engineering-orchestrator +description: "Engineering Orchestrator Skill. Use when an agent needs to route creation, adoption, evolution, or audit of the shared Engineering System across technical landscape, standards, operations, evidence, aggregation, and human approval." +--- + +# Engineering Orchestrator Skill + +## Layer +Governance + +## Responsibility +Coordinate the complete shared engineering baseline across specialist owners, gates, sequencing, and handoffs. It never authors specialist catalogs, changes application code, or grants approval. + +## Operating modes +- create: route the first complete engineering baseline. +- update: route affected specialists after evidence or contracts change. +- audit: find missing, stale, conflicting, or unapproved engineering surfaces. +- explain: summarize baseline coverage, blockers, and next ownership. + +## Inputs +Product Landscape; declared code roots; product and engineering contexts; current Engineering System; approvals; decisions; specialist findings. + +## Outputs +Persisted engineering-baseline handoff; readiness verdict; ordered specialist routes; blockers; human approval checkpoint. + +## Required reading +- [`engineering-systems.md`](../../docs/engineering-systems.md) and [`engineering-catalog-and-standards.md`](../../docs/engineering-catalog-and-standards.md). +- [`execution-runtime.md`](../../docs/execution-runtime.md) and [`lifecycle-and-approvals.md`](../../docs/lifecycle-and-approvals.md). +- the framework root's `FRAMEWORK.md` +- Relevant parent context.md files. +- This skill owns `assets/engineering-baseline-handoff-template.json` and `assets/engineering-readiness-report-template.md` for persisted routing and readiness reporting. +- Approved decisions are discovered through the active product root's `.product/decisions.json`; resolve each registered path from its declared decision domain root. + +## Discovery and challenge + +Follow the shared [Discovery And Challenge contract](../discovery-and-challenge.md) when scope, ownership, compatibility, or a blocking route requires human choice. + +## Workflow +1. Revalidate the product manifest, Product Landscape, code roots, current Engineering System hash, approvals, and persisted workspace state. +2. Classify the route as create, update, adopt, or audit and identify affected specialist contracts without assuming one repository, application, or deployment. +3. Route `technical-landscape` first when entity or relation coverage is absent or stale. +4. Route `engineering-standards` after the entity graph can express applicability and inheritance. +5. Route `operations-baseline` after applications, components, deployments, interfaces, and data stores are identifiable. +6. Route `engineering-evidence` to resolve evidence, maturity, coverage, and staleness for every claimed area. +7. Route `engineering-system` only after specialist outputs are internally consistent, so it can consolidate identity, version, quality, compatibility, and the composite contract. +8. Re-run Engineering System validation, report uncovered scope and decision candidates, and stop at the human approval gate. +9. After current approval, route product-wide bootstrap to `domain-architect` or delivery-specific work to `technical-discovery` according to the initiating scope. + +## Quality checklist +- [ ] Preserves traceability to affected artifacts. +- [ ] Uses the correct template and naming conventions. +- [ ] Every declared code root and engineering area has an owner or explicit gap. +- [ ] Specialist outputs agree on stable IDs, applicability, maturity, evidence, and compatibility. +- [ ] No specialist is bypassed merely because a previous aggregate contract exists. +- [ ] Approval remains human and applies to the current composite hash. +- [ ] Detects gaps, conflicts, and dependencies. +- [ ] Records or requests decisions for meaningful changes. +- [ ] Leaves a clear handoff for the next skill or orchestrator. + +## Handoff +Next: `technical-landscape`. + +This is the default first route. For an update or resumed baseline, route only the affected specialist or `engineering-system` consolidation described by the current readiness state. Pass forward scope, code roots, current hashes, completed specialist contracts, evidence, decisions, blockers, and the post-approval route. diff --git a/framework/skills/engineering-orchestrator/agents/openai.yaml b/framework/skills/engineering-orchestrator/agents/openai.yaml new file mode 100644 index 0000000..c4ca694 --- /dev/null +++ b/framework/skills/engineering-orchestrator/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Engineering Orchestrator" + short_description: "Route the complete shared engineering baseline" + default_prompt: "Use $engineering-orchestrator to route the shared engineering baseline through its specialist owners and approval gate." diff --git a/framework/skills/engineering-orchestrator/assets/engineering-baseline-handoff-template.json b/framework/skills/engineering-orchestrator/assets/engineering-baseline-handoff-template.json new file mode 100644 index 0000000..4d5f797 --- /dev/null +++ b/framework/skills/engineering-orchestrator/assets/engineering-baseline-handoff-template.json @@ -0,0 +1,12 @@ +{ + "schema_version": 1, + "scope": "product", + "mode": "create", + "current_engineering_system": null, + "routes": [], + "completed_contracts": [], + "blockers": [], + "decision_candidates": [], + "approval_required": true, + "next": "technical-landscape" +} diff --git a/framework/skills/engineering-orchestrator/assets/engineering-readiness-report-template.md b/framework/skills/engineering-orchestrator/assets/engineering-readiness-report-template.md new file mode 100644 index 0000000..844d09f --- /dev/null +++ b/framework/skills/engineering-orchestrator/assets/engineering-readiness-report-template.md @@ -0,0 +1,23 @@ +# Engineering Baseline Readiness + +## Scope + +[Product, repositories, applications, or affected engineering boundary.] + +## Coverage + +| Contract | Owner skill | Status | Evidence | Blockers | +| --- | --- | --- | --- | --- | +| Technical landscape | `technical-landscape` | `[status]` | `[paths]` | `[none or findings]` | +| Standards | `engineering-standards` | `[status]` | `[paths]` | `[none or findings]` | +| Operations | `operations-baseline` | `[status]` | `[paths]` | `[none or findings]` | +| Evidence and maturity | `engineering-evidence` | `[status]` | `[paths]` | `[none or findings]` | +| Engineering System aggregate | `engineering-system` | `[status]` | `[paths]` | `[none or findings]` | + +## Verdict + +`ready_for_human_approval | needs_changes | blocked` + +## Handoff + +[Exact next owner, scope, required evidence, and gate.] diff --git a/framework/skills/engineering-standards/SKILL.md b/framework/skills/engineering-standards/SKILL.md new file mode 100644 index 0000000..701caa0 --- /dev/null +++ b/framework/skills/engineering-standards/SKILL.md @@ -0,0 +1,61 @@ +--- +name: engineering-standards +description: "Engineering Standards Skill. Use when an agent needs to create, evolve, resolve, or audit versioned engineering standards, profiles, applicability, verifiable rules, and governed exceptions." +--- + +# Engineering Standards Skill + +## Layer +Planning + +## Responsibility +Own the standards catalog, profiles, individual standards, effective resolutions, conformance reports, and `STDEX-*` exceptions. It does not own the technical graph, Quality System, product decisions, or delivery approval. + +## Operating modes +- create: establish the first evidence-backed standards catalog and profiles. +- update: version rules and profiles while declaring compatibility impact. +- adopt: register authoritative external standards with pinned versions. +- audit: find cycles, silent weakening, unverifiable rules, gaps, and expired exceptions. +- explain: summarize effective standards for a scope. + +## Inputs +Technical entity graph; engineering conventions; code and CI evidence; quality and security contracts; approved decisions; compatibility constraints. + +## Outputs +`engineering/standards/standards.yaml`; `PROFILE-*`; `STD-*` YAML and Markdown; `STDEX-*`; profile resolutions; conformance and gap reports. + +## Required reading +- [`engineering-systems.md`](../../docs/engineering-systems.md) and [`engineering-catalog-and-standards.md`](../../docs/engineering-catalog-and-standards.md). +- the framework root's `FRAMEWORK.md` +- Relevant parent context.md files. +- This skill owns every generation resource in `assets/`. +- Approved decisions are discovered through the active product root's `.product/decisions.json`. + +## Discovery and challenge + +Follow the shared [Discovery And Challenge contract](../discovery-and-challenge.md) before selecting obligation levels, compatibility policy, profile inheritance, or exception boundaries. + +## Workflow +1. Read the technical graph, existing conventions, quality and security contracts, and real enforcement evidence. +2. Define standards only for stable technical rules with an explicit applicability boundary and verification method. +3. Assign semantic versions, categories, obligation levels, individually identifiable rules, compatibility notes, and exception policy. +4. Compose profiles by entity type, capability, or explicit assignment; reject inheritance cycles and silent weakening of required rules. +5. Resolve effective standards for affected entities and record uncovered or conflicting applicability. +6. Create `STDEX-*` only with exact scope, owner, rationale, residual risk, mitigation, expiry, re-entry gate, and status; never self-approve it. +7. Validate all references, versions, rules, profiles, and exception expiry. +8. Return standards coverage and gaps to `engineering-orchestrator`. + +## Quality checklist +- [ ] Preserves traceability to affected artifacts. +- [ ] Uses the correct template and naming conventions. +- [ ] Rules are verifiable and do not duplicate Quality System or security policy. +- [ ] Profiles are acyclic, version-pinned, and cannot silently weaken inherited requirements. +- [ ] Exceptions are scoped, temporary, risk-explicit, and approval-bound. +- [ ] Detects gaps, conflicts, and dependencies. +- [ ] Records or requests decisions for meaningful changes. +- [ ] Leaves a clear handoff for the next skill or orchestrator. + +## Handoff +Next: `engineering-orchestrator`. + +Pass forward standards, profiles, resolved applicability, exceptions, verification evidence, compatibility impacts, and blockers. diff --git a/framework/skills/engineering-standards/agents/openai.yaml b/framework/skills/engineering-standards/agents/openai.yaml new file mode 100644 index 0000000..575ceab --- /dev/null +++ b/framework/skills/engineering-standards/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Engineering Standards" + short_description: "Govern versioned technical standards and profiles" + default_prompt: "Use $engineering-standards to define or audit standards, profiles, effective applicability, and governed exceptions." diff --git a/framework/skills/engineering-standards/assets/conformance-report-template.md b/framework/skills/engineering-standards/assets/conformance-report-template.md new file mode 100644 index 0000000..d818899 --- /dev/null +++ b/framework/skills/engineering-standards/assets/conformance-report-template.md @@ -0,0 +1,9 @@ +# Standards Conformance: [scope] + +| Standard and rule | Applicability | Evidence | Result | Exception or gap | +| --- | --- | --- | --- | --- | +| `[STD-*@version / rule]` | `[reason]` | `[reference]` | `[conforms | does_not_conform | unknown]` | `[None | STDEX-* | finding]` | + +## Verdict + +`conforms | needs_changes | blocked` diff --git a/framework/skills/engineering-standards/assets/profile-resolution-template.yaml b/framework/skills/engineering-standards/assets/profile-resolution-template.yaml new file mode 100644 index 0000000..3cfd71f --- /dev/null +++ b/framework/skills/engineering-standards/assets/profile-resolution-template.yaml @@ -0,0 +1,6 @@ +schema_version: 1 +entity: "[stable entity ID]" +profiles: [] +effective_standards: [] +exceptions: [] +conflicts: [] diff --git a/framework/skills/engineering-system/assets/standard-exception-template.yaml b/framework/skills/engineering-standards/assets/standard-exception-template.yaml similarity index 100% rename from framework/skills/engineering-system/assets/standard-exception-template.yaml rename to framework/skills/engineering-standards/assets/standard-exception-template.yaml diff --git a/framework/skills/engineering-system/assets/standard-profile-template.yaml b/framework/skills/engineering-standards/assets/standard-profile-template.yaml similarity index 100% rename from framework/skills/engineering-system/assets/standard-profile-template.yaml rename to framework/skills/engineering-standards/assets/standard-profile-template.yaml diff --git a/framework/skills/engineering-system/assets/standard-template.md b/framework/skills/engineering-standards/assets/standard-template.md similarity index 100% rename from framework/skills/engineering-system/assets/standard-template.md rename to framework/skills/engineering-standards/assets/standard-template.md diff --git a/framework/skills/engineering-system/assets/standard-template.yaml b/framework/skills/engineering-standards/assets/standard-template.yaml similarity index 100% rename from framework/skills/engineering-system/assets/standard-template.yaml rename to framework/skills/engineering-standards/assets/standard-template.yaml diff --git a/framework/skills/engineering-system/assets/standards-catalog-template.yaml b/framework/skills/engineering-standards/assets/standards-catalog-template.yaml similarity index 76% rename from framework/skills/engineering-system/assets/standards-catalog-template.yaml rename to framework/skills/engineering-standards/assets/standards-catalog-template.yaml index 0a91076..3f9fa8d 100644 --- a/framework/skills/engineering-system/assets/standards-catalog-template.yaml +++ b/framework/skills/engineering-standards/assets/standards-catalog-template.yaml @@ -1,4 +1,5 @@ schema_version: 1 +owner_skill: engineering-standards profiles: PROFILE-PRODUCT-DEFAULT: profiles/product-default.yaml standards: {} diff --git a/framework/skills/engineering-standards/assets/standards-overview-template.md b/framework/skills/engineering-standards/assets/standards-overview-template.md new file mode 100644 index 0000000..9b919e5 --- /dev/null +++ b/framework/skills/engineering-standards/assets/standards-overview-template.md @@ -0,0 +1,17 @@ +# Engineering Standards + +## Profiles + +| Profile | Version | Applies to | Extends | Status | +| --- | --- | --- | --- | --- | +| `[PROFILE-*]` | `[semver]` | `[entity types or capabilities]` | `[profiles or None]` | `[status]` | + +## Standards + +| Standard | Version | Category | Level | Verification | +| --- | --- | --- | --- | --- | +| `[STD-*]` | `[semver]` | `[category]` | `[level]` | `[methods]` | + +## Exceptions And Gaps + +[Open governed exceptions, uncovered scopes, conflicts, and decision candidates.] diff --git a/framework/skills/engineering-system/SKILL.md b/framework/skills/engineering-system/SKILL.md index 59c7418..d733500 100644 --- a/framework/skills/engineering-system/SKILL.md +++ b/framework/skills/engineering-system/SKILL.md @@ -1,6 +1,6 @@ --- name: engineering-system -description: "Engineering System Skill. Use when an agent needs to create, adopt, evolve, version, or audit shared product architecture, standards, quality attributes, fitness functions, operations, and engineering evidence in the Spec Framework workflow." +description: "Engineering System Skill. Use when an agent needs to consolidate, version, validate, migrate, or explain the shared Engineering System aggregate and Engineering Quality System after specialist engineering contracts are ready." --- # Engineering System Skill @@ -9,27 +9,27 @@ description: "Engineering System Skill. Use when an agent needs to create, adopt Planning ## Responsibility -Own the shared `engineering/engineering-system.md` and `engineering-system.yaml` contracts, technical entity graph, standards, operations catalogs, quality policy, and evidence index. Maintain stable product engineering knowledge without making product decisions, planning a specific delivery, or writing application code. +Own the aggregate `engineering/engineering-system.md`, `engineering-system.yaml`, and Engineering Quality System contracts. Consolidate specialist-owned technical landscape, standards, operations, and evidence without authoring those contracts, making product decisions, planning a specific delivery, or writing application code. ## Operating modes -- create: establish the first evidence-backed Engineering System. -- update: revise stable contracts while preserving approved decisions and compatibility. +- create: consolidate the first evidence-backed Engineering System after specialist outputs exist. +- update: revise the aggregate and quality contracts while preserving approved decisions and compatibility. - adopt: register an authoritative versioned external engineering source. - audit: find stale evidence, missing ownership, ungoverned boundaries, and unsupported maturity claims. - explain: summarize the system, maturity, consumers, and gaps. ## Inputs -Product context; real code and test tree; deployment and operations configuration; approved decisions; conventions; runbooks; dependency evidence. +Engineering Orchestrator handoff; specialist-owned technical landscape, standards, operations, and evidence contracts; product context; Quality System evidence; approved decisions. ## Outputs -`engineering/engineering-system.md`; `engineering/engineering-system.yaml`; root technical catalog; on-demand system, application, component, repository, interface, data-store, and deployment records; standards catalog, profiles, standards, and exceptions; architecture, quality, operations, runbook, and evidence links; decision candidates; version and compatibility notes. +`engineering/engineering-system.md`; `engineering/engineering-system.yaml`; Engineering Quality System contracts; consolidated area maturity and references; version, compatibility, migration, and approval-impact notes. ## Required reading - [`engineering-systems.md`](../../docs/engineering-systems.md) for shared Engineering System and Quality System versioning, migration, and approval boundaries. - [`engineering-catalog-and-standards.md`](../../docs/engineering-catalog-and-standards.md) for the scalable entity graph, standards inheritance, exceptions, and materialization boundary. - the framework root's `FRAMEWORK.md` - Relevant parent context.md files. -- This skill owns every generation resource in `assets/`: Engineering System, technical catalog and entity, standards catalog, standard, profile, exception, quality system and model, test strategy, fitness functions, operations catalog, and evidence inventory. +- This skill owns only the aggregate Engineering System and Engineering Quality System generation resources in `assets/`: Engineering System, quality system and model, test strategy, and fitness functions. - Approved decisions are discovered through the active product root's `.product/decisions.json`; resolve each registered `path` from its declared domain root (`knowledge/decisions/`, `design/decisions/`, or `engineering/decisions/`). ## Discovery and challenge @@ -39,19 +39,16 @@ Follow the shared [Discovery And Challenge contract](../discovery-and-challenge. Use scripts/invoke-cli.ps1 on Windows or scripts/invoke-cli.sh on macOS/Linux for the CLI operation in this skill's reviewed scope. The wrapper never adds --yes or an approver identity. -Use `scripts/inventory-engineering-evidence.ps1` on Windows or `scripts/inventory-engineering-evidence.sh` on macOS/Linux before authoring the shared baseline. It inventories deterministic engineering evidence and can call `engineering-system validate`; it never changes product or code files. - ## Workflow -1. Inspect the real product code, tests, configuration, environments, operations evidence, and existing engineering documents. If no code exists, create explicit hypothesis contracts, pending decisions, and intended constraints rather than pretending evidence exists. -2. Define the covered product and repository boundaries and choose `generate`, `evolve`, or `adopt`. -3. Inventory systems, applications, components, repositories, ownership, data stores, interfaces, deployments, standards, quality attributes, environments, runbooks, and consumers with evidence paths. Model stable IDs and graph relations instead of inferring them from folders. -4. Initialize only root catalogs. Materialize entity records, standards, profiles, exceptions, environments, deployments, and runbooks on demand from evidence or explicit hypotheses. -5. Compose standards through profiles selected by entity type, capability, or explicit assignment. A narrower contract may add constraints but requires a governed exception or decision to weaken an inherited required rule. -6. Maintain `engineering/quality/quality-system.md` and `quality-system.yaml` together with the quality model, test strategy, and fitness functions. Keep commands canonical in `knowledge/conventions/gates.md`. -7. Declare maturity per area only when its required evidence exists; maturity never implies approval. -8. Link approved `DEC-*` records for governed choices and record candidates for missing structural, data, security, standards, or operational decisions. -9. Update the human contract and every affected mechanical catalog together, applying semantic versioning and compatibility notes. -10. Stop before inventing architecture, editing application code, or creating approval records. +1. Require a current `engineering-orchestrator` handoff and read the specialist-owned technical landscape, standards, operations, and evidence contracts. +2. Stop when a required specialist contract is absent, inconsistent, stale, or reports a blocking gap; return the exact scope to the orchestrator. +3. Define the covered product boundary and choose `generate`, `evolve`, or `adopt` without redefining specialist-owned content. +4. Maintain `engineering/quality/quality-system.md` and `quality-system.yaml` together with the quality model, test strategy, and fitness functions. Keep commands canonical in `knowledge/conventions/gates.md`. +5. Consolidate area references and maturity only when the evidence skill supports each claim; maturity never implies approval. +6. Link approved `DEC-*` records and surface unresolved decision candidates without creating decisions or approvals. +7. Update the human and mechanical aggregate together, applying semantic versioning, compatibility, migration, consumer, and stale-approval notes. +8. Run `engineering-system validate` and return the current composite hash and blockers to `engineering-orchestrator`. +9. Stop before editing specialist catalogs, application code, product decisions, or approval records. ## Quality checklist - [ ] Preserves traceability to affected artifacts. @@ -63,15 +60,14 @@ Use `scripts/inventory-engineering-evidence.ps1` on Windows or `scripts/inventor - [ ] Quality exceptions identify scope, owner, rationale, residual risk, mitigation, expiry or review date, re-entry gate, and status. - [ ] Only open, unexpired, in-scope exceptions are passed to delivery consumers. - [ ] Stable knowledge is not duplicated into delivery proposals. -- [ ] Repository, application, component, and deployment relations use stable IDs and support monorepo and polyrepo shapes. -- [ ] Standards declare version, obligation level, applicability, verification, evidence, and exception policy. -- [ ] Profiles do not contain cycles and consumers cannot silently weaken inherited required standards. -- [ ] Optional records are materialized only when evidence or explicit hypotheses require them. +- [ ] Every specialist-owned area is referenced rather than duplicated. +- [ ] Aggregate maturity agrees with the evidence assessment and specialist contracts. +- [ ] Version, compatibility, consumers, migration, and approval staleness are explicit. - [ ] Detects gaps, conflicts, and dependencies. - [ ] Records or requests decisions for meaningful changes. - [ ] Leaves a clear handoff for the next skill or orchestrator. ## Handoff -Next: `technical-discovery` for delivery-specific work. +Next: `engineering-orchestrator` for completeness review and the human approval gate. -Pass forward the pinned system id/version, architecture and ownership contracts, standards, quality attributes, decisions, evidence, gaps, and compatibility constraints. +Pass forward the system id/version, composite hash, quality contract, specialist references, maturity, decisions, blockers, compatibility, and required approval. diff --git a/framework/skills/engineering-system/agents/openai.yaml b/framework/skills/engineering-system/agents/openai.yaml index cb0ad90..799ad7d 100644 --- a/framework/skills/engineering-system/agents/openai.yaml +++ b/framework/skills/engineering-system/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "Engineering System" - short_description: "Spec Framework workflow: Engineering System" - default_prompt: "Use $engineering-system to follow the Engineering System workflow." + short_description: "Consolidate and validate the Engineering System" + default_prompt: "Use $engineering-system to consolidate, version, validate, or migrate the Engineering System aggregate and Quality System after specialist contracts are ready." diff --git a/framework/skills/engineering-system/assets/engineering-system-template.md b/framework/skills/engineering-system/assets/engineering-system-template.md index dad9d25..06c15bc 100644 --- a/framework/skills/engineering-system/assets/engineering-system-template.md +++ b/framework/skills/engineering-system/assets/engineering-system-template.md @@ -33,14 +33,14 @@ ## Architecture -| Area | Contract | Evidence | Maturity | -| --- | --- | --- | --- | -| System context | [architecture/system-context.md](architecture/system-context.md) | `[code/config/decision path]` | `baseline | mapped | governed | verified | operated` | -| Modules | [architecture/modules.md](architecture/modules.md) | `[code/test path]` | `baseline | mapped | governed | verified | operated` | -| Technical catalog | [catalog/catalog.yaml](catalog/catalog.yaml) | `[catalog/entity evidence]` | `baseline | mapped | governed | verified | operated` | -| Data ownership | `[path or Not configured]` | `[evidence]` | `[maturity]` | -| Integrations | `[path or Not configured]` | `[evidence]` | `[maturity]` | -| Deployment | `[path or Not configured]` | `[evidence]` | `[maturity]` | +| Area | Owner skill | Contract | Evidence | Maturity | +| --- | --- | --- | --- | --- | +| System context | `technical-landscape` | [architecture/system-context.md](architecture/system-context.md) | `[code/config/decision path]` | `baseline | mapped | governed | verified | operated` | +| Modules | `technical-landscape` | [architecture/modules.md](architecture/modules.md) | `[code/test path]` | `baseline | mapped | governed | verified | operated` | +| Technical catalog | `technical-landscape` | [catalog/catalog.yaml](catalog/catalog.yaml) | `[catalog/entity evidence]` | `baseline | mapped | governed | verified | operated` | +| Data ownership | `technical-landscape` | `[path or Not configured]` | `[evidence]` | `[maturity]` | +| Integrations | `technical-landscape` | `[path or Not configured]` | `[evidence]` | `[maturity]` | +| Deployment | `operations-baseline` | `[path or Not configured]` | `[evidence]` | `[maturity]` | ## Standards And Quality @@ -83,11 +83,11 @@ only through a scoped, governed exception or approved decision. ## Handoff -Next: `technical-discovery` for delivery-specific work. +Next: `engineering-orchestrator` for readiness review and the human approval gate. ## ✅ Agent Verification Checklist - [ ] Scope, version, origin, mechanical catalog, and architecture boundaries are consistent. -- [ ] Module, data, integration, standards, quality, security, and operational ownership are explicit. +- [ ] Technical landscape, standards, operations, evidence, and quality contracts are linked to their specialist owners without duplicated ownership. - [ ] Decisions, deviations, migrations, consumers, and compatibility expectations are traceable. - [ ] The handoff identifies required downstream pins, evidence, and unresolved system gaps. diff --git a/framework/skills/engineering-system/assets/engineering-system-template.yaml b/framework/skills/engineering-system/assets/engineering-system-template.yaml index 14b66a1..e2e9905 100644 --- a/framework/skills/engineering-system/assets/engineering-system-template.yaml +++ b/framework/skills/engineering-system/assets/engineering-system-template.yaml @@ -7,30 +7,37 @@ scope: product areas: system_context: contract: architecture/system-context.md + owner_skill: technical-landscape maturity: baseline evidence: [] modules: contract: architecture/modules.md + owner_skill: technical-landscape maturity: baseline evidence: [] technical_catalog: contract: catalog/catalog.yaml + owner_skill: technical-landscape maturity: baseline evidence: [] standards: contract: standards/standards.yaml + owner_skill: engineering-standards maturity: baseline evidence: [] quality: contract: quality/quality-system.md + owner_skill: engineering-system maturity: baseline evidence: [] operations: contract: operations/operations.yaml + owner_skill: operations-baseline maturity: baseline evidence: [] evidence: contract: evidence/inventory.md + owner_skill: engineering-evidence maturity: baseline evidence: [] decisions: [] diff --git a/framework/skills/framework-guide/SKILL.md b/framework/skills/framework-guide/SKILL.md index ef510c5..4d5e0b9 100644 --- a/framework/skills/framework-guide/SKILL.md +++ b/framework/skills/framework-guide/SKILL.md @@ -24,7 +24,7 @@ Human goal; current working directory; canonical `product/.product/framework.jso Intent summary; discovered state; recommended command or specialist; mutation preview; gate explanation; result summary; next safe action; explicit blocker when human authority is required. ## Required reading -- The pinned runtime contracts: [`execution-runtime.md`](../../docs/execution-runtime.md), [`engineering-systems.md`](../../docs/engineering-systems.md), and [`lifecycle-and-approvals.md`](../../docs/lifecycle-and-approvals.md). +- The pinned runtime contracts: [`execution-runtime.md`](../../docs/execution-runtime.md), [`engineering-systems.md`](../../docs/engineering-systems.md), [`engineering-catalog-and-standards.md`](../../docs/engineering-catalog-and-standards.md), and [`lifecycle-and-approvals.md`](../../docs/lifecycle-and-approvals.md). - The pinned runtime's `AGENTS.framework.md` common agent rules. - the framework root's `FRAMEWORK.md` - The active product root's `BOOTSTRAP.md` when it exists. @@ -76,7 +76,8 @@ Intent summary; discovered state; recommended command or specialist; mutation pr | Diagnose or install an optional visual adapter | `adapters list/status/doctor`, then version-pinned `install/update --yes` after preview | | Inspect a decision change | `impact` | | Prepare implementation | `gates`, Graph readiness, then Task readiness | -| Establish, inspect, or migrate shared engineering contracts | `engineering-system inspect/validate/migrate` plus Engineering System, then Technical Discovery for a delivery | +| Establish or evolve the shared engineering baseline | `engineering-orchestrator`, which routes Technical Landscape, Engineering Standards, Operations Baseline, Engineering Evidence, and Engineering System consolidation | +| Inspect, validate, or migrate the aggregate Engineering System | `engineering-system inspect/validate/migrate`, then return to `engineering-orchestrator` for readiness and approval | | Review a proposed technical solution | Engineering Proposal, then independent Engineering Review | | Execute governed commands | Command Planner, then Command Executor | | Validate repository state | `validate` | diff --git a/framework/skills/operations-baseline/SKILL.md b/framework/skills/operations-baseline/SKILL.md new file mode 100644 index 0000000..9b5656e --- /dev/null +++ b/framework/skills/operations-baseline/SKILL.md @@ -0,0 +1,60 @@ +--- +name: operations-baseline +description: "Operations Baseline Skill. Use when an agent needs to create, evolve, or audit shared environments, deployments, release and rollback strategies, observability, service levels, continuity, and runbooks." +--- + +# Operations Baseline Skill + +## Layer +Planning + +## Responsibility +Own the shared operations catalog and on-demand environment, deployment, runbook, service-level, observability, continuity, release, and rollback contracts. It does not execute deployments, respond to incidents, or own delivery-specific rollout approval. + +## Operating modes +- create: establish the first evidence-backed operations baseline. +- update: evolve operations contracts after platform or topology changes. +- adopt: reference authoritative external operational sources. +- audit: find missing runbooks, unsafe recovery assumptions, stale ownership, and unsupported service claims. +- explain: summarize operational capability and gaps. + +## Inputs +Technical graph; deployment configuration; infrastructure; runtime and observability evidence; incident knowledge; quality and security constraints; approved decisions. + +## Outputs +`engineering/operations/operations.yaml`; environment and deployment records; runbooks; service-level, observability, continuity, release, and rollback contracts. + +## Required reading +- [`engineering-systems.md`](../../docs/engineering-systems.md) and [`engineering-catalog-and-standards.md`](../../docs/engineering-catalog-and-standards.md). +- the framework root's `FRAMEWORK.md` +- Relevant parent context.md files. +- This skill owns every generation resource in `assets/`. +- Approved decisions are discovered through the active product root's `.product/decisions.json`. + +## Discovery and challenge + +Follow the shared [Discovery And Challenge contract](../discovery-and-challenge.md) before material service-level, recovery, data-loss, rollout, or ownership choices. + +## Workflow +1. Read the technical graph and inspect deployment, infrastructure, observability, incident, continuity, and runbook evidence. +2. Inventory real environments, deployable units, release paths, rollback mechanisms, dependencies, service objectives, and operational owners. +3. Distinguish observed capability from intended or hypothetical capability; do not claim recovery, monitoring, or rollback without evidence. +4. Update the root operations catalog and create detailed records only for evidenced or explicitly proposed operational surfaces. +5. Link operational deployment records to technical `DEPLOY-*`, application, component, interface, and data-store IDs. +6. Record gaps and decision candidates for ownership, recovery objectives, data loss, observability, continuity, and irreversible rollout risk. +7. Validate safe references and return the baseline to `engineering-orchestrator` without performing operational changes. + +## Quality checklist +- [ ] Preserves traceability to affected artifacts. +- [ ] Uses the correct template and naming conventions. +- [ ] Environments, deployments, dependencies, owners, release, and rollback are explicit. +- [ ] Runbooks have triggers, safe actions, validation, escalation, and recovery boundaries. +- [ ] Service-level and continuity claims are supported by evidence or marked as hypotheses. +- [ ] Detects gaps, conflicts, and dependencies. +- [ ] Records or requests decisions for meaningful changes. +- [ ] Leaves a clear handoff for the next skill or orchestrator. + +## Handoff +Next: `engineering-orchestrator`. + +Pass forward catalogs, environment and deployment records, runbooks, service objectives, evidence, operational gaps, decisions, and risks. diff --git a/framework/skills/operations-baseline/agents/openai.yaml b/framework/skills/operations-baseline/agents/openai.yaml new file mode 100644 index 0000000..ec4d3ba --- /dev/null +++ b/framework/skills/operations-baseline/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Operations Baseline" + short_description: "Model shared operational capability and runbooks" + default_prompt: "Use $operations-baseline to model environments, deployments, release, rollback, observability, continuity, and runbooks." diff --git a/framework/skills/operations-baseline/assets/deployment-template.yaml b/framework/skills/operations-baseline/assets/deployment-template.yaml new file mode 100644 index 0000000..eb98fdf --- /dev/null +++ b/framework/skills/operations-baseline/assets/deployment-template.yaml @@ -0,0 +1,12 @@ +schema_version: 1 +id: DEPLOY-NAME +status: draft +technical_entity: DEPLOY-001 +environment: ENV-NAME +applications: [] +components: [] +release_strategy: "[strategy or Not configured]" +rollback_strategy: "[strategy or Not configured]" +observability: [] +runbooks: [] +evidence: [] diff --git a/framework/skills/operations-baseline/assets/environment-template.yaml b/framework/skills/operations-baseline/assets/environment-template.yaml new file mode 100644 index 0000000..fa86542 --- /dev/null +++ b/framework/skills/operations-baseline/assets/environment-template.yaml @@ -0,0 +1,9 @@ +schema_version: 1 +id: ENV-NAME +status: draft +purpose: "[purpose]" +deployments: [] +dependencies: [] +ownership: + operational: Unassigned +evidence: [] diff --git a/framework/skills/engineering-system/assets/operations-catalog-template.yaml b/framework/skills/operations-baseline/assets/operations-catalog-template.yaml similarity index 65% rename from framework/skills/engineering-system/assets/operations-catalog-template.yaml rename to framework/skills/operations-baseline/assets/operations-catalog-template.yaml index 220e4b1..662c75a 100644 --- a/framework/skills/engineering-system/assets/operations-catalog-template.yaml +++ b/framework/skills/operations-baseline/assets/operations-catalog-template.yaml @@ -1,4 +1,5 @@ schema_version: 1 +owner_skill: operations-baseline environments: {} deployments: {} runbooks: {} diff --git a/framework/skills/operations-baseline/assets/operations-overview-template.md b/framework/skills/operations-baseline/assets/operations-overview-template.md new file mode 100644 index 0000000..a9d24ae --- /dev/null +++ b/framework/skills/operations-baseline/assets/operations-overview-template.md @@ -0,0 +1,15 @@ +# Engineering Operations + +## Operational Surface + +| Environment or deployment | Technical IDs | Owner | Release | Rollback | Evidence | +| --- | --- | --- | --- | --- | --- | +| `[ID]` | `[APP/CMP/DEPLOY-*]` | `[owner]` | `[strategy]` | `[strategy]` | `[paths]` | + +## Service And Continuity + +[Service objectives, observability, dependencies, recovery boundaries, and explicit hypotheses.] + +## Gaps + +[Missing ownership, runbooks, evidence, rollback, recovery, monitoring, or decisions.] diff --git a/framework/skills/operations-baseline/assets/release-strategy-template.md b/framework/skills/operations-baseline/assets/release-strategy-template.md new file mode 100644 index 0000000..e9837c1 --- /dev/null +++ b/framework/skills/operations-baseline/assets/release-strategy-template.md @@ -0,0 +1,13 @@ +# Release And Rollback Strategy + +## Scope + +[Applications, components, deployments, environments, and dependencies.] + +## Release + +[Mechanism, ordering, compatibility, validation, observation, and stop conditions.] + +## Rollback And Recovery + +[Rollback mechanism, data implications, recovery objectives, evidence, and escalation.] diff --git a/framework/skills/operations-baseline/assets/runbook-template.md b/framework/skills/operations-baseline/assets/runbook-template.md new file mode 100644 index 0000000..601623a --- /dev/null +++ b/framework/skills/operations-baseline/assets/runbook-template.md @@ -0,0 +1,17 @@ +# [RUNBOOK-*] — [Scenario] + +## Trigger And Scope + +[Observable trigger, affected technical IDs, and explicit non-goals.] + +## Preconditions And Safety + +[Required access, backups, evidence, approvals, and stop conditions.] + +## Procedure + +1. [Safe diagnostic or recovery action.] + +## Validation, Escalation, And Rollback + +[Expected recovery evidence, escalation owner, rollback, and unresolved risk.] diff --git a/framework/skills/operations-baseline/assets/service-level-template.yaml b/framework/skills/operations-baseline/assets/service-level-template.yaml new file mode 100644 index 0000000..c63126c --- /dev/null +++ b/framework/skills/operations-baseline/assets/service-level-template.yaml @@ -0,0 +1,9 @@ +schema_version: 1 +id: SLO-001 +status: draft +scope: [] +indicator: "[measurable indicator]" +objective: "[target and period]" +measurement_source: "[evidence reference]" +owner: Unassigned +exceptions: [] diff --git a/framework/skills/product-orchestrator/SKILL.md b/framework/skills/product-orchestrator/SKILL.md index 0c6063b..838e4a1 100644 --- a/framework/skills/product-orchestrator/SKILL.md +++ b/framework/skills/product-orchestrator/SKILL.md @@ -19,7 +19,7 @@ Orchestrator. Controls workflow, gates, handoffs, and approval checkpoints. It s - Approved decisions are discovered through the active product root's `.product/decisions.json`; resolve each registered `path` from its declared domain root. ## Default sequence -Problem Discovery -> Vision -> Strategy -> Product Landscape -> Knowledge, Engineering System, and Design System baselines -> Domain Architect -> User Goal -> Roadmap alignment +Problem Discovery -> Vision -> Strategy -> Product Landscape -> Knowledge baseline -> Engineering Orchestrator -> Design System baseline -> Domain Architect -> User Goal -> Roadmap alignment For a repository whose canonical manifest declares `starting_point: existing-feature`, replace that sequence with Feature Brief -> explicit individual approval -> existing Feature selection -> workspace. Escalate to the default sequence when the feature cannot be bounded without product-wide decisions. @@ -44,7 +44,7 @@ Use `scripts/inventory-product-landscape.ps1` on Windows or `scripts/inventory-p 8. For `existing-implementation`, own `knowledge/assessments/implementation-assessment.md`, do not modify application code during assessment, and retain the full Foundation sequence before workspace creation. 9. For `existing-product`, own `foundation/product-baseline.md`, keep uncertain intent visible, and promote to the full Foundation sequence when the baseline cannot establish audience and delivered value confidently. 10. Before Domain modeling, inventory every declared code root comprehensively in `knowledge/assessments/product-landscape.md`; cover modules, user surfaces, data, integrations, business rules, tests, configuration, design assets, and operational constraints. Do not collapse a broad product into one feature-sized domain. -11. Establish `knowledge/`, `engineering/`, and `design/system/` as evidence-backed baselines when code exists, or as explicit draft hypotheses when it does not. Their approval is required before a Specification. +11. Establish `knowledge/`, `engineering/`, and `design/system/` as evidence-backed baselines when code exists, or as explicit draft hypotheses when it does not. Route the complete engineering baseline through `engineering-orchestrator`; all three baseline approvals are required before a Specification. 12. Keep implementation repositories as semantic siblings of `product/`; when code must be created, route stack selection and official scaffold-command verification before any implementation action. ## Outputs diff --git a/framework/skills/technical-landscape/SKILL.md b/framework/skills/technical-landscape/SKILL.md new file mode 100644 index 0000000..569586a --- /dev/null +++ b/framework/skills/technical-landscape/SKILL.md @@ -0,0 +1,61 @@ +--- +name: technical-landscape +description: "Technical Landscape Skill. Use when an agent needs to discover, create, evolve, or audit the shared graph of systems, applications, components, repositories, interfaces, data stores, deployments, and their relations." +--- + +# Technical Landscape Skill + +## Layer +Planning + +## Responsibility +Own the shared system context, module map, technical entity catalog, topology, entity records, and boundary assessments under `engineering/`. It does not define standards, operational procedures, delivery-specific changes, or application code. + +## Operating modes +- create: establish the first evidence-backed technical graph. +- update: evolve entities and relations while preserving stable IDs. +- adopt: map authoritative external architecture sources into local references. +- audit: find omitted roots, boundaries, ownership, relations, and stale evidence. +- explain: summarize the topology and its gaps. + +## Inputs +Product Landscape; declared code roots; repositories; manifests; source and test trees; configuration; deployment evidence; approved decisions. + +## Outputs +`engineering/architecture/system-context.md`; `engineering/architecture/modules.md`; `engineering/catalog/catalog.yaml`; `engineering/architecture/topology.yaml`; on-demand entity YAML and Markdown records; technical landscape and boundary assessments; relation findings. + +## Required reading +- [`engineering-catalog-and-standards.md`](../../docs/engineering-catalog-and-standards.md). +- the framework root's `FRAMEWORK.md` +- Relevant parent context.md files. +- This skill owns every generation resource in `assets/` and the read-only inventory scripts in `scripts/`. +- Approved decisions are discovered through the active product root's `.product/decisions.json`. + +## Discovery and challenge + +Follow the shared [Discovery And Challenge contract](../discovery-and-challenge.md) before material boundary, ownership, identity, split, merge, or adoption choices. + +## Workflow +1. Run the platform-appropriate technical landscape inventory script and inspect every declared code root, not only the first repository or dominant application. +2. Identify systems, applications, components, repositories, interfaces, data stores, deployments, ownership, capabilities, and observable relations. +3. Distinguish observed evidence from inference and explicit hypotheses; do not convert folder nesting into ownership automatically. +4. Reuse stable IDs when meaning is unchanged. Record move, split, merge, replacement, or compatibility implications when identity changes. +5. Update `catalog.yaml` and `topology.yaml`; create entity records only when evidence or explicit hypotheses justify them. +6. Validate entity identity, type, safe references, relation endpoints, and coverage of every declared code root. +7. Record decision candidates for unresolved boundaries, ownership, data, integration, or repository structure. +8. Return the current graph and gaps to `engineering-orchestrator` without changing aggregate status or approvals. + +## Quality checklist +- [ ] Preserves traceability to affected artifacts. +- [ ] Uses the correct template and naming conventions. +- [ ] Covers monorepo, polyrepo, shared-component, and multi-deployment shapes without path-derived ownership. +- [ ] Every entity and relation has a stable ID and evidence or explicit hypothesis. +- [ ] Every declared code root is represented or reported as a gap. +- [ ] Detects gaps, conflicts, and dependencies. +- [ ] Records or requests decisions for meaningful changes. +- [ ] Leaves a clear handoff for the next skill or orchestrator. + +## Handoff +Next: `engineering-orchestrator`. + +Pass forward catalogs, topology, entity records, source evidence, assumptions, missing coverage, decision candidates, and compatibility risks. diff --git a/framework/skills/technical-landscape/agents/openai.yaml b/framework/skills/technical-landscape/agents/openai.yaml new file mode 100644 index 0000000..6088149 --- /dev/null +++ b/framework/skills/technical-landscape/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Technical Landscape" + short_description: "Map the complete technical entity graph" + default_prompt: "Use $technical-landscape to map systems, applications, components, repositories, interfaces, data stores, deployments, and relations." diff --git a/framework/skills/technical-landscape/assets/boundary-assessment-template.md b/framework/skills/technical-landscape/assets/boundary-assessment-template.md new file mode 100644 index 0000000..05202a9 --- /dev/null +++ b/framework/skills/technical-landscape/assets/boundary-assessment-template.md @@ -0,0 +1,17 @@ +# Technical Boundary Assessment: [scope] + +## Evidence + +[Repositories, manifests, source, configuration, tests, and runtime references inspected.] + +## Observed Boundary + +[Responsibilities, ownership, dependencies, interfaces, and data stores.] + +## Inference And Hypotheses + +[Claims that are not directly established by evidence.] + +## Recommendation + +[Keep, split, merge, rename, or request a decision, including compatibility impact.] diff --git a/framework/skills/technical-landscape/assets/module-map-template.md b/framework/skills/technical-landscape/assets/module-map-template.md new file mode 100644 index 0000000..ef33bf7 --- /dev/null +++ b/framework/skills/technical-landscape/assets/module-map-template.md @@ -0,0 +1,9 @@ +# Module Map + +| Module or component ID | Responsibility | Owns | Allowed dependencies | Evidence | +| --- | --- | --- | --- | --- | +| `[CMP-* or module]` | `[responsibility]` | `[data/capability]` | `[stable IDs]` | `[code/test path]` | + +## Constraints And Gaps + +[Dependency rules, cycles, unowned modules, inferred boundaries, and decisions required.] diff --git a/framework/skills/technical-landscape/assets/system-context-template.md b/framework/skills/technical-landscape/assets/system-context-template.md new file mode 100644 index 0000000..a8c9332 --- /dev/null +++ b/framework/skills/technical-landscape/assets/system-context-template.md @@ -0,0 +1,15 @@ +# System Context + +## Scope And Evidence + +[Product boundary, code roots, observed sources, and explicit limitations.] + +## Actors And Systems + +| Actor or system | Responsibility | Ownership | Trust or deployment boundary | Evidence | +| --- | --- | --- | --- | --- | +| `[name or stable ID]` | `[responsibility]` | `[owned | external | shared]` | `[boundary]` | `[path]` | + +## Gaps And Decisions + +[Unresolved ownership, trust, integration, or boundary decisions.] diff --git a/framework/skills/engineering-system/assets/technical-catalog-template.yaml b/framework/skills/technical-landscape/assets/technical-catalog-template.yaml similarity index 90% rename from framework/skills/engineering-system/assets/technical-catalog-template.yaml rename to framework/skills/technical-landscape/assets/technical-catalog-template.yaml index 92896e3..5dfd89c 100644 --- a/framework/skills/engineering-system/assets/technical-catalog-template.yaml +++ b/framework/skills/technical-landscape/assets/technical-catalog-template.yaml @@ -1,4 +1,5 @@ schema_version: 1 +owner_skill: technical-landscape entities: systems: {} applications: {} diff --git a/framework/skills/engineering-system/assets/technical-entity-template.md b/framework/skills/technical-landscape/assets/technical-entity-template.md similarity index 100% rename from framework/skills/engineering-system/assets/technical-entity-template.md rename to framework/skills/technical-landscape/assets/technical-entity-template.md diff --git a/framework/skills/engineering-system/assets/technical-entity-template.yaml b/framework/skills/technical-landscape/assets/technical-entity-template.yaml similarity index 100% rename from framework/skills/engineering-system/assets/technical-entity-template.yaml rename to framework/skills/technical-landscape/assets/technical-entity-template.yaml diff --git a/framework/skills/technical-landscape/assets/technical-landscape-template.md b/framework/skills/technical-landscape/assets/technical-landscape-template.md new file mode 100644 index 0000000..a4d848a --- /dev/null +++ b/framework/skills/technical-landscape/assets/technical-landscape-template.md @@ -0,0 +1,23 @@ +# Technical Landscape + +## Scope And Evidence + +| Code root or source | Role | Coverage | Evidence or limitation | +| --- | --- | --- | --- | +| `[path]` | `[web | api | worker | library | infrastructure | other]` | `[complete | partial | missing]` | `[paths or reason]` | + +## Entity Summary + +| Entity ID | Type | Responsibility | Repositories | Evidence | +| --- | --- | --- | --- | --- | +| `[ID]` | `[type]` | `[boundary]` | `[REPO-*]` | `[paths]` | + +## Relations And Boundaries + +[Describe material topology, ownership, interfaces, data boundaries, and unresolved hypotheses without duplicating the mechanical catalog.] + +## Gaps And Decisions + +| Finding | Impact | Owner or decision candidate | +| --- | --- | --- | +| `[gap]` | `[impact]` | `[owner or DEC-* candidate]` | diff --git a/framework/skills/technical-landscape/assets/topology-template.yaml b/framework/skills/technical-landscape/assets/topology-template.yaml new file mode 100644 index 0000000..650e292 --- /dev/null +++ b/framework/skills/technical-landscape/assets/topology-template.yaml @@ -0,0 +1,7 @@ +schema_version: 1 +owner_skill: technical-landscape +systems: [] +applications: [] +components: [] +repositories: [] +relations: [] diff --git a/framework/skills/technical-landscape/scripts/inventory-technical-landscape.ps1 b/framework/skills/technical-landscape/scripts/inventory-technical-landscape.ps1 new file mode 100644 index 0000000..f744ca2 --- /dev/null +++ b/framework/skills/technical-landscape/scripts/inventory-technical-landscape.ps1 @@ -0,0 +1,19 @@ +[CmdletBinding()] +param([string]$RepositoryRoot = ".", [string]$ProductRoot = "product") + +$ErrorActionPreference = "Stop" +$repo = (Resolve-Path $RepositoryRoot).Path +$manifestPath = Join-Path $repo (Join-Path $ProductRoot ".product/framework.json") +$manifest = Get-Content -Raw $manifestPath | ConvertFrom-Json +Write-Output "# Technical landscape inventory" +foreach ($root in $manifest.code_roots) { + $path = Join-Path $repo $root.path + Write-Output "`n## code-root: $($root.path)" + Write-Output "role: $($root.role)" + if (-not (Test-Path -LiteralPath $path -PathType Container)) { Write-Output "coverage: missing"; continue } + Write-Output "coverage: inspect" + Get-ChildItem -LiteralPath $path -Force -File -Recurse -ErrorAction SilentlyContinue | + Where-Object { $_.Name -match '^(go\.mod|package\.json|pyproject\.toml|Cargo\.toml|pom\.xml|build\.gradle|settings\.gradle|Dockerfile|docker-compose.*|.*\.ya?ml|.*\.proto|.*\.graphql|.*\.tf)$' -or $_.Extension -in @(".sln", ".csproj", ".fsproj") } | + Sort-Object FullName | + ForEach-Object { Write-Output ("signal: " + $_.FullName.Substring($repo.Length + 1).Replace("\", "/")) } +} diff --git a/framework/skills/technical-landscape/scripts/inventory-technical-landscape.sh b/framework/skills/technical-landscape/scripts/inventory-technical-landscape.sh new file mode 100644 index 0000000..a296230 --- /dev/null +++ b/framework/skills/technical-landscape/scripts/inventory-technical-landscape.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail +repository_root="."; product_root="product" +while [[ $# -gt 0 ]]; do case "$1" in --repository-root) repository_root="$2"; shift 2;; --product-root) product_root="$2"; shift 2;; *) echo "Usage: $0 [--repository-root ] [--product-root ]" >&2; exit 2;; esac; done +repo="$(cd "$repository_root" && pwd)"; manifest="$repo/$product_root/.product/framework.json" +[[ -f "$manifest" ]] || { echo "Spec Framework manifest not found: $manifest" >&2; exit 1; } +echo "# Technical landscape inventory" +roots="$(awk -F '"' '/^[[:space:]]*"path"[[:space:]]*:/ { path=$4 } /^[[:space:]]*"role"[[:space:]]*:/ { if (path != "") { print path "\t" $4; path="" } }' "$manifest")" +while IFS=$'\t' read -r path role; do + [[ -n "$path" ]] || continue; root="$repo/$path"; echo; echo "## code-root: $path"; echo "role: $role" + [[ -d "$root" ]] || { echo "coverage: missing"; continue; } + echo "coverage: inspect" + find "$root" -type f \( -name go.mod -o -name package.json -o -name pyproject.toml -o -name Cargo.toml -o -name pom.xml -o -name build.gradle -o -name settings.gradle -o -name '*.sln' -o -name '*.csproj' -o -name '*.fsproj' -o -name Dockerfile -o -name 'docker-compose*' -o -name '*.yml' -o -name '*.yaml' -o -name '*.proto' -o -name '*.graphql' -o -name '*.tf' \) -print | sort | while IFS= read -r file; do echo "signal: ${file#"$repo/"}"; done +done <<< "$roots" diff --git a/framework/validators/README.md b/framework/validators/README.md index d451890..1bcaeef 100644 --- a/framework/validators/README.md +++ b/framework/validators/README.md @@ -44,7 +44,7 @@ The validator checks: - decision index paths in `.product/decisions.json`; - visual Design source identity, immutable versions, authority, screen inventories, snapshot paths, origin mode, maturity, fidelity policy, source references, and requirement coverage values; - canonical Design System identity, status, semantic version, token schema, aliases and cycles, plus proposed-or-later Design consumer pins; -- canonical Engineering System identity, semantic version, origin, catalog areas, technical entity graph, standards profiles and exceptions, operations references, maturity evidence, contract paths, and structured delivery triggers; +- canonical Engineering System identity, semantic version, origin, specialist area ownership, catalog areas, technical entity graph, standards profiles and exceptions, operations references, maturity evidence, contract paths, and structured delivery triggers; - Engineering Proposal consumer pins, Engineering Review proposal hashes, and current approval evidence for operational navigation; - decision references against `.product/decisions.json`, including approved delivery dependencies; - visual Mermaid standards for flowcharts; diff --git a/internal/cli/engineering_system.go b/internal/cli/engineering_system.go index 6b11077..1575986 100644 --- a/internal/cli/engineering_system.go +++ b/internal/cli/engineering_system.go @@ -67,7 +67,11 @@ func runEngineeringSystem(args []string, stdout, stderr io.Writer) int { } else { fmt.Fprintf(stdout, "Engineering System: %s\n- Status: %s\n- Version: %s\n- Origin: %s\n- Scope: %s\n- Areas: %d\n- Decisions: %d\n- Standards: %d\n- Fitness functions: %d\n", inspection.ID, inspection.Status, inspection.Version, inspection.OriginMode, inspection.Scope, len(inspection.Areas), inspection.Decisions, inspection.Standards, inspection.FitnessFunctions) for _, area := range inspection.Areas { - fmt.Fprintf(stdout, "- %s: %s (%s, evidence=%d)\n", area.Name, area.Maturity, area.Contract, area.Evidence) + owner := "legacy-or-unspecified" + if area.OwnerSkill != "" { + owner = area.OwnerSkill + } + fmt.Fprintf(stdout, "- %s: %s (%s, owner=%s, evidence=%d)\n", area.Name, area.Maturity, area.Contract, owner, area.Evidence) } for _, blocker := range inspection.Blockers { fmt.Fprintln(stdout, "BLOCKED:", blocker) diff --git a/internal/engineeringsystem/system.go b/internal/engineeringsystem/system.go index ca325f9..9c84172 100644 --- a/internal/engineeringsystem/system.go +++ b/internal/engineeringsystem/system.go @@ -52,11 +52,22 @@ var allowedStandardLevels = map[string]bool{ "required": true, "recommended": true, "experimental": true, "deprecated": true, } +var expectedAreaOwners = map[string]string{ + "system_context": "technical-landscape", + "modules": "technical-landscape", + "technical_catalog": "technical-landscape", + "standards": "engineering-standards", + "quality": "engineering-system", + "operations": "operations-baseline", + "evidence": "engineering-evidence", +} + type Area struct { - Name string `json:"name"` - Contract string `json:"contract"` - Maturity string `json:"maturity"` - Evidence int `json:"evidence"` + Name string `json:"name"` + Contract string `json:"contract"` + Maturity string `json:"maturity"` + Evidence int `json:"evidence"` + OwnerSkill string `json:"ownerSkill,omitempty"` } type Inspection struct { @@ -100,9 +111,10 @@ type catalogDocument struct { } type catalogArea struct { - Contract string `yaml:"contract"` - Maturity string `yaml:"maturity"` - Evidence []string `yaml:"evidence"` + Contract string `yaml:"contract"` + Maturity string `yaml:"maturity"` + Evidence []string `yaml:"evidence"` + OwnerSkill string `yaml:"owner_skill"` } type qualityCatalogDocument struct { @@ -146,6 +158,7 @@ type qualityExceptionRecord struct { type technicalCatalogDocument struct { SchemaVersion int `yaml:"schema_version"` + OwnerSkill string `yaml:"owner_skill"` Entities map[string]map[string]string `yaml:"entities"` Relations []technicalRelationDocument `yaml:"relations"` } @@ -167,6 +180,7 @@ type technicalRelationDocument struct { type standardsCatalogDocument struct { SchemaVersion int `yaml:"schema_version"` + OwnerSkill string `yaml:"owner_skill"` Profiles map[string]string `yaml:"profiles"` Standards map[string]string `yaml:"standards"` Exceptions map[string]string `yaml:"exceptions"` @@ -213,6 +227,7 @@ type standardExceptionDocument struct { type operationsCatalogDocument struct { SchemaVersion int `yaml:"schema_version"` + OwnerSkill string `yaml:"owner_skill"` Environments map[string]string `yaml:"environments"` Deployments map[string]string `yaml:"deployments"` Runbooks map[string]string `yaml:"runbooks"` @@ -289,7 +304,7 @@ func Inspect(root string) (Inspection, error) { i.Blockers = append(i.Blockers, "catalog areas are missing") } for name, source := range catalog.Areas { - area := Area{Name: name, Contract: source.Contract, Maturity: source.Maturity, Evidence: len(source.Evidence)} + area := Area{Name: name, Contract: source.Contract, Maturity: source.Maturity, Evidence: len(source.Evidence), OwnerSkill: source.OwnerSkill} i.Areas = append(i.Areas, area) if area.Contract == "" || area.Maturity == "" { i.Blockers = append(i.Blockers, fmt.Sprintf("area %s is missing contract or maturity", name)) @@ -298,6 +313,9 @@ func Inspect(root string) (Inspection, error) { if !allowedMaturity[area.Maturity] { i.Blockers = append(i.Blockers, fmt.Sprintf("area %s has invalid maturity %s", name, area.Maturity)) } + if expected := expectedAreaOwners[name]; area.OwnerSkill != "" && expected != "" && area.OwnerSkill != expected { + i.Blockers = append(i.Blockers, fmt.Sprintf("area %s owner_skill must be %s", name, expected)) + } contractPath := filepath.Clean(filepath.Join(dir, filepath.FromSlash(area.Contract))) relative, relErr := filepath.Rel(dir, contractPath) if relErr != nil || relative == ".." || strings.HasPrefix(relative, ".."+string(filepath.Separator)) || filepath.IsAbs(filepath.FromSlash(area.Contract)) { @@ -346,6 +364,9 @@ func validateTechnicalCatalog(engineeringDir, contract string) []string { if catalog.SchemaVersion != 1 { blockers = append(blockers, "technical catalog schema_version must be 1") } + if catalog.OwnerSkill != "" && catalog.OwnerSkill != "technical-landscape" { + blockers = append(blockers, "technical catalog owner_skill must be technical-landscape") + } for category := range catalog.Entities { if technicalEntityPrefixes[category] == "" { blockers = append(blockers, "technical catalog has unknown entity category "+category) @@ -398,6 +419,9 @@ func validateStandardsCatalog(engineeringDir, contract string) []string { if catalog.SchemaVersion != 1 { blockers = append(blockers, "standards catalog schema_version must be 1") } + if catalog.OwnerSkill != "" && catalog.OwnerSkill != "engineering-standards" { + blockers = append(blockers, "standards catalog owner_skill must be engineering-standards") + } base := filepath.Dir(path) profiles := map[string]standardProfileDocument{} for id, reference := range catalog.Profiles { @@ -511,6 +535,9 @@ func validateOperationsCatalog(engineeringDir, contract string) []string { if catalog.SchemaVersion != 1 { blockers = append(blockers, "operations catalog schema_version must be 1") } + if catalog.OwnerSkill != "" && catalog.OwnerSkill != "operations-baseline" { + blockers = append(blockers, "operations catalog owner_skill must be operations-baseline") + } for category, entries := range map[string]map[string]string{"environment": catalog.Environments, "deployment": catalog.Deployments, "runbook": catalog.Runbooks} { for id, reference := range entries { blockers = append(blockers, validateCatalogReference(engineeringDir, filepath.Dir(path), reference, category+" "+id)...) diff --git a/internal/engineeringsystem/system_test.go b/internal/engineeringsystem/system_test.go index dd6a41f..466abac 100644 --- a/internal/engineeringsystem/system_test.go +++ b/internal/engineeringsystem/system_test.go @@ -130,7 +130,7 @@ func TestInspectRejectsInvalidStandardInheritanceAndMissingCatalogRecord(t *test files := map[string]string{ "context.md": "---\nid: ENGSYS-TEST-001\nstatus: draft\nversion: 1.0.0\norigin_mode: generate\n---\n", "engineering-system.md": "| Field | Value |\n| --- | --- |\n| ID | `ENGSYS-TEST-001` |\n| Status | `draft` |\n| Version | `1.0.0` |\n", - "engineering-system.yaml": "schema_version: 1\nid: ENGSYS-TEST-001\nstatus: draft\nversion: 1.0.0\norigin_mode: generate\nscope: product\nareas:\n technical_catalog: {contract: catalog/catalog.yaml, maturity: baseline, evidence: []}\n standards: {contract: standards/standards.yaml, maturity: baseline, evidence: []}\n", + "engineering-system.yaml": "schema_version: 1\nid: ENGSYS-TEST-001\nstatus: draft\nversion: 1.0.0\norigin_mode: generate\nscope: product\nareas:\n technical_catalog: {contract: catalog/catalog.yaml, maturity: baseline, evidence: [], owner_skill: engineering-system}\n standards: {contract: standards/standards.yaml, maturity: baseline, evidence: []}\n", "catalog/catalog.yaml": "schema_version: 1\nentities:\n systems: {BAD-ID: systems/missing.yaml}\nrelations:\n - {id: REL-BROKEN, type: depends_on, source: SYS-MISSING, target: APP-MISSING}\n", "standards/standards.yaml": "schema_version: 1\nprofiles: {PROFILE-A: profiles/a.yaml, PROFILE-B: profiles/b.yaml}\nstandards: {}\nexceptions: {}\n", "standards/profiles/a.yaml": "schema_version: 1\nid: PROFILE-A\nversion: 1.0.0\nstatus: draft\nextends: [PROFILE-B]\nstandards: []\n", @@ -150,7 +150,7 @@ func TestInspectRejectsInvalidStandardInheritanceAndMissingCatalogRecord(t *test t.Fatal(err) } joined := strings.Join(inspection.Blockers, "\n") - for _, expected := range []string{"must start with SYS-", "is missing", "unknown source or target", "inheritance cycle"} { + for _, expected := range []string{"owner_skill must be technical-landscape", "must start with SYS-", "is missing", "unknown source or target", "inheritance cycle"} { if !strings.Contains(joined, expected) { t.Fatalf("expected %q in blockers=%v", expected, inspection.Blockers) } diff --git a/internal/install/install_test.go b/internal/install/install_test.go index cd66535..39a196d 100644 --- a/internal/install/install_test.go +++ b/internal/install/install_test.go @@ -268,7 +268,8 @@ func TestInitShipsEngineeringCatalogRootsForEveryAgentTarget(t *testing.T) { t.Setenv("SPEC_FRAMEWORK_CACHE", filepath.Join(t.TempDir(), "cache")) t.Setenv("SPEC_FRAMEWORK_AGENT_HOME", filepath.Join(t.TempDir(), "agents")) target := filepath.Join(t.TempDir(), "product") - if _, err := Init(Options{Target: target, Version: "test", Agents: []Agent{Codex, Cursor, Claude}}); err != nil { + result, err := Init(Options{Target: target, Version: "test", Agents: []Agent{Codex, Cursor, Claude}}) + if err != nil { t.Fatal(err) } for _, file := range []string{ @@ -283,6 +284,20 @@ func TestInitShipsEngineeringCatalogRootsForEveryAgentTarget(t *testing.T) { t.Errorf("missing %s: %v", file, err) } } + for _, skill := range []string{"engineering-orchestrator", "technical-landscape", "engineering-standards", "operations-baseline", "engineering-evidence", "engineering-system"} { + if _, err := os.Stat(filepath.Join(result.SpecRoot, "skills", skill, "SKILL.md")); err != nil { + t.Errorf("runtime missing engineering skill %s: %v", skill, err) + } + } + aggregate, err := os.ReadFile(filepath.Join(target, "product", "engineering", "engineering-system.yaml")) + if err != nil { + t.Fatal(err) + } + for _, owner := range []string{"technical-landscape", "engineering-standards", "operations-baseline", "engineering-evidence", "engineering-system"} { + if !strings.Contains(string(aggregate), "owner_skill: "+owner) { + t.Errorf("engineering aggregate missing owner %s", owner) + } + } owned := filepath.Join(target, "product", "engineering", "catalog", "catalog.yaml") if err := os.WriteFile(owned, []byte("adopter-owned catalog\n"), 0o644); err != nil { t.Fatal(err) diff --git a/internal/runtimeassets/runtime_test.go b/internal/runtimeassets/runtime_test.go index 9e98ad7..7b5e7e2 100644 --- a/internal/runtimeassets/runtime_test.go +++ b/internal/runtimeassets/runtime_test.go @@ -38,7 +38,7 @@ func TestEnsureMaterializesVersionedAssets(t *testing.T) { if err != nil { t.Fatal(err) } - for _, name := range []string{"FRAMEWORK.md", "AGENTS.framework.md", "docs/execution-runtime.md", "docs/engineering-systems.md", "docs/lifecycle-and-approvals.md", "docs/artifact-registry-modules.md", "init/schema.json", "init/catalog.json", "init/bootstrap.json", "init/contracts/new-product.json", "skills/code-runner/SKILL.md", "skills/grill-me/SKILL.md", "skills/grill-me/agents/openai.yaml", "skills/framework-guide/agents/openai.yaml", "skills/framework-guide/scripts/inspect-workspace.ps1", "skills/framework-guide/scripts/inspect-workspace.sh", "skills/framework-guide/scripts/inspect-code-roots.ps1", "skills/product-orchestrator/scripts/inventory-product-landscape.sh", "skills/engineering-system/scripts/inventory-engineering-evidence.ps1", "skills/design-system/scripts/inventory-design-evidence.sh", "skills/artifact-importer/scripts/record-review-and-validate.ps1", "skills/documentation-writer/scripts/validate-artifacts.sh", "skills/discovery-and-challenge.md", "skills/problem-discovery/assets/interview-note-template.md", "skills/problem-discovery/assets/research-summary-template.md", "skills/specification/assets/specification-template.md", "examples/events/domains/events/domain.md", ".complete"} { + for _, name := range []string{"FRAMEWORK.md", "AGENTS.framework.md", "docs/execution-runtime.md", "docs/engineering-systems.md", "docs/lifecycle-and-approvals.md", "docs/artifact-registry-modules.md", "init/schema.json", "init/catalog.json", "init/bootstrap.json", "init/contracts/new-product.json", "skills/code-runner/SKILL.md", "skills/grill-me/SKILL.md", "skills/grill-me/agents/openai.yaml", "skills/framework-guide/agents/openai.yaml", "skills/framework-guide/scripts/inspect-workspace.ps1", "skills/framework-guide/scripts/inspect-workspace.sh", "skills/framework-guide/scripts/inspect-code-roots.ps1", "skills/product-orchestrator/scripts/inventory-product-landscape.sh", "skills/engineering-orchestrator/SKILL.md", "skills/technical-landscape/SKILL.md", "skills/technical-landscape/assets/technical-catalog-template.yaml", "skills/technical-landscape/scripts/inventory-technical-landscape.ps1", "skills/engineering-standards/SKILL.md", "skills/operations-baseline/SKILL.md", "skills/engineering-evidence/SKILL.md", "skills/engineering-evidence/scripts/inventory-engineering-evidence.ps1", "skills/design-system/scripts/inventory-design-evidence.sh", "skills/artifact-importer/scripts/record-review-and-validate.ps1", "skills/documentation-writer/scripts/validate-artifacts.sh", "skills/discovery-and-challenge.md", "skills/problem-discovery/assets/interview-note-template.md", "skills/problem-discovery/assets/research-summary-template.md", "skills/specification/assets/specification-template.md", "examples/events/domains/events/domain.md", ".complete"} { if _, err := os.Stat(filepath.Join(root, filepath.FromSlash(name))); err != nil { t.Fatal(err) } diff --git a/starter/AGENTS.md b/starter/AGENTS.md index 8f8b5e2..e438f27 100644 --- a/starter/AGENTS.md +++ b/starter/AGENTS.md @@ -32,6 +32,8 @@ Do not edit framework-owned assets to encode product scope. Put product-specific Start with Framework Guide unless a current CLI result or explicit human request names both the specialist and the concrete artifact or workspace scope. Revalidate persisted handoffs/checkpoints with `dashboard`, `status`, `next`, or `guide` before routing directly. A skill name without scope is only a hint. For a new product, continue through Product Orchestrator, Problem Discovery, Vision, and Strategy before creating domains or features. For `existing-documents`, let Artifact Importer read every inventoried source and complete `traceability.json` and `mapping.json` before requesting materialization. For a scalable import, claim and review one persisted chunk at a time with source evidence; never mark a chunk reviewed without it. +For the shared engineering baseline, route through Engineering Orchestrator. It sequences Technical Landscape, Engineering Standards, Operations Baseline, Engineering Evidence, and Engineering System consolidation, then stops for human approval before domain modeling or delivery-specific Technical Discovery. + ## Canonical Delivery Flow ```text diff --git a/starter/README.md b/starter/README.md index 3605e2a..517805e 100644 --- a/starter/README.md +++ b/starter/README.md @@ -38,7 +38,7 @@ These are the default `new-product` steps. For every initialized repository, rea 6. Continue through Goal -> Feature -> Use Case -> Specification -> Design -> Technical Discovery -> applicable Engineering Proposal and Engineering Review -> Implementation Plan -> Execution Graph -> Tasks. 7. Replace product gate placeholders in `product/knowledge/conventions/gates.md` before marking executable work as implemented or validated. 8. Use `spec-framework work`, then `resume`, leases, checkpoints, and handoffs so execution can be continued without conversation history. -9. Use `spec-framework engineering-system inspect` for the shared technical baseline, `spec-framework dashboard --work WORK-001` for the consolidated flow, and `spec-framework decisions migrate` to preview legacy decision metadata upgrades. +9. Use `engineering-orchestrator` to establish the shared technical baseline, `spec-framework engineering-system inspect` to inspect its aggregate, `spec-framework dashboard --work WORK-001` for the consolidated flow, and `spec-framework decisions migrate` to preview legacy decision metadata upgrades. 11. Run `spec-framework decisions check --strict` in CI to validate decision paths, domains, approvals, references, and navigable links. 10. For `existing-documents`, ask the Artifact Importer agent to read each source and fill `traceability.json` plus proposed `mapping.json` entries before materialization; for a large source set, use bounded import chunks with evidence per source and review unmapped gaps explicitly. diff --git a/starter/product/engineering/README.md b/starter/product/engineering/README.md index 2d4b09c..c338533 100644 --- a/starter/product/engineering/README.md +++ b/starter/product/engineering/README.md @@ -1,10 +1,12 @@ # Product Engineering -This area owns the versioned Engineering System: architecture, standards, quality attributes, fitness functions, operations, and evidence. +This area contains the versioned Engineering System aggregate and specialist-owned technical landscape, standards, operations, quality, and evidence contracts. ## Start here - Read `context.md`, `engineering-system.md`, and `engineering-system.yaml`. +- Route complete baseline creation and evolution through `engineering-orchestrator`. +- Use `technical-landscape`, `engineering-standards`, `operations-baseline`, and `engineering-evidence` for their respective subtrees; use `engineering-system` only for aggregate and Quality System consolidation. - Base maturity claims on real code and operational evidence. - Model systems, applications, components, repositories, interfaces, data stores, and deployments as stable graph entities under `catalog/`. - Compose verifiable rules through versioned profiles under `standards/`; never weaken inherited required rules without a governed exception. diff --git a/starter/product/engineering/architecture/README.md b/starter/product/engineering/architecture/README.md index 89abd0e..c4dee06 100644 --- a/starter/product/engineering/architecture/README.md +++ b/starter/product/engineering/architecture/README.md @@ -1,5 +1,8 @@ # Product Architecture +Shared topology and boundary records are owned by `technical-landscape`; the +Engineering System aggregate references them without duplicating ownership. + ## Purpose Store stable product architecture knowledge and product ADR references: diff --git a/starter/product/engineering/architecture/topology.yaml b/starter/product/engineering/architecture/topology.yaml index 3042751..650e292 100644 --- a/starter/product/engineering/architecture/topology.yaml +++ b/starter/product/engineering/architecture/topology.yaml @@ -1,4 +1,5 @@ schema_version: 1 +owner_skill: technical-landscape systems: [] applications: [] components: [] diff --git a/starter/product/engineering/catalog/README.md b/starter/product/engineering/catalog/README.md index ddcf801..de4f704 100644 --- a/starter/product/engineering/catalog/README.md +++ b/starter/product/engineering/catalog/README.md @@ -1,5 +1,7 @@ # Technical Entity Catalog +Owner skill: `technical-landscape`. + `catalog.yaml` indexes systems, applications, components, repositories, data stores, interfaces, and deployments by stable ID. Create records only from observed evidence or explicit hypotheses. Folder position does not define graph diff --git a/starter/product/engineering/catalog/catalog.yaml b/starter/product/engineering/catalog/catalog.yaml index d0c294a..6c7f6f4 100644 --- a/starter/product/engineering/catalog/catalog.yaml +++ b/starter/product/engineering/catalog/catalog.yaml @@ -1,4 +1,5 @@ schema_version: 1 +owner_skill: technical-landscape entities: systems: {} applications: {} diff --git a/starter/product/engineering/context.md b/starter/product/engineering/context.md index ed62760..7c901e1 100644 --- a/starter/product/engineering/context.md +++ b/starter/product/engineering/context.md @@ -30,4 +30,4 @@ This placeholder does not describe a real product architecture. Replace it from ## Handoff -Next: `engineering-system` to establish the product baseline. +Next: `engineering-orchestrator` to route the complete product engineering baseline. diff --git a/starter/product/engineering/engineering-system.md b/starter/product/engineering/engineering-system.md index dc0f288..5bdf7fe 100644 --- a/starter/product/engineering/engineering-system.md +++ b/starter/product/engineering/engineering-system.md @@ -18,18 +18,18 @@ Draft contract. Inspect the adopter's real repository before defining system bou ## Architecture -| Area | Contract | Evidence | Maturity | -| --- | --- | --- | --- | -| System context | [architecture/system-context.md](architecture/system-context.md) | Not configured | `baseline` | -| Modules | [architecture/modules.md](architecture/modules.md) | Not configured | `baseline` | -| Technical catalog | [catalog/catalog.yaml](catalog/catalog.yaml) | Not configured | `baseline` | -| Standards | [standards/standards.yaml](standards/standards.yaml) | Not configured | `baseline` | -| Quality | [quality/quality-system.md](quality/quality-system.md) | Not configured | `baseline` | -| Operations | [operations/operations.yaml](operations/operations.yaml) | Not configured | `baseline` | -| Evidence | [evidence/inventory.md](evidence/inventory.md) | Not configured | `baseline` | +| Area | Owner skill | Contract | Evidence | Maturity | +| --- | --- | --- | --- | --- | +| System context | `technical-landscape` | [architecture/system-context.md](architecture/system-context.md) | Not configured | `baseline` | +| Modules | `technical-landscape` | [architecture/modules.md](architecture/modules.md) | Not configured | `baseline` | +| Technical catalog | `technical-landscape` | [catalog/catalog.yaml](catalog/catalog.yaml) | Not configured | `baseline` | +| Standards | `engineering-standards` | [standards/standards.yaml](standards/standards.yaml) | Not configured | `baseline` | +| Quality | `engineering-system` | [quality/quality-system.md](quality/quality-system.md) | Not configured | `baseline` | +| Operations | `operations-baseline` | [operations/operations.yaml](operations/operations.yaml) | Not configured | `baseline` | +| Evidence | `engineering-evidence` | [evidence/inventory.md](evidence/inventory.md) | Not configured | `baseline` | No maturity beyond placeholder baseline is claimed. ## Handoff -Next: `engineering-system` after the real codebase and operations can be inspected. +Next: `engineering-orchestrator` after the real codebase and operations can be inspected. diff --git a/starter/product/engineering/engineering-system.yaml b/starter/product/engineering/engineering-system.yaml index 2e504e7..f4f36b1 100644 --- a/starter/product/engineering/engineering-system.yaml +++ b/starter/product/engineering/engineering-system.yaml @@ -7,30 +7,37 @@ scope: not-configured areas: system_context: contract: architecture/system-context.md + owner_skill: technical-landscape maturity: baseline evidence: [] modules: contract: architecture/modules.md + owner_skill: technical-landscape maturity: baseline evidence: [] technical_catalog: contract: catalog/catalog.yaml + owner_skill: technical-landscape maturity: baseline evidence: [] standards: contract: standards/standards.yaml + owner_skill: engineering-standards maturity: baseline evidence: [] quality: contract: quality/quality-system.md + owner_skill: engineering-system maturity: baseline evidence: [] operations: contract: operations/operations.yaml + owner_skill: operations-baseline maturity: baseline evidence: [] evidence: contract: evidence/inventory.md + owner_skill: engineering-evidence maturity: baseline evidence: [] decisions: [] diff --git a/starter/product/engineering/evidence/README.md b/starter/product/engineering/evidence/README.md index 0184afd..51f5934 100644 --- a/starter/product/engineering/evidence/README.md +++ b/starter/product/engineering/evidence/README.md @@ -1,4 +1,6 @@ # Engineering Evidence +Owner skill: `engineering-evidence`. + Index evidence used by maturity claims without copying volatile command or CI output into stable contracts. diff --git a/starter/product/engineering/evidence/inventory.md b/starter/product/engineering/evidence/inventory.md index b33a5a4..a3da776 100644 --- a/starter/product/engineering/evidence/inventory.md +++ b/starter/product/engineering/evidence/inventory.md @@ -1,4 +1,6 @@ # Engineering Evidence Inventory +Owner skill: `engineering-evidence`. + | Evidence ID | Scope | Kind | Source | Observed on | Notes | | --- | --- | --- | --- | --- | --- | diff --git a/starter/product/engineering/operations/README.md b/starter/product/engineering/operations/README.md index 4808200..a74daf3 100644 --- a/starter/product/engineering/operations/README.md +++ b/starter/product/engineering/operations/README.md @@ -1,4 +1,6 @@ # Engineering Operations +Owner skill: `operations-baseline`. + Index environments, deployments, and runbooks in `operations.yaml`. Create records on demand and link them to stable catalog entity IDs. diff --git a/starter/product/engineering/operations/operations.yaml b/starter/product/engineering/operations/operations.yaml index 220e4b1..662c75a 100644 --- a/starter/product/engineering/operations/operations.yaml +++ b/starter/product/engineering/operations/operations.yaml @@ -1,4 +1,5 @@ schema_version: 1 +owner_skill: operations-baseline environments: {} deployments: {} runbooks: {} diff --git a/starter/product/engineering/quality/quality-system.md b/starter/product/engineering/quality/quality-system.md index c1c9e8c..4ba2a67 100644 --- a/starter/product/engineering/quality/quality-system.md +++ b/starter/product/engineering/quality/quality-system.md @@ -34,4 +34,4 @@ No exceptions are configured. Real exceptions require owner, rationale, residual ## Handoff -Next: `engineering-system` after real product evidence can be inspected. +Next: `engineering-orchestrator` after real product evidence can be inspected and specialist contracts are ready. diff --git a/starter/product/engineering/standards/README.md b/starter/product/engineering/standards/README.md index 81333ee..5185951 100644 --- a/starter/product/engineering/standards/README.md +++ b/starter/product/engineering/standards/README.md @@ -1,5 +1,7 @@ # Engineering Standards +Owner skill: `engineering-standards`. + `standards.yaml` indexes versioned profiles, standards, and governed exceptions. Standards apply by entity type, capability, or explicit assignment. Required inherited standards cannot be silently weakened. diff --git a/starter/product/engineering/standards/standards.yaml b/starter/product/engineering/standards/standards.yaml index 0a91076..3f9fa8d 100644 --- a/starter/product/engineering/standards/standards.yaml +++ b/starter/product/engineering/standards/standards.yaml @@ -1,4 +1,5 @@ schema_version: 1 +owner_skill: engineering-standards profiles: PROFILE-PRODUCT-DEFAULT: profiles/product-default.yaml standards: {} From e0afbc9cd5e12c404b2804a884c9e0fcf6fc0ba3 Mon Sep 17 00:00:00 2001 From: JonatasFreireDev Date: Sat, 18 Jul 2026 13:48:20 -0300 Subject: [PATCH 9/9] feat(engineering): delegate specialists to subagents --- FRAMEWORK.md | 2 + README.md | 2 +- docs/engineering-systems.md | 2 + docs/execution-runtime.md | 4 + docs/index.html | 1 + docs/subagent-dispatch.md | 21 + framework/AGENTS.framework.md | 2 + framework/skills/README.md | 2 + .../skills/dispatch-orchestrator/SKILL.md | 8 +- .../references/template-authoring.md | 2 + .../skills/engineering-evidence/SKILL.md | 4 + .../skills/engineering-orchestrator/SKILL.md | 28 +- ...engineering-baseline-handoff-template.json | 44 +- .../skills/engineering-standards/SKILL.md | 4 + framework/skills/engineering-system/SKILL.md | 4 + framework/skills/operations-baseline/SKILL.md | 4 + .../skills/subagent-return-reviewer/SKILL.md | 12 +- ...ngineering-specialist-return-template.json | 16 + framework/skills/technical-landscape/SKILL.md | 4 + internal/cli/dispatch.go | 39 ++ internal/cli/dispatch_engineering_test.go | 63 +++ internal/dispatch/dispatch.go | 380 ++++++++++++++++-- internal/dispatch/dispatch_test.go | 240 +++++++++++ internal/install/install_test.go | 4 + internal/runtimeassets/runtime_test.go | 5 + starter/AGENTS.md | 2 + starter/README.md | 2 +- 27 files changed, 858 insertions(+), 43 deletions(-) create mode 100644 framework/skills/subagent-return-reviewer/assets/engineering-specialist-return-template.json create mode 100644 internal/cli/dispatch_engineering_test.go diff --git a/FRAMEWORK.md b/FRAMEWORK.md index 8d70100..3459789 100644 --- a/FRAMEWORK.md +++ b/FRAMEWORK.md @@ -148,6 +148,8 @@ The optional Engineering System versions stable architecture, module and data ow `engineering-orchestrator` governs completeness and sequencing across `technical-landscape`, `engineering-standards`, `operations-baseline`, `engineering-evidence`, and final `engineering-system` consolidation. Each specialist owns its canonical contracts; the orchestrator never authors them, and `engineering-system` aggregates and versions them without absorbing their ownership. The baseline stops at human approval before Domain modeling or delivery-specific Technical Discovery. +Engineering baseline execution supports `sequential` and `delegated` modes. Sequential execution is the compatible default. Delegated execution uses harness-native subagents under persisted dispatch envelopes with minimal context, bounded parallelism, explicit dependency phases, disjoint write scopes, and hash-verified compact returns. `dispatch-orchestrator` owns assignment and observation; `subagent-return-reviewer` validates returns; the CLI persists their contracts but never starts an agent itself. Technical Landscape runs first; Standards and Operations may then run concurrently; Evidence follows their returned contracts; Engineering System aggregates last. If native subagents are unavailable, the declared fallback applies without weakening gates or importing the entire specialist conversation into the parent context. + Engineering standards are versioned, verifiable rules composed through profiles and applied by entity type, capability, or explicit assignment. A more specific contract may add constraints but cannot silently weaken an inherited required standard. Divergence requires a scoped, governed exception or approved decision. ### Engineering Quality System diff --git a/README.md b/README.md index 8ff6c59..5dcae90 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ spec-framework uninstall --purge --yes | --- | --- | | **Specification** | The central contract: flow, UI, APIs, data, permissions, analytics, security, and acceptance — written before any code. | | **Design flow** | Specification → `design.md` (origin `generate`/`evolve`/`adopt`, versioned visual sources) → independent UX Review → human gate. | -| **Engineering flow** | Shared baseline: Engineering Orchestrator → Technical Landscape → Standards → Operations → Evidence → Engineering System approval. Delivery: Technical Discovery → Engineering Proposal → independent Engineering Review → Implementation Plan. | +| **Engineering flow** | Shared baseline: Engineering Orchestrator → Technical Landscape → Standards + Operations → Evidence → Engineering System approval. It runs sequentially by default or through bounded minimal-context native subagents. Delivery: Technical Discovery → Engineering Proposal → independent Engineering Review → Implementation Plan. | | **Execution Graph** | Complete vertical task contracts as a DAG with explicit `writeScope`; parallel work never overlaps write paths. | | **Approval gates** | `draft → proposed → approved → in_progress → implemented → validated → released`, each transition mechanically checked (content hashes, same-diff QA + Code Review, staleness detection). | | **Shared systems** | Optional versioned Design System (`design/system/`) and scalable Engineering System (`engineering/`) whose graph, standards, operations, evidence, aggregate, and quality contracts have explicit specialist owners — pinned per delivery, never self-approving. | diff --git a/docs/engineering-systems.md b/docs/engineering-systems.md index b116007..9939bb7 100644 --- a/docs/engineering-systems.md +++ b/docs/engineering-systems.md @@ -12,6 +12,8 @@ The standards system composes versioned rules through profiles. Consumers pin ap The shared baseline is coordinated by `engineering-orchestrator`. It routes `technical-landscape`, `engineering-standards`, `operations-baseline`, and `engineering-evidence`, then asks `engineering-system` to consolidate the aggregate and Quality System. Specialist contracts remain independently owned even though the Engineering System composite approval hash governs their use together. +The orchestrator may execute these owners sequentially or delegate them through harness-native subagents. Delegation is phased rather than unrestricted: Technical Landscape first; Standards and Operations concurrently only while their scopes do not overlap; Evidence after both; and Engineering System aggregation last. Each delegated return is compact and hash-verifiable so the parent context retains conclusions and blockers instead of full specialist exploration. + Engineering System approval hashes its complete contract surface deterministically. A change to an approved shared contract makes its approval stale and requires human re-approval. Specification and approved product decisions remain authoritative when contracts conflict. ## Engineering Quality System diff --git a/docs/execution-runtime.md b/docs/execution-runtime.md index c469477..778ec94 100644 --- a/docs/execution-runtime.md +++ b/docs/execution-runtime.md @@ -6,6 +6,8 @@ This document contains the operational contract for resumable, parallel, and evi The runtime coordinates workspaces, task ownership, command plans, execution waves, isolated worktrees, local integration, and recovery. It does not grant product approval, replace task write scopes, or spawn agents. +Harness-native agent delegation is distinct from process execution. The runtime may persist a supervised envelope that a parent agent passes to a native subagent, but only the active harness creates, waits for, interrupts, or collects that subagent. This keeps the CLI portable across Codex, Cursor, Claude, and environments without native delegation. + ## Configuration authority Runtime behavior is determined by the pinned product manifest, approved product @@ -51,6 +53,8 @@ Operational memory is optional and has shared (`memory/shared.md`) and task-loca ACP dispatch is experimental and disabled by default. A run requires explicit enablement and per-run acknowledgement, then claims exactly one ready task for its named agent. It records a local transcript hash and releases that lease at the end. It cannot invoke Git delivery commands and has no approval, review-resolution, push, merge, or release authority. +Engineering baseline delegation does not use ACP process execution. Its `engineering-specialist` envelopes are activated by an Engineering Orchestrator handoff whose execution mode is `delegated`. Dispatch Orchestrator owns assignment and observation; Subagent Return Reviewer validates the compact return before CLI persistence. The envelope pins the specialist role, handoff input hash, dependency returns, minimal-context policy, phase, and product-relative write scope. Assignment capacity is claimed atomically per workspace, so concurrent CLI invocations cannot exceed `max_parallel` or duplicate an active specialist role. Returns require evidence and SHA-256 hashes for outputs inside that scope. The default handoff mode is `sequential`; delegated mode has a declared sequential fallback and a bounded `max_parallel` value. + Extensions are versioned manifests discovered without execution. A capability is usable only when that manifest declares it and the product has a matching versioned record under `.product/extensions/`; discovery itself grants no trust or authority. ## Local project-status server diff --git a/docs/index.html b/docs/index.html index fe2d4a3..46e673e 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1603,6 +1603,7 @@

O fluxo de Engenharia

Engineering System — conhecimento estável, versionado

+

Execução com contexto controlado: o Engineering Orchestrator usa o modo sequencial por padrão ou delega Technical Landscape, Standards, Operations, Evidence e a consolidação final a subagentes nativos. Cada atribuição recebe somente o contexto necessário, respeita dependências e escopo de escrita e retorna hashes verificáveis; Standards e Operations podem executar em paralelo após o mapa técnico.

Vive em engineering/, é opcional e declara origin_mode (generate/evolve/adopt) e versão semântica — o mesmo modelo do Design System. engineering-system.md é o contrato humano; engineering-system.yaml, o catálogo mecânico que os validadores leem. diff --git a/docs/subagent-dispatch.md b/docs/subagent-dispatch.md index 5c6548f..60a4fa5 100644 --- a/docs/subagent-dispatch.md +++ b/docs/subagent-dispatch.md @@ -1,5 +1,26 @@ # Despacho supervisionado de subagents +## Engineering baseline specialists + +Engineering Orchestrator can persist a baseline handoff in either `sequential` +or `delegated` mode. Sequential is the compatible default. In delegated mode, +the parent agent uses the harness-native subagent capability; the CLI only +validates and stores the envelope. + +```powershell +spec-framework dispatch assign --product-root product --work WORK-001 --task .product/workspaces/WORK-001/engineering-handoff.json --role technical-landscape --agent landscape-1 --yes +spec-framework dispatch return --product-root product --work WORK-001 --id DISPATCH-... --agent landscape-1 --summary "technical graph mapped" --evidence "catalog and topology" --output-hashes "engineering/catalog/catalog.yaml=" --blockers "" --decision-candidates "" --yes +``` + +The canonical phase order is Technical Landscape; Standards and Operations in +a bounded parallel phase; Evidence; then Engineering System aggregation. Later +assignments provide returned dispatch IDs through `--depends-on`. Every return +must identify product-relative outputs and their SHA-256 hashes. The runtime +rejects missing dependencies, stale or unreturned prerequisites, concurrency +above `max_parallel`, outputs outside the declared scope, and hash mismatches. +Blockers and decision candidates are persisted as structured envelope fields; +a blocked dependency cannot unlock a later specialist phase. + `dispatch` coordena subagents por envelopes persistidos. Ele não aprova, commita, envia, faz merge ou publica. diff --git a/framework/AGENTS.framework.md b/framework/AGENTS.framework.md index 8fc4140..4df115b 100644 --- a/framework/AGENTS.framework.md +++ b/framework/AGENTS.framework.md @@ -36,6 +36,8 @@ When the product declares `product/design/system/`, route shared foundations, to Route creation or evolution of `product/engineering/` through Engineering Orchestrator. Technical Landscape owns shared architecture and entity graph contracts; Engineering Standards owns standards and profiles; Operations Baseline owns operational contracts; Engineering Evidence owns evidence and maturity assessments; Engineering System owns only aggregate and Quality System consolidation. Do not let one specialist edit another specialist's subtree without an explicit handoff, and stop for human approval of the current composite hash. +Engineering Orchestrator may use harness-native subagents only when its persisted handoff selects `delegated` execution. Keep `sequential` as the compatible default. In delegated mode, give each specialist only its dispatch envelope and required reading, enforce declared dependencies and write scope, bound concurrency, verify output hashes, and reconcile compact returns in the parent context. If native subagents are unavailable, apply the declared fallback; the CLI itself never spawns agents. + ## Required Reading Authority order: diff --git a/framework/skills/README.md b/framework/skills/README.md index 8a4ea91..dd21e6b 100644 --- a/framework/skills/README.md +++ b/framework/skills/README.md @@ -8,6 +8,8 @@ This folder contains framework-owned skills that operationalize the method. Each Use these skills when creating, updating, auditing, explaining, routing, or handing off framework artifacts. Specialist skills own canonical artifact content. Orchestrator skills own flow, gates, sequencing, and handoff. +Engineering Orchestrator supports sequential execution by default and optional harness-native subagent delegation. Delegated specialists receive persisted minimal-context envelopes, bounded phases, non-overlapping write scopes, and hash-verified return contracts; the CLI never starts agents itself. + Definition and planning skills must follow [Discovery And Challenge](discovery-and-challenge.md). They inspect evidence before asking, use the harness-native structured question tool when available, compare meaningful options, recommend a path, warn about material risks, and stop on unanswered blocking questions. ## Path Resolution diff --git a/framework/skills/dispatch-orchestrator/SKILL.md b/framework/skills/dispatch-orchestrator/SKILL.md index 6b27c60..4ad1d75 100644 --- a/framework/skills/dispatch-orchestrator/SKILL.md +++ b/framework/skills/dispatch-orchestrator/SKILL.md @@ -18,7 +18,7 @@ Owns dispatch envelopes, waves, capacity observation, and handoff sequencing. It - explain: summarize eligible work and blockers. ## Inputs -Approved Execution Graph; ready task or import chunk; workspace state; dispatch configuration; current product decisions. +Approved Execution Graph; ready task or import chunk; Engineering Orchestrator handoff; workspace state; dispatch configuration; current product decisions. ## Outputs Persisted envelopes; dispatch plan; wave observation; reconciliation findings; handoffs. @@ -38,7 +38,9 @@ Use scripts/invoke-cli.ps1 on Windows or scripts/invoke-cli.sh on macOS/Linux fo 3. Require explicit human confirmation before assignment or execution. 4. Persist envelope, lease, required reading, hashes, scope, forbidden operations, and expected evidence. 5. Dispatch QA, Code Review, and Security Review only from the returned Code Runner diff hash. -6. Reconcile and route blockers; never repair approval or product state. +6. For an Engineering Orchestrator handoff in delegated mode, assign only the declared `engineering-specialist` route, dependency IDs, minimal context, phase, and specialist-owned write scope. The harness creates the native subagent; this skill and the CLI do not. +7. Enforce bounded `max_parallel` across active Engineering assignments and allow concurrency only for disjoint scopes in the same ready phase. +8. Reconcile and route blockers; never repair approval or product state. ## Quality checklist - [ ] Preserves traceability to affected artifacts. @@ -50,6 +52,6 @@ Use scripts/invoke-cli.ps1 on Windows or scripts/invoke-cli.sh on macOS/Linux fo - [ ] Leaves a clear handoff for the next skill or orchestrator. ## Handoff -Next: code-runner, artifact-importer, qa, code-review, security-review, or product-historian. +Next: code-runner, artifact-importer, technical-landscape, engineering-standards, operations-baseline, engineering-evidence, engineering-system, qa, code-review, security-review, or product-historian. Pass forward envelope, hashes, evidence, blockers, risks, and required follow-up work. diff --git a/framework/skills/documentation-writer/references/template-authoring.md b/framework/skills/documentation-writer/references/template-authoring.md index 5a2d39b..84f138c 100644 --- a/framework/skills/documentation-writer/references/template-authoring.md +++ b/framework/skills/documentation-writer/references/template-authoring.md @@ -33,6 +33,8 @@ Use the generating skill's own templates whenever a new canonical artifact is cr - `specification-contract-template.md`: reusable structure for modular product, behavior, UX, API, data, security, quality, observability, and rollout contracts. - `import-traceability-template.json`, `import-plan-template.json`, `import-mapping-template.json`, `import-report-template.md`: staged source-import evidence, review, selection, and reporting contracts. - `command-plan-template.json`, `handoff-template.json`, `runtime-workspace-template.json`, `integration-template.json`: execution-runtime planning, handoff, workspace, and integration state contracts. +- `engineering-baseline-handoff-template.json`: Engineering Orchestrator execution-mode, dependency, minimal-context, and write-scope contract. +- `engineering-specialist-return-template.json`: compact engineering return contract owned by Subagent Return Reviewer. ## Responsibility diff --git a/framework/skills/engineering-evidence/SKILL.md b/framework/skills/engineering-evidence/SKILL.md index 3f57a6d..3add9f7 100644 --- a/framework/skills/engineering-evidence/SKILL.md +++ b/framework/skills/engineering-evidence/SKILL.md @@ -34,6 +34,10 @@ Technical graph; standards; operations; Quality System; code and tests; CI and r Follow the shared [Discovery And Challenge contract](../discovery-and-challenge.md) when evidence limitations, freshness policy, maturity interpretation, or ownership requires a material choice. +## Delegated execution + +When `engineering-orchestrator` supplies an `engineering-specialist` dispatch envelope through `dispatch-orchestrator`, treat it as a minimal-context subagent assignment. Require returned Standards and Operations dependencies, verify the input hash and write scope, read their contracts plus evidence sources, write only under `engineering/evidence/`, and return a compact summary, blockers, evidence, decision candidates, and product-relative output hashes to `subagent-return-reviewer`. Do not request or retain the parent conversation. + ## Workflow 1. Run the platform-appropriate evidence inventory script and inspect referenced local, CI, runtime, and approved external sources. 2. Normalize each material source into a stable `EVID-*` reference without copying volatile logs into canonical contracts. diff --git a/framework/skills/engineering-orchestrator/SKILL.md b/framework/skills/engineering-orchestrator/SKILL.md index 5ec83c9..d38414f 100644 --- a/framework/skills/engineering-orchestrator/SKILL.md +++ b/framework/skills/engineering-orchestrator/SKILL.md @@ -29,22 +29,33 @@ Persisted engineering-baseline handoff; readiness verdict; ordered specialist ro - the framework root's `FRAMEWORK.md` - Relevant parent context.md files. - This skill owns `assets/engineering-baseline-handoff-template.json` and `assets/engineering-readiness-report-template.md` for persisted routing and readiness reporting. +- Compact delegated returns use the `engineering-specialist-return-template.json` owned by `subagent-return-reviewer`. - Approved decisions are discovered through the active product root's `.product/decisions.json`; resolve each registered path from its declared decision domain root. ## Discovery and challenge Follow the shared [Discovery And Challenge contract](../discovery-and-challenge.md) when scope, ownership, compatibility, or a blocking route requires human choice. +## Execution modes + +- `sequential` is the compatible default. The current agent follows one specialist contract at a time and persists the same handoff state. +- `delegated` uses harness-native subagents when that capability is available. The orchestrator remains in the parent context and gives each subagent only its dispatch envelope, the pinned specialist skill, the engineering handoff, declared code roots, and files required by that specialist. +- When selecting `delegated`, set `max_parallel` to the supported bounded capacity, normally `2` for the disjoint Standards and Operations phase. Keep it at `1` when concurrency is not safe. +- If delegated execution is requested but the harness cannot create subagents, record the unavailable capability and use the handoff's `fallback`. Never simulate delegation by loading all specialist context into the parent window. +- Route envelope creation and observation through `dispatch-orchestrator`, and return validation through `subagent-return-reviewer`. The CLI persists and validates their runtime contracts but never starts an agent. Agent creation, waiting, interruption, and collection use the current harness's native capability. + ## Workflow 1. Revalidate the product manifest, Product Landscape, code roots, current Engineering System hash, approvals, and persisted workspace state. 2. Classify the route as create, update, adopt, or audit and identify affected specialist contracts without assuming one repository, application, or deployment. -3. Route `technical-landscape` first when entity or relation coverage is absent or stale. -4. Route `engineering-standards` after the entity graph can express applicability and inheritance. -5. Route `operations-baseline` after applications, components, deployments, interfaces, and data stores are identifiable. -6. Route `engineering-evidence` to resolve evidence, maturity, coverage, and staleness for every claimed area. -7. Route `engineering-system` only after specialist outputs are internally consistent, so it can consolidate identity, version, quality, compatibility, and the composite contract. -8. Re-run Engineering System validation, report uncovered scope and decision candidates, and stop at the human approval gate. -9. After current approval, route product-wide bootstrap to `domain-architect` or delivery-specific work to `technical-discovery` according to the initiating scope. +3. Persist the execution mode, minimal-context policy, fallback, maximum parallelism, specialist phases, dependencies, and non-overlapping write scopes in the engineering handoff. +4. In delegated mode, route supervised envelope creation to `dispatch-orchestrator` before spawning specialists. Do not inherit the full conversation; pass only the envelope and its `required_reading`. In sequential mode, follow the same phases without creating subagents. +5. Route `technical-landscape` alone in phase 1 when entity or relation coverage is absent or stale. +6. Route `engineering-standards` and `operations-baseline` in phase 2 after the technical graph returns. They may run concurrently because their canonical write scopes do not overlap. +7. Route `engineering-evidence` in phase 3 after standards and operations return, so its coverage and maturity assessment sees their current contracts. +8. Route `engineering-system` in phase 4 only after all specialist outputs are internally consistent, so it can consolidate identity, version, quality, compatibility, and the composite contract. +9. Keep the handoff immutable while a phase has active assignments. Route every compact result through `subagent-return-reviewer`, which verifies the dispatch ID, input hash, dependency returns, write scope, output hashes, evidence, blockers, and decision candidates before the CLI records it. Update handoff state once the phase is collected. Reject stale, escaped, incomplete, or conflicting returns and route only the affected specialist again. +10. Re-run Engineering System validation, report uncovered scope and decision candidates, and stop at the human approval gate. +11. After current approval, route product-wide bootstrap to `domain-architect` or delivery-specific work to `technical-discovery` according to the initiating scope. ## Quality checklist - [ ] Preserves traceability to affected artifacts. @@ -52,6 +63,9 @@ Follow the shared [Discovery And Challenge contract](../discovery-and-challenge. - [ ] Every declared code root and engineering area has an owner or explicit gap. - [ ] Specialist outputs agree on stable IDs, applicability, maturity, evidence, and compatibility. - [ ] No specialist is bypassed merely because a previous aggregate contract exists. +- [ ] Delegated specialists receive minimal context and cannot write outside their owned scope. +- [ ] Parallel assignments have returned dependencies, disjoint write scopes, and bounded concurrency. +- [ ] The parent reconciles compact returns rather than importing subagent conversation history. - [ ] Approval remains human and applies to the current composite hash. - [ ] Detects gaps, conflicts, and dependencies. - [ ] Records or requests decisions for meaningful changes. diff --git a/framework/skills/engineering-orchestrator/assets/engineering-baseline-handoff-template.json b/framework/skills/engineering-orchestrator/assets/engineering-baseline-handoff-template.json index 4d5f797..a262f7c 100644 --- a/framework/skills/engineering-orchestrator/assets/engineering-baseline-handoff-template.json +++ b/framework/skills/engineering-orchestrator/assets/engineering-baseline-handoff-template.json @@ -2,8 +2,50 @@ "schema_version": 1, "scope": "product", "mode": "create", + "execution": { + "mode": "sequential", + "context_policy": "minimal", + "max_parallel": 1, + "fallback": "sequential" + }, "current_engineering_system": null, - "routes": [], + "routes": [ + { + "skill": "technical-landscape", + "phase": 1, + "depends_on": [], + "write_scope": ["engineering/architecture", "engineering/catalog"], + "status": "pending" + }, + { + "skill": "engineering-standards", + "phase": 2, + "depends_on": ["technical-landscape"], + "write_scope": ["engineering/standards"], + "status": "pending" + }, + { + "skill": "operations-baseline", + "phase": 2, + "depends_on": ["technical-landscape"], + "write_scope": ["engineering/operations"], + "status": "pending" + }, + { + "skill": "engineering-evidence", + "phase": 3, + "depends_on": ["engineering-standards", "operations-baseline"], + "write_scope": ["engineering/evidence"], + "status": "pending" + }, + { + "skill": "engineering-system", + "phase": 4, + "depends_on": ["technical-landscape", "engineering-standards", "operations-baseline", "engineering-evidence"], + "write_scope": ["engineering/engineering-system.md", "engineering/engineering-system.yaml", "engineering/quality"], + "status": "pending" + } + ], "completed_contracts": [], "blockers": [], "decision_candidates": [], diff --git a/framework/skills/engineering-standards/SKILL.md b/framework/skills/engineering-standards/SKILL.md index 701caa0..2f91204 100644 --- a/framework/skills/engineering-standards/SKILL.md +++ b/framework/skills/engineering-standards/SKILL.md @@ -35,6 +35,10 @@ Technical entity graph; engineering conventions; code and CI evidence; quality a Follow the shared [Discovery And Challenge contract](../discovery-and-challenge.md) before selecting obligation levels, compatibility policy, profile inheritance, or exception boundaries. +## Delegated execution + +When `engineering-orchestrator` supplies an `engineering-specialist` dispatch envelope through `dispatch-orchestrator`, treat it as a minimal-context subagent assignment. Require the returned Technical Landscape dependency, verify the input hash and write scope, read the graph plus standards evidence, write only under `engineering/standards/`, and return a compact summary, blockers, evidence, decision candidates, and product-relative output hashes to `subagent-return-reviewer`. Do not request or retain the parent conversation. + ## Workflow 1. Read the technical graph, existing conventions, quality and security contracts, and real enforcement evidence. 2. Define standards only for stable technical rules with an explicit applicability boundary and verification method. diff --git a/framework/skills/engineering-system/SKILL.md b/framework/skills/engineering-system/SKILL.md index d733500..961bff2 100644 --- a/framework/skills/engineering-system/SKILL.md +++ b/framework/skills/engineering-system/SKILL.md @@ -36,6 +36,10 @@ Engineering Orchestrator handoff; specialist-owned technical landscape, standard Follow the shared [Discovery And Challenge contract](../discovery-and-challenge.md) before substantive creation or material revision. +## Delegated execution + +When `engineering-orchestrator` supplies an `engineering-specialist` dispatch envelope through `dispatch-orchestrator`, treat it as a minimal-context subagent assignment. Require every returned specialist dependency, verify the input hash and write scope, read their compact returns and canonical outputs, write only `engineering/engineering-system.md`, `engineering/engineering-system.yaml`, and `engineering/quality/`, and return the composite hash, validation evidence, blockers, and product-relative output hashes to `subagent-return-reviewer`. Do not request or retain the parent conversation and do not approve the aggregate. + Use scripts/invoke-cli.ps1 on Windows or scripts/invoke-cli.sh on macOS/Linux for the CLI operation in this skill's reviewed scope. The wrapper never adds --yes or an approver identity. diff --git a/framework/skills/operations-baseline/SKILL.md b/framework/skills/operations-baseline/SKILL.md index 9b5656e..4554e3e 100644 --- a/framework/skills/operations-baseline/SKILL.md +++ b/framework/skills/operations-baseline/SKILL.md @@ -35,6 +35,10 @@ Technical graph; deployment configuration; infrastructure; runtime and observabi Follow the shared [Discovery And Challenge contract](../discovery-and-challenge.md) before material service-level, recovery, data-loss, rollout, or ownership choices. +## Delegated execution + +When `engineering-orchestrator` supplies an `engineering-specialist` dispatch envelope through `dispatch-orchestrator`, treat it as a minimal-context subagent assignment. Require the returned Technical Landscape dependency, verify the input hash and write scope, read the graph plus operational evidence, write only under `engineering/operations/`, and return a compact summary, blockers, evidence, decision candidates, and product-relative output hashes to `subagent-return-reviewer`. Do not request or retain the parent conversation. + ## Workflow 1. Read the technical graph and inspect deployment, infrastructure, observability, incident, continuity, and runbook evidence. 2. Inventory real environments, deployable units, release paths, rollback mechanisms, dependencies, service objectives, and operational owners. diff --git a/framework/skills/subagent-return-reviewer/SKILL.md b/framework/skills/subagent-return-reviewer/SKILL.md index a65d211..009b1d1 100644 --- a/framework/skills/subagent-return-reviewer/SKILL.md +++ b/framework/skills/subagent-return-reviewer/SKILL.md @@ -18,22 +18,24 @@ Owns `dispatch-return.md` validation and routing. It does not fix code, approve - explain: summarize return readiness and blockers. ## Inputs -Dispatch envelope; transcript; task/chunk context; hashes; evidence; findings. +Dispatch envelope; transcript or specialist compact return; task, chunk, or engineering handoff context; hashes; evidence; findings. ## Outputs -`dispatch-return.md`; route; blockers; stale-return verdict. +`dispatch-return.md`; validated engineering specialist return when applicable; route; blockers; stale-return verdict. ## Required reading - the framework root's `FRAMEWORK.md` - Relevant parent context.md files. - This skill owns its generation resource: `assets/dispatch-return-template.md`. +- This skill owns `assets/engineering-specialist-return-template.json` for compact engineering specialist returns. - Approved product decisions in the active product root's `knowledge/decisions/` and `.product/decisions.json`. ## Workflow 1. Verify agent, unit, input hash, diff hash when applicable, scope, and evidence. 2. Reject returns with missing evidence, stale diff, or forbidden operation. -3. Preserve unresolved gaps and route to the independent owner. -4. Record the return without changing canonical task approval or lifecycle. +3. For `engineering-specialist`, verify returned dependencies, minimal-context assignment, specialist write scope, product-relative output hashes, blockers, and decision candidates before invoking the CLI return contract. +4. Preserve unresolved gaps and route to the independent owner. A specialist return with blockers cannot unlock a dependent phase. +5. Record the return without changing canonical task or artifact approval and without importing the subagent's conversation history. ## Quality checklist - [ ] Preserves traceability to affected artifacts. @@ -45,6 +47,6 @@ Dispatch envelope; transcript; task/chunk context; hashes; evidence; findings. - [ ] Leaves a clear handoff for the next skill or orchestrator. ## Handoff -Next: qa, code-review, security-review, artifact-importer, code-runner, or product-historian. +Next: qa, code-review, security-review, artifact-importer, code-runner, engineering-orchestrator, or product-historian. Pass forward return, hashes, evidence, blockers, findings, risks, and required follow-up work. diff --git a/framework/skills/subagent-return-reviewer/assets/engineering-specialist-return-template.json b/framework/skills/subagent-return-reviewer/assets/engineering-specialist-return-template.json new file mode 100644 index 0000000..2703f55 --- /dev/null +++ b/framework/skills/subagent-return-reviewer/assets/engineering-specialist-return-template.json @@ -0,0 +1,16 @@ +{ + "schema_version": 1, + "dispatch_id": "DISPATCH-NNN", + "role": "technical-landscape", + "summary": "", + "outputs": [ + { + "path": "engineering/catalog/catalog.yaml", + "sha256": "" + } + ], + "evidence": [], + "blockers": [], + "decision_candidates": [], + "next": "engineering-orchestrator" +} diff --git a/framework/skills/technical-landscape/SKILL.md b/framework/skills/technical-landscape/SKILL.md index 569586a..a402f84 100644 --- a/framework/skills/technical-landscape/SKILL.md +++ b/framework/skills/technical-landscape/SKILL.md @@ -35,6 +35,10 @@ Product Landscape; declared code roots; repositories; manifests; source and test Follow the shared [Discovery And Challenge contract](../discovery-and-challenge.md) before material boundary, ownership, identity, split, merge, or adoption choices. +## Delegated execution + +When `engineering-orchestrator` supplies an `engineering-specialist` dispatch envelope through `dispatch-orchestrator`, treat it as a minimal-context subagent assignment. Verify the role, input hash, dependencies, and write scope; read only the declared inputs plus evidence discovered within the code roots; write only under `engineering/architecture/` and `engineering/catalog/`; and return a compact summary, blockers, evidence, decision candidates, and product-relative output hashes to `subagent-return-reviewer`. Do not request or retain the parent conversation. + ## Workflow 1. Run the platform-appropriate technical landscape inventory script and inspect every declared code root, not only the first repository or dominant application. 2. Identify systems, applications, components, repositories, interfaces, data stores, deployments, ownership, capabilities, and observable relations. diff --git a/internal/cli/dispatch.go b/internal/cli/dispatch.go index 4adbe6e..5832bb8 100644 --- a/internal/cli/dispatch.go +++ b/internal/cli/dispatch.go @@ -35,6 +35,10 @@ func runDispatch(args []string, out, errout io.Writer) int { id := fs.String("id", "", "dispatch id") summary := fs.String("summary", "", "return summary") evidence := fs.String("evidence", "", "comma-separated evidence") + dependencies := fs.String("depends-on", "", "comma-separated dispatch dependencies") + outputHashes := fs.String("output-hashes", "", "comma-separated product-relative path=sha256 outputs") + blockers := fs.String("blockers", "", "comma-separated delegated blockers") + decisionCandidates := fs.String("decision-candidates", "", "comma-separated delegated decision candidates") command := fs.String("command", "", "supervised executable") enable := fs.Bool("enable", false, "explicitly enable supervised execution") wave := fs.String("wave", "", "persisted scheduler wave id") @@ -83,6 +87,23 @@ func runDispatch(args []string, out, errout io.Writer) int { } return 0 case "assign": + if isEngineeringDispatchRole(*role) { + if !*yes || *work == "" || *task == "" || *agent == "" { + fmt.Fprintln(errout, "engineering assignment requires --work --task --role --agent --yes") + return 2 + } + path := *task + if !filepath.IsAbs(path) { + path = filepath.Join(p, filepath.FromSlash(path)) + } + x, e := dispatch.AssignEngineering(p, *work, path, *role, *agent, splitCSV(*dependencies)) + if e != nil { + fmt.Fprintln(errout, e) + return 1 + } + fmt.Fprintln(out, "ASSIGNED", x.ID) + return 0 + } if *role == "technical-discovery" { if !*yes || *work == "" || *task == "" || *agent == "" { fmt.Fprintln(errout, "technical-discovery assignment requires --work --task --agent --yes") @@ -155,6 +176,15 @@ func runDispatch(args []string, out, errout io.Writer) int { fmt.Fprintln(errout, "dispatch return requires --work --id --agent --yes") return 2 } + if len(splitCSV(*outputHashes)) > 0 { + x, e := dispatch.ReturnEngineering(p, *work, *id, *agent, *summary, splitCSV(*evidence), splitCSV(*outputHashes), splitCSV(*blockers), splitCSV(*decisionCandidates)) + if e != nil { + fmt.Fprintln(errout, e) + return 1 + } + fmt.Fprintln(out, "RETURNED", x.ID) + return 0 + } if *reviewInput != "" { data, e := os.ReadFile(*reviewInput) if e != nil { @@ -252,3 +282,12 @@ func runDispatch(args []string, out, errout io.Writer) int { fmt.Fprintln(errout, "unknown dispatch operation") return 2 } + +func isEngineeringDispatchRole(role string) bool { + switch role { + case "technical-landscape", "engineering-standards", "operations-baseline", "engineering-evidence", "engineering-system": + return true + default: + return false + } +} diff --git a/internal/cli/dispatch_engineering_test.go b/internal/cli/dispatch_engineering_test.go new file mode 100644 index 0000000..dfac126 --- /dev/null +++ b/internal/cli/dispatch_engineering_test.go @@ -0,0 +1,63 @@ +package cli + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/JonatasFreireDev/spec-framework/internal/dispatch" +) + +func TestDispatchEngineeringAssignAndReturn(t *testing.T) { + root := t.TempDir() + work := "WORK-ENG-001" + handoff := filepath.Join(root, ".product", "workspaces", work, "engineering-handoff.json") + if err := os.MkdirAll(filepath.Dir(handoff), 0755); err != nil { + t.Fatal(err) + } + contract := []byte(`{"schema_version":1,"execution":{"mode":"delegated","context_policy":"minimal","max_parallel":1,"fallback":"sequential"},"routes":[{"skill":"technical-landscape","phase":1,"depends_on":[],"write_scope":["engineering/catalog"],"status":"pending"}]}`) + if err := os.WriteFile(handoff, contract, 0644); err != nil { + t.Fatal(err) + } + var out, errout bytes.Buffer + code := runDispatch([]string{"assign", "--product-root", root, "--work", work, "--task", ".product/workspaces/" + work + "/engineering-handoff.json", "--role", "technical-landscape", "--agent", "landscape-1", "--yes"}, &out, &errout) + if code != 0 { + t.Fatalf("assign code=%d out=%q err=%q", code, out.String(), errout.String()) + } + fields := strings.Fields(out.String()) + if len(fields) != 2 || fields[0] != "ASSIGNED" { + t.Fatalf("unexpected assign output %q", out.String()) + } + id := fields[1] + outputPath := filepath.Join(root, "engineering", "catalog", "catalog.yaml") + if err := os.MkdirAll(filepath.Dir(outputPath), 0755); err != nil { + t.Fatal(err) + } + content := []byte("schema_version: 1\n") + if err := os.WriteFile(outputPath, content, 0644); err != nil { + t.Fatal(err) + } + sum := sha256.Sum256(content) + out.Reset() + errout.Reset() + code = runDispatch([]string{"return", "--product-root", root, "--work", work, "--id", id, "--agent", "landscape-1", "--summary", "mapped", "--evidence", "catalog", "--output-hashes", "engineering/catalog/catalog.yaml=" + hex.EncodeToString(sum[:]), "--decision-candidates", "DEC-ENG-001", "--yes"}, &out, &errout) + if code != 0 { + t.Fatalf("return code=%d out=%q err=%q", code, out.String(), errout.String()) + } + data, err := os.ReadFile(filepath.Join(root, ".product", "workspaces", work, "dispatches", id+".json")) + if err != nil { + t.Fatal(err) + } + var envelope dispatch.Envelope + if err := json.Unmarshal(data, &envelope); err != nil { + t.Fatal(err) + } + if envelope.Status != "returned" || len(envelope.DecisionCandidates) != 1 || envelope.DecisionCandidates[0] != "DEC-ENG-001" { + t.Fatalf("returned envelope=%+v", envelope) + } +} diff --git a/internal/dispatch/dispatch.go b/internal/dispatch/dispatch.go index 709ab96..ba91651 100644 --- a/internal/dispatch/dispatch.go +++ b/internal/dispatch/dispatch.go @@ -20,30 +20,36 @@ import ( ) type Envelope struct { - Version int `json:"version"` - ID string `json:"id"` - WorkspaceID string `json:"workspace_id"` - TaskID string `json:"task_id"` - UnitKind string `json:"unit_kind"` - UnitPath string `json:"unit_path,omitempty"` - ImportRun string `json:"import_run,omitempty"` - ImportChunk string `json:"import_chunk,omitempty"` - Role string `json:"role"` - Agent string `json:"agent"` - Graph string `json:"graph"` - TaskPath string `json:"task_path"` - InputHash string `json:"input_hash"` - DiffHash string `json:"diff_hash,omitempty"` - ParentID string `json:"parent_id,omitempty"` - RequiredReading []string `json:"required_reading"` - WriteScope []string `json:"write_scope"` - ExpectedEvidence []string `json:"expected_evidence"` - Status string `json:"status"` - CreatedAt string `json:"created_at"` - ReturnedAt string `json:"returned_at,omitempty"` - Summary string `json:"summary,omitempty"` - Evidence []string `json:"evidence,omitempty"` - Forbidden []string `json:"forbidden"` + Version int `json:"version"` + ID string `json:"id"` + WorkspaceID string `json:"workspace_id"` + TaskID string `json:"task_id"` + UnitKind string `json:"unit_kind"` + UnitPath string `json:"unit_path,omitempty"` + ImportRun string `json:"import_run,omitempty"` + ImportChunk string `json:"import_chunk,omitempty"` + Role string `json:"role"` + Agent string `json:"agent"` + Graph string `json:"graph"` + TaskPath string `json:"task_path"` + InputHash string `json:"input_hash"` + DiffHash string `json:"diff_hash,omitempty"` + ParentID string `json:"parent_id,omitempty"` + Dependencies []string `json:"dependencies,omitempty"` + Phase int `json:"phase,omitempty"` + ContextPolicy string `json:"context_policy,omitempty"` + RequiredReading []string `json:"required_reading"` + WriteScope []string `json:"write_scope"` + ExpectedEvidence []string `json:"expected_evidence"` + Status string `json:"status"` + CreatedAt string `json:"created_at"` + ReturnedAt string `json:"returned_at,omitempty"` + Summary string `json:"summary,omitempty"` + Evidence []string `json:"evidence,omitempty"` + OutputHashes []string `json:"output_hashes,omitempty"` + Blockers []string `json:"blockers,omitempty"` + DecisionCandidates []string `json:"decision_candidates,omitempty"` + Forbidden []string `json:"forbidden"` } type Candidate struct { TaskID, Role, Path string @@ -82,6 +88,43 @@ type Config struct { TranscriptRetention int `json:"transcript_retention"` } +type engineeringExecution struct { + Mode string `json:"mode"` + ContextPolicy string `json:"context_policy"` + MaxParallel int `json:"max_parallel"` + Fallback string `json:"fallback"` +} + +type engineeringRoute struct { + Skill string `json:"skill"` + Phase int `json:"phase"` + DependsOn []string `json:"depends_on"` + WriteScope []string `json:"write_scope"` + Status string `json:"status"` +} + +type engineeringHandoff struct { + SchemaVersion int `json:"schema_version"` + Execution engineeringExecution `json:"execution"` + Routes []engineeringRoute `json:"routes"` +} + +var engineeringRoles = map[string]bool{ + "technical-landscape": true, + "engineering-standards": true, + "operations-baseline": true, + "engineering-evidence": true, + "engineering-system": true, +} + +var engineeringWriteScopes = map[string][]string{ + "technical-landscape": {"engineering/architecture", "engineering/catalog"}, + "engineering-standards": {"engineering/standards"}, + "operations-baseline": {"engineering/operations"}, + "engineering-evidence": {"engineering/evidence"}, + "engineering-system": {"engineering/engineering-system.md", "engineering/engineering-system.yaml", "engineering/quality"}, +} + func dir(root, work string) string { return filepath.Join(root, ".product", "workspaces", work, "dispatches") } @@ -119,6 +162,267 @@ func SaveConfig(root string, c Config) error { } return os.WriteFile(configPath(root), append(data, '\n'), 0644) } + +// AssignEngineering persists one harness-native engineering specialist +// assignment. The CLI validates the bounded context and write scope but never +// starts a subagent itself. +func AssignEngineering(root, work, handoffPath, role, agent string, dependencies []string) (Envelope, error) { + if !engineeringRoles[role] { + return Envelope{}, errors.New("unsupported engineering specialist role") + } + if strings.TrimSpace(work) == "" || strings.TrimSpace(agent) == "" || strings.TrimSpace(handoffPath) == "" { + return Envelope{}, errors.New("engineering assignment requires work, handoff path, role, and agent") + } + if err := validateEngineeringHandoffPath(root, work, handoffPath); err != nil { + return Envelope{}, err + } + data, err := os.ReadFile(handoffPath) + if err != nil { + return Envelope{}, err + } + var handoff engineeringHandoff + if err := json.Unmarshal(data, &handoff); err != nil { + return Envelope{}, fmt.Errorf("read engineering handoff: %w", err) + } + if handoff.SchemaVersion != 1 || handoff.Execution.Mode != "delegated" { + return Envelope{}, errors.New("engineering handoff must use schema_version 1 and delegated execution") + } + if handoff.Execution.ContextPolicy != "minimal" { + return Envelope{}, errors.New("delegated engineering context_policy must be minimal") + } + if handoff.Execution.MaxParallel < 1 { + return Envelope{}, errors.New("delegated engineering max_parallel must be at least 1") + } + var route *engineeringRoute + for index := range handoff.Routes { + if handoff.Routes[index].Skill == role { + route = &handoff.Routes[index] + break + } + } + if route == nil { + return Envelope{}, errors.New("engineering role is not present in the handoff routes") + } + if route.Status != "pending" && route.Status != "ready" { + return Envelope{}, errors.New("engineering route is not assignable") + } + if route.Phase < 1 || len(route.WriteScope) == 0 { + return Envelope{}, errors.New("engineering route has no write scope") + } + for _, scope := range route.WriteScope { + if !insideAnyScope(scope, engineeringWriteScopes[role]) { + return Envelope{}, fmt.Errorf("engineering route write scope %s is not owned by %s", scope, role) + } + } + if err := os.MkdirAll(dir(root, work), 0755); err != nil { + return Envelope{}, err + } + release, err := acquireEngineeringAssignmentLock(root, work) + if err != nil { + return Envelope{}, err + } + defer release() + resolved, err := validateEngineeringDependencies(root, work, route.DependsOn, dependencies) + if err != nil { + return Envelope{}, err + } + active, duplicate, err := activeEngineeringAssignments(root, work, role) + if err != nil { + return Envelope{}, err + } + if duplicate { + return Envelope{}, errors.New("engineering specialist role already has an active assignment") + } + if active >= handoff.Execution.MaxParallel { + return Envelope{}, errors.New("delegated engineering assignments reached max_parallel") + } + sum := sha256.Sum256(data) + id := fmt.Sprintf("DISPATCH-%d", time.Now().UTC().UnixNano()) + e := Envelope{ + Version: 1, ID: id, WorkspaceID: work, UnitKind: "engineering-specialist", UnitPath: filepath.ToSlash(handoffPath), + Role: role, Agent: agent, InputHash: hex.EncodeToString(sum[:]), Dependencies: resolved, Phase: route.Phase, + ContextPolicy: "minimal", RequiredReading: []string{filepath.ToSlash(handoffPath), "FRAMEWORK.md", "skills/" + role + "/SKILL.md"}, + WriteScope: route.WriteScope, ExpectedEvidence: []string{"summary", "evidence", "output hashes", "blockers"}, + Status: "assigned", CreatedAt: time.Now().UTC().Format(time.RFC3339), + Forbidden: []string{"approval", "product-decision", "application-code", "commit", "push", "merge", "release", "write-outside-scope"}, + } + return e, write(filepath.Join(dir(root, work), id+".json"), e) +} + +func validateEngineeringDependencies(root, work string, expectedRoles, ids []string) ([]string, error) { + if len(expectedRoles) != len(ids) { + return nil, errors.New("engineering assignment dependencies do not match the handoff route") + } + roles := map[string]bool{} + resolved := append([]string(nil), ids...) + for _, id := range resolved { + if !validDispatchID(id) { + return nil, errors.New("engineering dependency has invalid dispatch id") + } + var dependency Envelope + if err := read(filepath.Join(dir(root, work), id+".json"), &dependency); err != nil { + return nil, fmt.Errorf("read engineering dependency %s: %w", id, err) + } + if dependency.UnitKind != "engineering-specialist" || dependency.Status != "returned" { + return nil, fmt.Errorf("engineering dependency %s is not returned", id) + } + if len(dependency.Blockers) > 0 { + return nil, fmt.Errorf("engineering dependency %s returned blockers", id) + } + if err := verifyEngineeringOutputs(root, dependency); err != nil { + return nil, fmt.Errorf("engineering dependency %s is stale: %w", id, err) + } + roles[dependency.Role] = true + } + for _, role := range expectedRoles { + if !roles[role] { + return nil, fmt.Errorf("engineering dependency role %s is missing", role) + } + } + sort.Strings(resolved) + return resolved, nil +} + +func activeEngineeringAssignments(root, work, role string) (int, bool, error) { + xs, err := Observe(root, work) + if err != nil { + return 0, false, err + } + active := 0 + duplicate := false + for _, item := range xs { + if item.UnitKind == "engineering-specialist" && item.Status == "assigned" { + active++ + if item.Role == role { + duplicate = true + } + } + } + return active, duplicate, nil +} + +func acquireEngineeringAssignmentLock(root, work string) (func(), error) { + lock := filepath.Join(dir(root, work), ".engineering-assign.lock") + for attempt := 0; attempt < 50; attempt++ { + if err := os.Mkdir(lock, 0755); err == nil { + return func() { _ = os.Remove(lock) }, nil + } else if !os.IsExist(err) { + return nil, err + } + if info, err := os.Stat(lock); err == nil && time.Since(info.ModTime()) > 30*time.Second { + _ = os.Remove(lock) + continue + } + time.Sleep(10 * time.Millisecond) + } + return nil, errors.New("engineering assignment lock is busy") +} + +// ReturnEngineering records a compact specialist result after verifying that +// every declared output is inside the assignment scope and still matches its +// SHA-256 hash. It does not grant approval or merge specialist contracts. +func ReturnEngineering(root, work, id, agent, summary string, evidence, outputHashes, blockers, decisionCandidates []string) (Envelope, error) { + var e Envelope + if !validDispatchID(id) { + return e, errors.New("engineering return has invalid dispatch id") + } + release, err := acquireEngineeringAssignmentLock(root, work) + if err != nil { + return e, err + } + defer release() + path := filepath.Join(dir(root, work), id+".json") + if err := read(path, &e); err != nil { + return e, err + } + if e.UnitKind != "engineering-specialist" || e.Agent != agent || e.Status != "assigned" { + return e, errors.New("dispatch is not an assigned engineering specialist for this agent") + } + if strings.TrimSpace(summary) == "" || len(evidence) == 0 || len(outputHashes) == 0 { + return e, errors.New("engineering return requires summary, evidence, and output hashes") + } + currentInput, err := os.ReadFile(filepath.FromSlash(e.UnitPath)) + if err != nil { + return e, fmt.Errorf("read current engineering handoff: %w", err) + } + inputSum := sha256.Sum256(currentInput) + if e.InputHash != hex.EncodeToString(inputSum[:]) { + return e, errors.New("engineering assignment input handoff is stale") + } + for _, item := range outputHashes { + parts := strings.SplitN(item, "=", 2) + if len(parts) != 2 || !insideAnyScope(parts[0], e.WriteScope) { + return e, fmt.Errorf("engineering output %s escapes or omits its write scope", item) + } + data, err := os.ReadFile(filepath.Join(root, filepath.FromSlash(parts[0]))) + if err != nil { + return e, fmt.Errorf("read engineering output %s: %w", parts[0], err) + } + sum := sha256.Sum256(data) + if !strings.EqualFold(parts[1], hex.EncodeToString(sum[:])) { + return e, fmt.Errorf("engineering output hash mismatch for %s", parts[0]) + } + } + e.Status = "returned" + e.Summary = summary + e.Evidence = evidence + e.OutputHashes = append([]string(nil), outputHashes...) + e.Blockers = append([]string(nil), blockers...) + e.DecisionCandidates = append([]string(nil), decisionCandidates...) + e.ReturnedAt = time.Now().UTC().Format(time.RFC3339) + return e, write(path, e) +} + +func insideAnyScope(path string, scopes []string) bool { + path = filepath.ToSlash(filepath.Clean(filepath.FromSlash(path))) + if path == "." || path == ".." || strings.HasPrefix(path, "../") || filepath.IsAbs(filepath.FromSlash(path)) { + return false + } + for _, scope := range scopes { + scope = filepath.ToSlash(strings.TrimSuffix(filepath.Clean(filepath.FromSlash(scope)), "/")) + if path == scope || strings.HasPrefix(path, scope+"/") { + return true + } + } + return false +} + +func validateEngineeringHandoffPath(root, work, handoffPath string) error { + workspace := filepath.Join(root, ".product", "workspaces", work) + relative, err := filepath.Rel(workspace, handoffPath) + if err != nil || relative == "." || relative == ".." || strings.HasPrefix(relative, ".."+string(filepath.Separator)) || filepath.IsAbs(relative) { + return errors.New("engineering handoff must be inside its product workspace") + } + if strings.ToLower(filepath.Ext(relative)) != ".json" { + return errors.New("engineering handoff must be a JSON file") + } + return nil +} + +func validDispatchID(id string) bool { + return strings.HasPrefix(id, "DISPATCH-") && filepath.Base(id) == id && !strings.ContainsAny(id, `/\\`) +} + +func verifyEngineeringOutputs(root string, e Envelope) error { + if len(e.OutputHashes) == 0 { + return errors.New("returned dependency has no output hashes") + } + for _, output := range e.OutputHashes { + parts := strings.SplitN(output, "=", 2) + if len(parts) != 2 || !insideAnyScope(parts[0], e.WriteScope) { + return errors.New("returned dependency output is malformed or outside scope") + } + data, err := os.ReadFile(filepath.Join(root, filepath.FromSlash(parts[0]))) + if err != nil { + return err + } + sum := sha256.Sum256(data) + if !strings.EqualFold(parts[1], hex.EncodeToString(sum[:])) { + return errors.New("returned dependency output hash mismatch") + } + } + return nil +} func Plan(root, graph string) ([]Candidate, error) { nodes, err := workflow.ReadyUnclaimed(root, graph) if err != nil { @@ -356,9 +660,35 @@ func Reconcile(root, work string) ([]Finding, error) { out = append(out, Finding{"review-diff-mismatch", x.ID, "review does not match parent diff", "code-review"}) } } - if x.Status == "assigned" && x.Role != "code-runner" && x.DiffHash == "" { + if x.Status == "assigned" && (x.Role == "qa" || x.Role == "code-review" || x.Role == "security-review") && x.DiffHash == "" { out = append(out, Finding{"review-missing-diff", x.ID, "independent review lacks diff hash", "delivery-orchestrator"}) } + if x.UnitKind == "engineering-specialist" { + for _, dependencyID := range x.Dependencies { + dependency, ok := byID[dependencyID] + if !ok || dependency.UnitKind != "engineering-specialist" || dependency.Status != "returned" { + out = append(out, Finding{"engineering-dependency-not-returned", x.ID, "dependency " + dependencyID + " is missing or no longer returned", "engineering-orchestrator"}) + } + } + if x.Status == "returned" { + for _, output := range x.OutputHashes { + parts := strings.SplitN(output, "=", 2) + if len(parts) != 2 || !insideAnyScope(parts[0], x.WriteScope) { + out = append(out, Finding{"engineering-output-invalid", x.ID, "returned output hash is malformed", "engineering-orchestrator"}) + continue + } + data, err := os.ReadFile(filepath.Join(root, filepath.FromSlash(parts[0]))) + if err != nil { + out = append(out, Finding{"engineering-output-missing", x.ID, "returned output " + parts[0] + " is missing", "engineering-orchestrator"}) + continue + } + sum := sha256.Sum256(data) + if !strings.EqualFold(parts[1], hex.EncodeToString(sum[:])) { + out = append(out, Finding{"engineering-output-stale", x.ID, "returned output " + parts[0] + " changed after return", "engineering-orchestrator"}) + } + } + } + } } sort.Slice(out, func(i, j int) bool { return out[i].DispatchID < out[j].DispatchID }) return out, nil diff --git a/internal/dispatch/dispatch_test.go b/internal/dispatch/dispatch_test.go index 8c9a1c4..6a83394 100644 --- a/internal/dispatch/dispatch_test.go +++ b/internal/dispatch/dispatch_test.go @@ -1,8 +1,12 @@ package dispatch import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" "os" "path/filepath" + "sync" "testing" ) @@ -42,3 +46,239 @@ func TestReconcileReportsOrphanReview(t *testing.T) { t.Fatalf("items=%+v err=%v", items, err) } } + +func TestEngineeringDelegationEnforcesPhasesContextScopeAndReturns(t *testing.T) { + root := t.TempDir() + work := "WORK-ENG-001" + handoffPath := filepath.Join(root, ".product", "workspaces", work, "engineering-handoff.json") + handoff := engineeringHandoff{ + SchemaVersion: 1, + Execution: engineeringExecution{Mode: "delegated", ContextPolicy: "minimal", MaxParallel: 2, Fallback: "sequential"}, + Routes: []engineeringRoute{ + {Skill: "technical-landscape", Phase: 1, WriteScope: []string{"engineering/architecture", "engineering/catalog"}, Status: "pending"}, + {Skill: "engineering-standards", Phase: 2, DependsOn: []string{"technical-landscape"}, WriteScope: []string{"engineering/standards"}, Status: "pending"}, + {Skill: "operations-baseline", Phase: 2, DependsOn: []string{"technical-landscape"}, WriteScope: []string{"engineering/operations"}, Status: "pending"}, + {Skill: "engineering-evidence", Phase: 3, DependsOn: []string{"engineering-standards", "operations-baseline"}, WriteScope: []string{"engineering/evidence"}, Status: "pending"}, + {Skill: "engineering-system", Phase: 4, DependsOn: []string{"technical-landscape", "engineering-standards", "operations-baseline", "engineering-evidence"}, WriteScope: []string{"engineering/engineering-system.md", "engineering/engineering-system.yaml", "engineering/quality"}, Status: "pending"}, + }, + } + data, err := json.MarshalIndent(handoff, "", " ") + if err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(filepath.Dir(handoffPath), 0755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(handoffPath, append(data, '\n'), 0644); err != nil { + t.Fatal(err) + } + + technical, err := AssignEngineering(root, work, handoffPath, "technical-landscape", "landscape-agent", nil) + if err != nil { + t.Fatal(err) + } + if technical.ContextPolicy != "minimal" || technical.Phase != 1 || technical.UnitKind != "engineering-specialist" { + t.Fatalf("technical envelope=%+v", technical) + } + if _, err := AssignEngineering(root, work, handoffPath, "engineering-standards", "standards-agent", nil); err == nil { + t.Fatal("standards assignment accepted without returned technical dependency") + } + technicalOutput := writeEngineeringOutput(t, root, "engineering/catalog/catalog.yaml", "schema_version: 1\n") + if _, err := ReturnEngineering(root, work, technical.ID, technical.Agent, "technical graph mapped", []string{"catalog evidence"}, []string{"../escape=" + technicalOutput}, nil, nil); err == nil { + t.Fatal("engineering return accepted output outside write scope") + } + technical, err = ReturnEngineering(root, work, technical.ID, technical.Agent, "technical graph mapped", []string{"catalog evidence"}, []string{"engineering/catalog/catalog.yaml=" + technicalOutput}, nil, nil) + if err != nil { + t.Fatal(err) + } + + standards, err := AssignEngineering(root, work, handoffPath, "engineering-standards", "standards-agent", []string{technical.ID}) + if err != nil { + t.Fatal(err) + } + operations, err := AssignEngineering(root, work, handoffPath, "operations-baseline", "operations-agent", []string{technical.ID}) + if err != nil { + t.Fatal(err) + } + if _, err := AssignEngineering(root, work, handoffPath, "engineering-evidence", "evidence-agent", []string{standards.ID, operations.ID}); err == nil { + t.Fatal("assignment exceeded max_parallel or accepted unreturned dependencies") + } + standardsHash := writeEngineeringOutput(t, root, "engineering/standards/standards.yaml", "schema_version: 1\n") + standardsReturned, err := ReturnEngineering(root, work, standards.ID, standards.Agent, "standards mapped", []string{"standards evidence"}, []string{"engineering/standards/standards.yaml=" + standardsHash}, nil, []string{"DEC-STANDARDS-001"}) + if err != nil { + t.Fatal(err) + } + if len(standardsReturned.DecisionCandidates) != 1 || standardsReturned.DecisionCandidates[0] != "DEC-STANDARDS-001" { + t.Fatalf("decision candidates were not persisted: %+v", standardsReturned) + } + operationsHash := writeEngineeringOutput(t, root, "engineering/operations/operations.yaml", "schema_version: 1\n") + if _, err := ReturnEngineering(root, work, operations.ID, operations.Agent, "operations mapped", []string{"operations evidence"}, []string{"engineering/operations/operations.yaml=" + operationsHash}, nil, nil); err != nil { + t.Fatal(err) + } + evidence, err := AssignEngineering(root, work, handoffPath, "engineering-evidence", "evidence-agent", []string{standards.ID, operations.ID}) + if err != nil { + t.Fatal(err) + } + if len(evidence.Dependencies) != 2 || evidence.Phase != 3 { + t.Fatalf("evidence envelope=%+v", evidence) + } + if err := os.WriteFile(filepath.Join(root, "engineering", "standards", "standards.yaml"), []byte("changed\n"), 0644); err != nil { + t.Fatal(err) + } + findings, err := Reconcile(root, work) + if err != nil { + t.Fatal(err) + } + foundStale := false + for _, finding := range findings { + if finding.Kind == "engineering-output-stale" && finding.DispatchID == standards.ID { + foundStale = true + } + if finding.Kind == "review-missing-diff" && finding.DispatchID == evidence.ID { + t.Fatal("engineering assignment was misclassified as a review") + } + } + if !foundStale { + t.Fatalf("missing stale engineering output finding: %+v", findings) + } +} + +func TestEngineeringDelegationRejectsSequentialHandoff(t *testing.T) { + root := t.TempDir() + path := filepath.Join(root, ".product", "workspaces", "WORK-001", "handoff.json") + data := []byte(`{"schema_version":1,"execution":{"mode":"sequential","context_policy":"minimal","max_parallel":1,"fallback":"sequential"},"routes":[{"skill":"technical-landscape","phase":1,"write_scope":["engineering/catalog"],"status":"pending"}]}`) + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, data, 0644); err != nil { + t.Fatal(err) + } + if _, err := AssignEngineering(root, "WORK-001", path, "technical-landscape", "agent", nil); err == nil { + t.Fatal("sequential engineering handoff accepted for subagent assignment") + } +} + +func TestEngineeringReturnRejectsStaleHandoff(t *testing.T) { + root := t.TempDir() + work := "WORK-001" + path := filepath.Join(root, ".product", "workspaces", work, "handoff.json") + data := []byte(`{"schema_version":1,"execution":{"mode":"delegated","context_policy":"minimal","max_parallel":1,"fallback":"sequential"},"routes":[{"skill":"technical-landscape","phase":1,"write_scope":["engineering/catalog"],"status":"pending"}]}`) + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, data, 0644); err != nil { + t.Fatal(err) + } + envelope, err := AssignEngineering(root, work, path, "technical-landscape", "agent", nil) + if err != nil { + t.Fatal(err) + } + hash := writeEngineeringOutput(t, root, "engineering/catalog/catalog.yaml", "schema_version: 1\n") + if err := os.WriteFile(path, append(data, '\n'), 0644); err != nil { + t.Fatal(err) + } + if _, err := ReturnEngineering(root, work, envelope.ID, envelope.Agent, "done", []string{"evidence"}, []string{"engineering/catalog/catalog.yaml=" + hash}, nil, nil); err == nil { + t.Fatal("engineering return accepted a stale handoff") + } +} + +func TestEngineeringAssignmentRejectsEscapedHandoffAndWriteScope(t *testing.T) { + root := t.TempDir() + outside := filepath.Join(root, "outside.json") + data := []byte(`{"schema_version":1,"execution":{"mode":"delegated","context_policy":"minimal","max_parallel":1,"fallback":"sequential"},"routes":[{"skill":"technical-landscape","phase":1,"write_scope":["engineering"],"status":"pending"}]}`) + if err := os.WriteFile(outside, data, 0644); err != nil { + t.Fatal(err) + } + if _, err := AssignEngineering(root, "WORK-001", outside, "technical-landscape", "agent", nil); err == nil { + t.Fatal("engineering assignment accepted a handoff outside its workspace") + } + inside := filepath.Join(root, ".product", "workspaces", "WORK-001", "handoff.json") + if err := os.MkdirAll(filepath.Dir(inside), 0755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(inside, data, 0644); err != nil { + t.Fatal(err) + } + if _, err := AssignEngineering(root, "WORK-001", inside, "technical-landscape", "agent", nil); err == nil { + t.Fatal("engineering assignment accepted a write scope outside specialist ownership") + } +} + +func TestEngineeringBlockedReturnDoesNotUnlockDependentPhase(t *testing.T) { + root := t.TempDir() + work := "WORK-001" + path := filepath.Join(root, ".product", "workspaces", work, "handoff.json") + data := []byte(`{"schema_version":1,"execution":{"mode":"delegated","context_policy":"minimal","max_parallel":1,"fallback":"sequential"},"routes":[{"skill":"technical-landscape","phase":1,"depends_on":[],"write_scope":["engineering/catalog"],"status":"pending"},{"skill":"engineering-standards","phase":2,"depends_on":["technical-landscape"],"write_scope":["engineering/standards"],"status":"pending"}]}`) + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, data, 0644); err != nil { + t.Fatal(err) + } + technical, err := AssignEngineering(root, work, path, "technical-landscape", "agent", nil) + if err != nil { + t.Fatal(err) + } + hash := writeEngineeringOutput(t, root, "engineering/catalog/catalog.yaml", "schema_version: 1\n") + technical, err = ReturnEngineering(root, work, technical.ID, technical.Agent, "partial", []string{"catalog"}, []string{"engineering/catalog/catalog.yaml=" + hash}, []string{"repository boundary unresolved"}, nil) + if err != nil { + t.Fatal(err) + } + if _, err := AssignEngineering(root, work, path, "engineering-standards", "standards", []string{technical.ID}); err == nil { + t.Fatal("blocked engineering return unlocked a dependent phase") + } +} + +func TestEngineeringAssignmentCapacityIsAtomic(t *testing.T) { + root := t.TempDir() + work := "WORK-001" + path := filepath.Join(root, ".product", "workspaces", work, "handoff.json") + data := []byte(`{"schema_version":1,"execution":{"mode":"delegated","context_policy":"minimal","max_parallel":1,"fallback":"sequential"},"routes":[{"skill":"technical-landscape","phase":1,"depends_on":[],"write_scope":["engineering/catalog"],"status":"pending"},{"skill":"engineering-standards","phase":1,"depends_on":[],"write_scope":["engineering/standards"],"status":"pending"}]}`) + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, data, 0644); err != nil { + t.Fatal(err) + } + start := make(chan struct{}) + results := make(chan error, 2) + var wait sync.WaitGroup + for _, assignment := range []struct{ role, agent string }{{"technical-landscape", "agent-1"}, {"engineering-standards", "agent-2"}} { + assignment := assignment + wait.Add(1) + go func() { + defer wait.Done() + <-start + _, err := AssignEngineering(root, work, path, assignment.role, assignment.agent, nil) + results <- err + }() + } + close(start) + wait.Wait() + close(results) + succeeded := 0 + failed := 0 + for err := range results { + if err == nil { + succeeded++ + } else { + failed++ + } + } + if succeeded != 1 || failed != 1 { + t.Fatalf("atomic capacity succeeded=%d failed=%d", succeeded, failed) + } +} + +func writeEngineeringOutput(t *testing.T, root, relative, content string) string { + t.Helper() + path := filepath.Join(root, filepath.FromSlash(relative)) + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, []byte(content), 0644); err != nil { + t.Fatal(err) + } + sum := sha256.Sum256([]byte(content)) + return hex.EncodeToString(sum[:]) +} diff --git a/internal/install/install_test.go b/internal/install/install_test.go index 39a196d..4ec224b 100644 --- a/internal/install/install_test.go +++ b/internal/install/install_test.go @@ -289,6 +289,10 @@ func TestInitShipsEngineeringCatalogRootsForEveryAgentTarget(t *testing.T) { t.Errorf("runtime missing engineering skill %s: %v", skill, err) } } + handoff, err := os.ReadFile(filepath.Join(result.SpecRoot, "skills", "engineering-orchestrator", "assets", "engineering-baseline-handoff-template.json")) + if err != nil || !strings.Contains(string(handoff), `"mode": "sequential"`) || !strings.Contains(string(handoff), `"context_policy": "minimal"`) || !strings.Contains(string(handoff), `"skill": "engineering-system"`) { + t.Fatalf("runtime engineering delegation contract missing: %v", err) + } aggregate, err := os.ReadFile(filepath.Join(target, "product", "engineering", "engineering-system.yaml")) if err != nil { t.Fatal(err) diff --git a/internal/runtimeassets/runtime_test.go b/internal/runtimeassets/runtime_test.go index 7b5e7e2..f1c4ca5 100644 --- a/internal/runtimeassets/runtime_test.go +++ b/internal/runtimeassets/runtime_test.go @@ -43,6 +43,11 @@ func TestEnsureMaterializesVersionedAssets(t *testing.T) { t.Fatal(err) } } + for _, name := range []string{"skills/engineering-orchestrator/assets/engineering-baseline-handoff-template.json", "skills/subagent-return-reviewer/assets/engineering-specialist-return-template.json"} { + if _, err := os.Stat(filepath.Join(root, filepath.FromSlash(name))); err != nil { + t.Fatal(err) + } + } if _, err := os.Stat(filepath.Join(root, "docs", "engineering-catalog-and-standards.md")); err != nil { t.Fatal(err) } diff --git a/starter/AGENTS.md b/starter/AGENTS.md index e438f27..071d612 100644 --- a/starter/AGENTS.md +++ b/starter/AGENTS.md @@ -32,6 +32,8 @@ Do not edit framework-owned assets to encode product scope. Put product-specific Start with Framework Guide unless a current CLI result or explicit human request names both the specialist and the concrete artifact or workspace scope. Revalidate persisted handoffs/checkpoints with `dashboard`, `status`, `next`, or `guide` before routing directly. A skill name without scope is only a hint. For a new product, continue through Product Orchestrator, Problem Discovery, Vision, and Strategy before creating domains or features. For `existing-documents`, let Artifact Importer read every inventoried source and complete `traceability.json` and `mapping.json` before requesting materialization. For a scalable import, claim and review one persisted chunk at a time with source evidence; never mark a chunk reviewed without it. +When Engineering Orchestrator selects delegated execution, use only harness-native subagents backed by persisted `engineering-specialist` dispatch envelopes. Pass minimal context, respect dependencies and write scopes, and reconcile compact hash-verified returns. Otherwise use the compatible sequential route; the CLI does not spawn agents. + For the shared engineering baseline, route through Engineering Orchestrator. It sequences Technical Landscape, Engineering Standards, Operations Baseline, Engineering Evidence, and Engineering System consolidation, then stops for human approval before domain modeling or delivery-specific Technical Discovery. ## Canonical Delivery Flow diff --git a/starter/README.md b/starter/README.md index 517805e..bda0611 100644 --- a/starter/README.md +++ b/starter/README.md @@ -38,7 +38,7 @@ These are the default `new-product` steps. For every initialized repository, rea 6. Continue through Goal -> Feature -> Use Case -> Specification -> Design -> Technical Discovery -> applicable Engineering Proposal and Engineering Review -> Implementation Plan -> Execution Graph -> Tasks. 7. Replace product gate placeholders in `product/knowledge/conventions/gates.md` before marking executable work as implemented or validated. 8. Use `spec-framework work`, then `resume`, leases, checkpoints, and handoffs so execution can be continued without conversation history. -9. Use `engineering-orchestrator` to establish the shared technical baseline, `spec-framework engineering-system inspect` to inspect its aggregate, `spec-framework dashboard --work WORK-001` for the consolidated flow, and `spec-framework decisions migrate` to preview legacy decision metadata upgrades. +9. Use `engineering-orchestrator` to establish the shared technical baseline sequentially or, when explicitly selected and supported by the harness, through minimal-context native subagents backed by dispatch envelopes. Use `spec-framework engineering-system inspect` to inspect its aggregate, `spec-framework dashboard --work WORK-001` for the consolidated flow, and `spec-framework decisions migrate` to preview legacy decision metadata upgrades. 11. Run `spec-framework decisions check --strict` in CI to validate decision paths, domains, approvals, references, and navigable links. 10. For `existing-documents`, ask the Artifact Importer agent to read each source and fill `traceability.json` plus proposed `mapping.json` entries before materialization; for a large source set, use bounded import chunks with evidence per source and review unmapped gaps explicitly.