feat(extensions): add assess idea assessment pipeline extension#3568
Conversation
Add a role-neutral, opt-in "Idea Assessment Pipeline" extension (id: assess) covering the discovery work that happens BEFORE spec-driven development. It provides a five-stage funnel: intake, research, define, shape, decide, each writing one artifact under .specify/assessments/<slug>/. A go verdict hands off to /speckit.specify; killing an idea is a first-class success outcome. Registration: - extensions/catalog.json: bundled core opt-in entry (before bug) - pyproject.toml: force-include maps into core_pack so it ships in the installed wheel (verified via wheel build) Also normalizes a Rich-wrapped substring assertion in test_workflows.py so the suite passes at CI's 80-column non-TTY width. Assisted-by: GitHub Copilot (model: Claude Opus 4.8, supervised) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1f25cf9d-b7eb-4b2b-b811-3e91d8db8f6a
There was a problem hiding this comment.
Pull request overview
Adds the opt-in assess extension for evaluating ideas before specification.
Changes:
- Adds five assessment-stage commands and documentation.
- Registers and packages the bundled extension with an optional hook.
- Adds extension tests and stabilizes a terminal-width-sensitive assertion.
Show a summary per file
| File | Description |
|---|---|
extensions/assess/README.md |
Documents the assessment pipeline. |
extensions/assess/extension.yml |
Defines commands, metadata, and hook. |
extensions/assess/commands/speckit.assess.intake.md |
Adds idea intake workflow. |
extensions/assess/commands/speckit.assess.research.md |
Adds evidence-gathering workflow. |
extensions/assess/commands/speckit.assess.define.md |
Adds problem-definition workflow. |
extensions/assess/commands/speckit.assess.shape.md |
Adds concept-shaping workflow. |
extensions/assess/commands/speckit.assess.decide.md |
Adds assessment decision gate. |
extensions/catalog.json |
Registers the bundled extension. |
pyproject.toml |
Includes the extension in wheels. |
tests/extensions/assess/__init__.py |
Initializes the test package. |
tests/extensions/assess/test_assess_extension.py |
Tests layout, registration, and installation. |
tests/test_workflows.py |
Normalizes wrapped CLI output assertions. |
Review details
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 11/12 changed files
- Comments generated: 9
- Review effort level: Medium
Resolve review feedback on github#3568: - catalog.json: bump top-level updated_at to this revision (2026-07-17) - extension.yml + catalog.json: shorten the assess description to under the documented 200-char manifest limit (kept aligned across both) - extension.yml: make the before_specify hook prompt condition-neutral (it fires on every /speckit.specify, so it must not claim "no assessment found") - intake.md: fix slug normalization to explicitly allow lowercase letters a-z (the old rule permitted only digits and '-', contradicting the offline-mode example) - intake.md + research.md: require a sanitized source URL (strip userinfo and credential/signature query params) instead of persisting a verbatim URL that could leak secrets into project artifacts - decide.md: remove the "trivially small" exception so a go always requires a shaped concept, making verdict behavior deterministic and consistent with the guardrails and README Assisted-by: GitHub Copilot (model: Claude Opus 4.8, supervised) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1f25cf9d-b7eb-4b2b-b811-3e91d8db8f6a
|
Thanks for the review. Addressed all nine comments in 4c8124d:
Tests green at CI's 80-column width: Disclosure: this response and commit 4c8124d were authored by GitHub Copilot (model: Claude Opus 4.8) under human supervision, on behalf of @mnriem. |
Assess is a separate business process from spec-driven development, so it should not inject itself into the /speckit.specify lifecycle. The hook fired on every /speckit.specify invocation (it had no condition), nagging even when an assessment already existed and the user was deliberately proceeding. Unlike git's before_specify (a mechanical prerequisite: create a feature branch) or agent-context's after_* hooks (reacting to spec output), assess is an upstream, optional, human-judgment process. The coupling that belongs here already runs forward and by choice: a `go` verdict from /speckit.assess.decide hands off to /speckit.specify. The backward hook was the redundant, intrusive direction. - extension.yml: drop the hooks block (commands-only manifest) - README.md: replace the Hooks section with a Handoff section - test: replace the hook assertion with test_declares_no_hooks to lock in the standalone-pipeline design Assisted-by: GitHub Copilot (model: Claude Opus 4.8, supervised) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1f25cf9d-b7eb-4b2b-b811-3e91d8db8f6a
Address the second review round on github#3568: - Slug path traversal: intake and all four downstream commands (research, define, shape, decide) now normalize an explicit or user-supplied slug to the [a-z0-9-] alphabet (dropping '.', '/', '\\') and reject an empty normalized result before constructing ASSESS_DIR. This guarantees a slug like `../..` cannot escape .specify/assessments/. - Metadata accuracy: the extension.yml and catalog.json descriptions no longer imply a "build/kill" call is handed to /speckit.specify — only a `go` hands off; a `kill` closes the assessment. - Verdict determinism (decide): a `go` now explicitly requires evidence strength `adequate`+ (never weak/unknown), resolving the conflict with the thin-evidence guardrail. - Risk polarity (decide): renamed the "Risk" criterion to "Risk posture" with positive polarity (strong = risks understood and mitigated) so it composes with the other scores that feed the verdict. - README: aligned the go-threshold guardrail with the evidence rule and documented the slug-normalization safety property. The PR description was also updated to drop the stale before_specify hook claim (the hook was removed in the previous commit). Assisted-by: GitHub Copilot (model: Claude Opus 4.8, supervised) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1f25cf9d-b7eb-4b2b-b811-3e91d8db8f6a
|
Addressed all ten comments from this round in 9cd07fb. Slug path traversal (intake, research, define, shape, decide) — every command that accepts an explicit or user-supplied slug now normalizes it to the Metadata accuracy ( Verdict determinism ( Risk polarity ( PR description — dropped the stale Tests green at CI's 80-column width: Disclosure: this response and commit 9cd07fb were authored by GitHub Copilot (model: Claude Opus 4.8) under human supervision, on behalf of @mnriem. |
…st allowlist Address the third review round on github#3568: - Path safety (intake, research, define, shape, decide): slug normalization blocks lexical `..` but not symlinked path components. Each command now, before any mkdir/read/write, resolves the real path of .specify/assessments/<slug>/ and every artifact, refuses to follow a symlinked .specify / assessments / slug dir / artifact, and verifies the resolved path stays inside the project root. This blocks a cloned or crafted project from redirecting reads/writes outside the repository. Each stage enforces this independently since research/define/decide can run without intake. - research URL policy: replaced the open-ended "and comparable well-known hosts" no-prompt branch with intake's exact enumerated allowlist, so an agent cannot classify an attacker-controlled host as "comparable" and fetch it without confirmation. - README: guardrail now documents symlink/realpath containment. Assisted-by: GitHub Copilot (model: Claude Opus 4.8, supervised) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1f25cf9d-b7eb-4b2b-b811-3e91d8db8f6a
|
Addressed all six comments from this round in c032a2e. Symlink traversal (intake, research, define, shape, decide) — you're right that normalizing the slug only blocks lexical Open-ended host heuristic (research) — replaced the "and comparable well-known hosts" no-prompt branch with intake's exact enumerated allowlist ( The README guardrail now documents the symlink/realpath containment property too. Tests green at CI's 80-column width: Disclosure: this response and commit c032a2e were authored by GitHub Copilot (model: Claude Opus 4.8) under human supervision, on behalf of @mnriem. |
…-slug collision Address the fourth review round on github#3568 (intake): - Secret leak in the captured idea: quoting the original "verbatim" contradicted the URL sanitization rule when the idea itself contained a credential-bearing URL. Capture now redacts secrets (sanitize URLs; strip tokens, passwords, keys, cookies) inside the quoted text as well as the Source field, and the section heading is "Idea (as captured)" rather than "verbatim". - Explicit-slug collision: in automated mode an existing intake.md caused a silent switch to a new slug, contradicting the no-suffix guarantee for user-provided slugs. Now: user-provided slug collision -> stop and report; only a self-generated slug (already disambiguated at resolution) is re-slugged. Assisted-by: GitHub Copilot (model: Claude Opus 4.8, supervised) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1f25cf9d-b7eb-4b2b-b811-3e91d8db8f6a
|
Addressed both comments from this round in 05968e4 (both in Secret leak in the captured idea — you're right that quoting the original "verbatim" contradicted the URL-sanitization rule when the idea itself contains a credential-bearing URL. Capture step 1 and the template now redact secrets inside the quoted text too (sanitize URLs; strip tokens, passwords, keys, cookies), matching the Source field. Renamed the section from "Idea (verbatim)" to "Idea (as captured)" so the heading no longer implies literal preservation of secrets. Explicit-slug collision — in automated mode an existing Tests green at CI's 80-column width: Disclosure: this response and commit 05968e4 were authored by GitHub Copilot (model: Claude Opus 4.8) under human supervision, on behalf of @mnriem. |
…policy Address the remaining open comment from review 4722852090 on github#3568 (the other six comments in that round were already resolved by the slug-validation and host-allowlist fixes in 9cd07fb and c032a2e). The URL Trust Policy refused only textual IPv4 loopback/RFC1918/metadata hosts, so an approved hostname resolving to an internal IPv6 or IPv4-mapped address could still reach internal services. The refuse- outright list now covers IPv6 link-local (fe80::/10), unique-local (fc00::/7), IPv4-mapped forms, and the IPv6 metadata address, and adds a resolution-time check: even an allowlisted or user-confirmed host is refused when it resolves to any non-public address, defeating DNS rebinding. Mirrored the summary in research's inherited-policy note. Assisted-by: GitHub Copilot (model: Claude Opus 4.8, supervised) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1f25cf9d-b7eb-4b2b-b811-3e91d8db8f6a
|
While reconciling review threads I found an earlier review round (4722852090, 7 comments) that hadn't been explicitly addressed. Six of its seven comments were already resolved by later commits:
The one remaining comment is now addressed in b3faa63: SSRF / IPv6 gap in the URL Trust Policy — the refuse-outright list only covered textual IPv4 loopback/RFC1918/metadata hosts. It now also refuses IPv6 link-local ( Tests green at CI's 80-column width: Disclosure: this response and commit b3faa63 were authored by GitHub Copilot (model: Claude Opus 4.8) under human supervision, on behalf of @mnriem. |
|
Addressed all four comments from this round in ecc090e. DNS rebinding (intake + research) — fair point: a separate DNS lookup followed by a hostname fetch is TOCTOU-vulnerable, since the client can re-resolve or select a private address from a mixed answer. The policy now requires the fetch to pin the connection to a validated public address (or verify the connected peer's IP after connecting) and re-apply the refusal ranges to the address actually connected to. If the available fetch mechanism cannot pin the address or expose the connected peer, the fetch is refused rather than trusted by hostname. Slug-only direct entry (research + define) — when Also removed a leftover duplicate Tests green at CI's 80-column width: Disclosure: this response and commit ecc090e were authored by GitHub Copilot (model: Claude Opus 4.8) under human supervision, on behalf of @mnriem. |
Address the sixth review round on github#3568. The intake guardrail said the command "only reads and writes inside .specify/assessments/<slug>/", which contradicts its documented inputs: intake must read a codebase pointer (repository inspection) and fetch an allowed URL to capture the idea. The guardrail now limits only *writes* to the assessment directory and explicitly permits read-only inspection of the supplied sources (repo + allowlisted URL fetch under the URL Trust Policy). The other four commands already phrased this correctly ("read only, and write inside ...") and are unchanged. Assisted-by: GitHub Copilot (model: Claude Opus 4.8, supervised) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1f25cf9d-b7eb-4b2b-b811-3e91d8db8f6a
|
Addressed the single comment from this round in ce635f3. Intake read/write guardrail — you're right, "only reads and writes inside The other four commands already phrased this correctly ("read only, and write inside …"), so they're unchanged. Tests green at CI's 80-column width: Disclosure: this response and commit ce635f3 were authored by GitHub Copilot (model: Claude Opus 4.8) under human supervision, on behalf of @mnriem. |
…s, research dir creation Addresses review 4723905370 on PR github#3568 across three themes: - Ancestor path safety: verify `.specify` and `.specify/assessments` are real directories (not symlinks) resolving inside the project root before any filesystem-based slug resolution, in all five commands. - Untrusted artifact reads: treat the contents of persisted assessment artifacts (intake/research/problem/concept) as untrusted data, not instructions — ignore embedded directives, mirroring the URL Trust Policy. - research now ensures the validated ASSESS_DIR exists before writing, since it may be the first assessment command run. Assisted-by: GitHub Copilot (model: Claude Opus 4.8, supervised) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1f25cf9d-b7eb-4b2b-b811-3e91d8db8f6a
|
Addressed review 4723905370 in cb4b95e, grouped into three themes: Ancestor path safety (before slug resolution). All five commands now verify that Untrusted artifact reads. research directory creation. Tests: 21 passed ( Posted on behalf of @mnriem by GitHub Copilot (model: Claude Opus 4.8). Commits carry |
Addresses review 4723955260 on PR github#3568. The ancestor path-safety clause required `.specify/assessments` to already be a real directory, which blocked the first-run commands (intake, research, define) from ever reaching the step that creates it. Reword the clause in all five commands so a not-yet-created directory is permitted, while still refusing when `.specify` or `.specify/assessments` exists as a symlink or escapes the project root. Assisted-by: GitHub Copilot (model: Claude Opus 4.8, supervised) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1f25cf9d-b7eb-4b2b-b811-3e91d8db8f6a
|
Addressed review 4723955260 in b2f609e. Good catch — the ancestor path-safety clause I added last round required Reworded the clause in all five commands so that a not-yet-created directory is permitted (it will be created safely later), while still refusing when Tests: 21 passed ( Posted on behalf of @mnriem by GitHub Copilot (model: Claude Opus 4.8). Commits carry |
Addresses review 4724027270 on PR github#3568. The overview flowchart routed every needs-clarification verdict back to research, but decide.md's Revisit stage can send an idea back to intake, research, define, or shape. Reroute the arrow as a generic loop back to the earlier stages so the diagram no longer misstates the pipeline. Assisted-by: GitHub Copilot (model: Claude Opus 4.8, supervised) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1f25cf9d-b7eb-4b2b-b811-3e91d8db8f6a
|
Addressed review 4724027270 in a3e2ac1. The overview flowchart routed every Docs-only change; assess tests still pass (9 passed). Posted on behalf of @mnriem by GitHub Copilot (model: Claude Opus 4.8). Commits carry |
Addresses review 4724080304 on PR github#3568: - decide.md frontmatter description hard-coded `/speckit.specify`. Frontmatter is parsed before command-reference resolution, so it now uses agent-neutral wording ("hand survivors off into Spec-Driven Development") instead of a dot-style literal that would be wrong for non-dot integrations. - The `## If go — Handoff to …` heading inside the decision.md output template hard-coded `/speckit.specify`, which would be written verbatim into decision.md. It now uses the `__SPECKIT_COMMAND_SPECIFY__` placeholder, like the rest of the command, so the active integration's invocation style is rendered. Assisted-by: GitHub Copilot (model: Claude Opus 4.8, supervised) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1f25cf9d-b7eb-4b2b-b811-3e91d8db8f6a
|
Addressed review 4724080304 in c59c457. Both comments concerned hard-coded dot-style Frontmatter description (line 2). Since extension frontmatter is parsed before command-reference resolution, a placeholder wouldn't render there. Switched to agent-neutral wording — "hand survivors off into Spec-Driven Development" — so generated help/skill metadata is correct regardless of the integration's command style. Handoff heading in the decision.md template (line 74). This literal was inside the fenced block that gets written verbatim into No dot-style literals remain in the command files. Tests: 21 passed. Posted on behalf of @mnriem by GitHub Copilot (model: Claude Opus 4.8). Commits carry |
Description
Adds a new opt-in, role-neutral extension —
assess, the "Idea Assessment Pipeline" — covering the discovery/problem-assessment work that happens before spec-driven development begins (i.e., before/speckit.specify).Today Spec Kit starts at the point where you already know what you're building.
assessfills the gap in front of that: a lightweight, structured funnel for pressure-testing an idea before it earns a spec. It is deliberately role-neutral (named for the activity, not a job title) so it composes cleanly into role bundles rather than competing with them.Pipeline (five stages, one artifact each)
Each command writes a single markdown artifact under
.specify/assessments/<slug>/:/speckit.assess.intakeintake.md/speckit.assess.researchresearch.md/speckit.assess.defineproblem.md/speckit.assess.shapeconcept.md/speckit.assess.decidedecision.mdgoverdict hands off to/speckit.specify./speckit.specify. The only coupling is the forwarddecide → /speckit.specifyhandoff.Registration (core opt-in)
extensions/catalog.json: bundled entry ("bundled": true), alphabetically beforebug.pyproject.toml:force-includemapsextensions/assess→specify_cli/core_pack/extensions/assessso it ships in the installed wheel. Verified by building the wheel and confirming all files land incore_pack.specify init; users opt in withspecify extension add assess.It mirrors the existing
bugextension's conventions (slug resolution, URL Trust Policy,__SPECKIT_COMMAND_*__cross-references, guardrails).Note on
tests/test_workflows.pyOne assertion in
test_add_from_url_download_failure_cleanup_error_preserves_original_errormatched a multi-word substring against Rich-rendered panel output, which wraps mid-phrase at CI's 80-column non-TTY width and fails there. Normalized it with the same whitespace-stripping pattern already used one line above. This is the only non-assesschange and is required for green CI. (A broader pass over other width-fragile tests that only fail at artificially narrow widths is left for a separate PR.)Testing
uv run specify --helpuv sync && uv run pytest— 4508 passed, 5 skipped, 0 failed at CI's 80-column widthassessships incore_packtests/extensions/assess/test_assess_extension.py(9 tests: layout, catalog registration, bundle resolution, install, and atest_declares_no_hooksguard)AI Disclosure
This change was developed with GitHub Copilot (model: Claude Opus 4.8) under human supervision. Copilot drafted the extension commands, README, tests, and packaging/registration changes; the author reviewed and validated the design, ran the test suite, and confirmed wheel packaging. Commits carry
Assisted-by:trailers.