diff --git a/extensions/assess/README.md b/extensions/assess/README.md index 7aae736ce..65edec1fa 100644 --- a/extensions/assess/README.md +++ b/extensions/assess/README.md @@ -94,7 +94,8 @@ specify extension enable assess - Only `speckit.assess.*` commands write, and only inside `.specify/assessments//`. **None of them modify source code** — solution design and implementation belong to the SDD lifecycle (`/speckit.specify` onward). - Web content fetched during `intake`/`research` is treated as untrusted data, governed by an explicit URL Trust Policy (allowlisted public sources fetched freely; unknown hosts prompted or skipped; loopback/RFC1918/metadata endpoints refused). - Evidence is never over-claimed: unsourced statements are tagged `ASSUMPTION`, and `research.md` always includes an *Evidence Against the Idea* section. -- Verdicts are never over-claimed: a `go` requires a valid problem and a shaped concept; otherwise the honest verdict is `needs-clarification`. +- Verdicts are never over-claimed: a `go` requires a valid problem, `adequate`+ evidence (never weak/unknown), and a shaped concept; otherwise the honest verdict is `needs-clarification`. +- Slugs are normalized to `[a-z0-9-]` and an empty result is rejected, so an assessment directory can never escape `.specify/assessments/`. - No command overwrites an existing artifact without confirmation; in automated mode it refuses. ## Relationship to Other Extensions diff --git a/extensions/assess/commands/speckit.assess.decide.md b/extensions/assess/commands/speckit.assess.decide.md index 2951a71c8..cd057f307 100644 --- a/extensions/assess/commands/speckit.assess.decide.md +++ b/extensions/assess/commands/speckit.assess.decide.md @@ -14,7 +14,7 @@ Decide **judges; it does not spec or build.** It weighs the evidence already gat $ARGUMENTS ``` -Resolve the slug: explicit `slug=…` → conversation context (a slug reported earlier this session, confirmed by an existing `.specify/assessments//` directory) → ask (interactive) → single existing directory (automated) → otherwise stop and ask. Set `ASSESS_SLUG` and `ASSESS_DIR = .specify/assessments/`. +Resolve the slug: explicit `slug=…` → conversation context (a slug reported earlier this session, confirmed by an existing `.specify/assessments//` directory) → ask (interactive) → single existing directory (automated) → otherwise stop and ask. **Slug safety**: normalize any explicit or user-supplied slug — lowercase; whitespace/underscores → `-`; keep only `[a-z0-9-]` (drop every other character, including `.`, `/`, `\`); collapse and trim `-`; reject an empty normalized result. Only then set `ASSESS_SLUG` (the normalized value) and `ASSESS_DIR = .specify/assessments/` — this keeps every read and write inside `.specify/assessments/`. ## Prerequisites @@ -31,9 +31,9 @@ Resolve the slug: explicit `slug=…` → conversation context (a slug reported - **Value vs. cost of inaction** — does solving it beat doing nothing? (from `problem.md`) - **Feasibility / appetite fit** — is there a credible option within a sane appetite? (from `concept.md`) - **Strategic fit** — does it align with the project's constitution/goals, if known? - - **Risk** — what could make this fail or backfire? + - **Risk posture** — are the major risks understood and acceptably mitigated? Rate with the same positive polarity as the other criteria: `strong` = key risks identified and credibly mitigated; `weak` = serious, unmitigated risk. (from all artifacts) 2. **Reach a verdict**: - - **go** — the idea is worth specifying. Requires problem validity `adequate`+ and a recommended concept option. + - **go** — the idea is worth specifying. Requires problem validity `adequate`+, **evidence strength `adequate`+ (never `weak` or `unknown`)**, and a recommended concept option. If evidence is `weak`/`unknown`, the verdict is `needs-clarification`, not `go`. - **needs-clarification** — promising but blocked on specific unknowns. List exactly what must be answered and which stage to revisit. - **kill** — not worth building now. State the decisive reason plainly (weak problem, better alternative exists, cost > value, out of scope, superseded). 3. **Record the rationale** so the decision is auditable months later. Any `unknown` score must be acknowledged, not glossed. @@ -58,7 +58,7 @@ Write `ASSESS_DIR/decision.md`: | Value vs. inaction | … | … | | Feasibility / appetite | … | … | | Strategic fit | … | … | -| Risk | … | … | +| Risk posture | … | … | ## Verdict & Rationale diff --git a/extensions/assess/commands/speckit.assess.define.md b/extensions/assess/commands/speckit.assess.define.md index 4aede34a0..3c3889614 100644 --- a/extensions/assess/commands/speckit.assess.define.md +++ b/extensions/assess/commands/speckit.assess.define.md @@ -14,7 +14,7 @@ Define **frames the problem; it does not shape or choose a solution.** If the in $ARGUMENTS ``` -Resolve the slug: explicit `slug=…` → conversation context (a slug reported earlier this session, confirmed by an existing `.specify/assessments//` directory) → ask (interactive) → single existing directory (automated) → otherwise stop and ask. Set `ASSESS_SLUG` and `ASSESS_DIR = .specify/assessments/`. +Resolve the slug: explicit `slug=…` → conversation context (a slug reported earlier this session, confirmed by an existing `.specify/assessments//` directory) → ask (interactive) → single existing directory (automated) → otherwise stop and ask. **Slug safety**: normalize any explicit or user-supplied slug — lowercase; whitespace/underscores → `-`; keep only `[a-z0-9-]` (drop every other character, including `.`, `/`, `\`); collapse and trim `-`; reject an empty normalized result. Only then set `ASSESS_SLUG` (the normalized value) and `ASSESS_DIR = .specify/assessments/` — this keeps every read and write inside `.specify/assessments/`. ## Prerequisites diff --git a/extensions/assess/commands/speckit.assess.intake.md b/extensions/assess/commands/speckit.assess.intake.md index 08e718c2e..af31b1c1e 100644 --- a/extensions/assess/commands/speckit.assess.intake.md +++ b/extensions/assess/commands/speckit.assess.intake.md @@ -27,11 +27,13 @@ If the input is empty, ask the user for the idea (interactive), or stop with a n Each idea gets its own directory under `.specify/assessments//`. Resolve the slug in this order: -1. **User-provided slug**: If the user explicitly passes a slug (e.g., `slug=offline-mode`, `--slug offline-mode`, or an obvious slug-like token), use it verbatim after normalization (lowercase; hyphen-separated; no spaces; keep only lowercase letters `a–z`, digits `0–9`, and `-`). Do not append timestamps or numbers. +1. **User-provided slug**: If the user explicitly passes a slug (e.g., `slug=offline-mode`, `--slug offline-mode`, or an obvious slug-like token), normalize it: lowercase; convert runs of whitespace/underscores to `-`; keep only lowercase letters `a–z`, digits `0–9`, and `-`; drop every other character (including `.`, `/`, `\`); collapse repeated `-`; strip leading/trailing `-`. Do not append timestamps or numbers. 2. **Interactive mode** (a human is driving): If no slug was provided, **ask the user** and wait. Suggest a 2–4 word kebab-case candidate derived from the idea as a default. 3. **Automated / non-interactive mode** (no human to ask): Generate a concise slug yourself (2–4 kebab-case words). The generated slug **MUST** produce a unique directory — if `.specify/assessments//` already exists, append the shortest disambiguating suffix (`-2`, `-3`, …) or a short ISO-style date (`-20260715`). Never overwrite an existing assessment directory. -After resolution, set `ASSESS_SLUG` and `ASSESS_DIR = .specify/assessments/`. +**Reject unsafe slugs.** If the normalized slug is empty (e.g. the input was `../..`, `/`, or non-ASCII-only), refuse it: ask again (interactive) or stop with a note (automated). Never build a path from an unnormalized slug — normalization strips `.`, `/`, and `\`, which guarantees `ASSESS_DIR` cannot escape `.specify/assessments/`. + +After resolution, set `ASSESS_SLUG` (the normalized, validated value) and `ASSESS_DIR = .specify/assessments/`. ## Prerequisites diff --git a/extensions/assess/commands/speckit.assess.research.md b/extensions/assess/commands/speckit.assess.research.md index f0bc13b24..f5efa36bc 100644 --- a/extensions/assess/commands/speckit.assess.research.md +++ b/extensions/assess/commands/speckit.assess.research.md @@ -16,11 +16,13 @@ $ARGUMENTS The input carries the slug and (optionally) research direction or links. Resolve the slug: -1. **Explicit slug** (`slug=…`, `--slug …`, or an obvious token) — use it verbatim after normalization. +1. **Explicit slug** (`slug=…`, `--slug …`, or an obvious token) — normalize it (see **Slug safety** below). 2. **Conversation context** — if this session just ran `__SPECKIT_COMMAND_ASSESS_INTAKE__`, reuse the slug it reported. Confirm by checking that `.specify/assessments//intake.md` exists; if not, fall through. 3. **Interactive** — ask the user for the slug and wait. 4. **Automated** — if exactly one assessment directory exists, use it; otherwise stop and ask. +**Slug safety**: normalize any explicit or user-supplied slug to the slug alphabet — lowercase; whitespace/underscores → `-`; keep only `[a-z0-9-]` (drop every other character, including `.`, `/`, `\`); collapse and trim `-`. **Reject** a slug whose normalized form is empty. Only then set `ASSESS_SLUG` (the normalized value) and `ASSESS_DIR = .specify/assessments/` — this keeps every read and write inside `.specify/assessments/`. + Set `ASSESS_SLUG` and `ASSESS_DIR = .specify/assessments/`. ## Prerequisites diff --git a/extensions/assess/commands/speckit.assess.shape.md b/extensions/assess/commands/speckit.assess.shape.md index 1177fff9a..b14c0c6d0 100644 --- a/extensions/assess/commands/speckit.assess.shape.md +++ b/extensions/assess/commands/speckit.assess.shape.md @@ -14,7 +14,7 @@ Shape **outlines options at the boundaries; it does not produce a spec or a plan $ARGUMENTS ``` -Resolve the slug: explicit `slug=…` → conversation context (a slug reported earlier this session, confirmed by an existing `.specify/assessments//` directory) → ask (interactive) → single existing directory (automated) → otherwise stop and ask. Set `ASSESS_SLUG` and `ASSESS_DIR = .specify/assessments/`. +Resolve the slug: explicit `slug=…` → conversation context (a slug reported earlier this session, confirmed by an existing `.specify/assessments//` directory) → ask (interactive) → single existing directory (automated) → otherwise stop and ask. **Slug safety**: normalize any explicit or user-supplied slug — lowercase; whitespace/underscores → `-`; keep only `[a-z0-9-]` (drop every other character, including `.`, `/`, `\`); collapse and trim `-`; reject an empty normalized result. Only then set `ASSESS_SLUG` (the normalized value) and `ASSESS_DIR = .specify/assessments/` — this keeps every read and write inside `.specify/assessments/`. ## Prerequisites diff --git a/extensions/assess/extension.yml b/extensions/assess/extension.yml index 897d52998..9161b268f 100644 --- a/extensions/assess/extension.yml +++ b/extensions/assess/extension.yml @@ -4,7 +4,7 @@ extension: id: assess name: "Idea Assessment Pipeline" version: "1.0.0" - description: "Assess an idea before Spec-Driven Development via intake, research, define, shape, and decide, then hand a build/kill call to /speckit.specify. Assessments live under .specify/assessments//" + description: "Assess an idea before Spec-Driven Development via intake, research, define, shape, and decide. A go verdict hands off to /speckit.specify; a kill closes it. Lives under .specify/assessments//" category: "process" effect: "read-write" author: spec-kit-core diff --git a/extensions/catalog.json b/extensions/catalog.json index e4ee209ce..d05c48e0e 100644 --- a/extensions/catalog.json +++ b/extensions/catalog.json @@ -21,7 +21,7 @@ "name": "Idea Assessment Pipeline", "id": "assess", "version": "1.0.0", - "description": "Assess an idea before Spec-Driven Development via intake, research, define, shape, and decide, then hand a build/kill call to /speckit.specify. Assessments live under .specify/assessments//", + "description": "Assess an idea before Spec-Driven Development via intake, research, define, shape, and decide. A go verdict hands off to /speckit.specify; a kill closes it. Lives under .specify/assessments//", "author": "spec-kit-core", "repository": "https://github.com/github/spec-kit", "bundled": true,