diff --git a/plugins/planning/.claude-plugin/plugin.json b/plugins/planning/.claude-plugin/plugin.json index ec6c058f2..b24550907 100644 --- a/plugins/planning/.claude-plugin/plugin.json +++ b/plugins/planning/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "planning", - "version": "0.21.2", + "version": "0.22.0", "userConfig": { "use_ask_user_question": { "type": "boolean", @@ -30,6 +30,10 @@ "plan", "stress-test", "implementation-plan", + "draft-goal-condition", + "goal", + "completion-condition", + "autonomous-goal", "skill" ] } diff --git a/plugins/planning/CHANGELOG.md b/plugins/planning/CHANGELOG.md index 1c46c2a3b..0753be144 100644 --- a/plugins/planning/CHANGELOG.md +++ b/plugins/planning/CHANGELOG.md @@ -3,6 +3,24 @@ All notable changes to the `planning` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.22.0] + +### Added + +- **New skill `draft-goal-condition`** — crafts a paste-ready `/goal` completion + condition from a stated intent. It reads the **current** official `/goal` docs + live for the condition shape and character limit (nothing is hardcoded, so the + skill does not rot when the documented contract changes between Claude Code + versions), gates the draft to the doc's transcript-demonstrable effective-condition + shape, and — because a model cannot reliably count characters — proves the draft + fits the limit with a deterministic counter rather than estimation. Includes a + lever-fit gate (step 0) that routes interval-shaped work to `/loop` and + cloud/sessionless work to routines/`/schedule` instead of authoring a goal. +- **New plugin-root script `scripts/goal-condition-length.sh`** (with companion + `goal-condition-length.test.sh`) — a mechanical, model-free character-length + gate. The limit is passed in by the caller (read live from the docs), never + baked into the script; exit `0` within limit, `1` over, `2` usage/env error. + ## [0.21.2] ### Added diff --git a/plugins/planning/README.md b/plugins/planning/README.md index 9f2299d2c..7bdc17bfd 100644 --- a/plugins/planning/README.md +++ b/plugins/planning/README.md @@ -14,6 +14,7 @@ where artifacts land in the consuming repo. | `/planning:prd` | Product intent | Produces a Product Requirements Document (problem, users, success metrics) in three tiers — one-pager, consumer-feature, B2B-internal — with a synthesize path and a review mode. | | `/planning:interview` | Engineering contract | Locks a task contract (goal, constraints, acceptance criteria, named assumptions) into a PLAN.md Brief — synthesizing when intent is clear, running frontier-rounds Q&A when it isn't, or interviewing relentlessly on request. | | `/planning:questionnaire` | Person hand-off | Turns a decision another person holds into a discovery questionnaire delivered async — interviews the user about the send only (recipient, what's needed back), writes the document to the topic's memory slice, and leaves delivery out-of-band. | +| `/planning:draft-goal-condition` | Goal authoring | Crafts a paste-ready `/goal` completion condition from a stated intent — reads the current official `/goal` docs live for the condition shape and character limit (nothing hardcoded), drafts a transcript-demonstrable condition, and proves it fits the limit with a deterministic character counter instead of model guesswork; a lever-fit gate routes interval-shaped or cloud/sessionless work elsewhere. Standalone. | | `/planning:design` | Design space | Explores types, contracts, module boundaries, and package topology through collaborative discussion rounds, producing capability-matrix / type-inventory / design-threads / topology artifacts; its `handoff` action delegates to `/planning:design-handoff`. | | `/planning:design-handoff` | Design→plan gate | Gates a finished design for `/planning:plan` — a binary check that every `design-threads.md` thread is RESOLVED, directional, or TAGGED-DEFERRED — then packages the plan-ready summary and resume prompt, or FAILs and routes back to `/planning:design`. | | `/planning:devils-advocate` | Adversarial review | Stress-tests plans via assumption extraction, evidence checks, failure scenarios, and operational-gotcha sweeps — every finding evidence-backed, never generic warnings. | diff --git a/plugins/planning/scripts/goal-condition-length.sh b/plugins/planning/scripts/goal-condition-length.sh new file mode 100755 index 000000000..54c2c17a7 --- /dev/null +++ b/plugins/planning/scripts/goal-condition-length.sh @@ -0,0 +1,119 @@ +#!/usr/bin/env bash +# Mechanical character-length gate for a drafted /goal completion condition. +# +# A language model cannot reliably count characters, so conformance to the +# /goal condition character limit is decided here — deterministically, with NO +# model involvement. The limit is NOT baked in: the caller passes the value it +# read from the current official docs at authoring time (--limit), so this gate +# never rots when the documented limit changes between Claude Code versions. +# +# Exit 0 = condition length is within the limit (count <= limit) +# Exit 1 = condition exceeds the limit (count > limit) +# Exit 2 = usage or environment error (bad/missing --limit, empty condition, ...) +# +# Usage: +# bash goal-condition-length.sh --limit [--file ] # else reads stdin +# bash goal-condition-length.sh --help +# +# Counting: Unicode code points ("characters"), locale-independent via perl when +# present, falling back to `wc -m`. A trailing newline (as a file editor adds) +# is not part of the pasted condition and is stripped before counting. +# +# Output (stdout, greppable): `chars= limit= status=` + +set -uo pipefail + +usage() { + # Sentinel range (not fixed line numbers) so the printed usage never silently + # truncates when the header grows or shrinks on a future edit. + sed -n '/^# Mechanical/,/^# Output/p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//' +} + +limit="" +file="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --help | -h) + usage + exit 0 + ;; + --limit) + limit="${2-}" + shift 2 || { + echo "error: --limit needs a value" >&2 + exit 2 + } + ;; + --limit=*) + limit="${1#*=}" + shift + ;; + --file) + file="${2-}" + shift 2 || { + echo "error: --file needs a value" >&2 + exit 2 + } + ;; + --file=*) + file="${1#*=}" + shift + ;; + *) + echo "error: unknown argument: $1" >&2 + exit 2 + ;; + esac +done + +# The limit is supplied by the caller (read live from the official docs); this +# gate deliberately has no default so a stale number can never be baked in. +if [[ -z "$limit" ]]; then + echo "error: --limit is required (pass the current limit read from the official /goal docs)" >&2 + exit 2 +fi +if ! [[ "$limit" =~ ^[0-9]+$ ]] || [[ "$limit" -eq 0 ]]; then + echo "error: --limit must be a positive integer, got: $limit" >&2 + exit 2 +fi + +# Read the condition. Command substitution strips trailing newlines, which is +# the desired normalization: a pasted /goal condition carries none. +if [[ -n "$file" ]]; then + if [[ ! -f "$file" ]]; then + echo "error: --file not found: $file" >&2 + exit 2 + fi + condition="$(cat -- "$file")" +else + condition="$(cat)" +fi + +if [[ -z "$condition" ]]; then + echo "error: empty condition (nothing to measure)" >&2 + exit 2 +fi + +# Count characters (Unicode code points), not bytes. +if command -v perl >/dev/null 2>&1; then + chars="$(printf '%s' "$condition" | perl -CSAD -e 'my $c = do { local $/; }; print length $c;')" +else + chars="$(printf '%s' "$condition" | wc -m | tr -d '[:space:]')" +fi + +# Without set -e, a crashed counter would leave $chars empty and the -gt test +# below would error-and-fall-through to a false "status=ok". Fail loudly instead: +# a length gate that silently passes when its own counter broke is worse than useless. +if ! [[ "$chars" =~ ^[0-9]+$ ]]; then + echo "error: character count failed (counter returned: '${chars:-}')" >&2 + exit 2 +fi + +if [[ "$chars" -gt "$limit" ]]; then + echo "chars=$chars limit=$limit status=over" + exit 1 +fi + +echo "chars=$chars limit=$limit status=ok" +exit 0 diff --git a/plugins/planning/scripts/goal-condition-length.test.sh b/plugins/planning/scripts/goal-condition-length.test.sh new file mode 100755 index 000000000..6e3d45a86 --- /dev/null +++ b/plugins/planning/scripts/goal-condition-length.test.sh @@ -0,0 +1,103 @@ +#!/usr/bin/env bash +# Black-box contract test for goal-condition-length.sh. +# +# Self-contained and cwd-independent; mutates only its own mktemp dir. Fixture +# limits are small, arbitrary numbers — never the documented /goal limit — so +# the tool and its test stay grep-clean of any baked-in doc value. +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SUT="$SCRIPT_DIR/goal-condition-length.sh" + +fails=0 +pass() { printf 'ok - %s\n' "$1"; } +fail() { + printf 'FAIL - %s\n' "$1" >&2 + fails=$((fails + 1)) +} + +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT + +# Assert an exit code from running the SUT with stdin `input` and given args. +# Usage: expect_exit