Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion plugins/planning/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -30,6 +30,10 @@
"plan",
"stress-test",
"implementation-plan",
"draft-goal-condition",
"goal",
"completion-condition",
"autonomous-goal",
"skill"
]
}
18 changes: 18 additions & 0 deletions plugins/planning/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions plugins/planning/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
119 changes: 119 additions & 0 deletions plugins/planning/scripts/goal-condition-length.sh
Original file line number Diff line number Diff line change
@@ -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 <N> [--file <path>] # 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=<n> limit=<N> status=<ok|over>`

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 <N> 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 $/; <STDIN> }; 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:-<empty>}')" >&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
103 changes: 103 additions & 0 deletions plugins/planning/scripts/goal-condition-length.test.sh
Original file line number Diff line number Diff line change
@@ -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 <label> <want_code> <input> <args...>
expect_exit() {
local label="$1" want="$2" input="$3"
shift 3
local got
printf '%s' "$input" | bash "$SUT" "$@" >/dev/null 2>&1
got=$?
if [[ "$got" -eq "$want" ]]; then pass "$label"; else fail "$label (want exit $want, got $got)"; fi
}

# 1. --help exits 0.
if bash "$SUT" --help >/dev/null 2>&1; then pass "--help exits 0"; else fail "--help should exit 0"; fi

# 2. Under limit -> exit 0.
expect_exit "under limit -> 0" 0 "hello" --limit 50

# 3. Exactly at limit -> exit 0 (inclusive boundary: 'up to N characters').
expect_exit "at limit (boundary) -> 0" 0 "abcde" --limit 5

# 4. Over limit -> exit 1.
expect_exit "over limit -> 1" 1 "abcdef" --limit 5

# 5. Missing --limit -> exit 2.
expect_exit "missing --limit -> 2" 2 "hello"

# 6. Non-integer limit -> exit 2.
expect_exit "non-integer limit -> 2" 2 "hello" --limit abc

# 7. Zero limit -> exit 2.
expect_exit "zero limit -> 2" 2 "hello" --limit 0

# 8. Empty condition -> exit 2.
expect_exit "empty condition -> 2" 2 "" --limit 50

# 9. Trailing newline is stripped (5 chars, not 6) -> at-limit passes.
expect_exit "trailing newline stripped -> 0" 0 $'abcde\n' --limit 5

# 10. Reads condition from --file.
printf 'abcdef' >"$TMP/cond.txt"
bash "$SUT" --limit 5 --file "$TMP/cond.txt" >/dev/null 2>&1
rc=$?
if [[ $rc -eq 1 ]]; then pass "--file over limit -> 1"; else fail "--file over limit -> wrong code ($rc)"; fi

# 11. Missing --file target -> exit 2.
bash "$SUT" --limit 5 --file "$TMP/nope.txt" >/dev/null 2>&1
rc=$?
if [[ $rc -eq 2 ]]; then pass "missing --file -> 2"; else fail "missing --file -> wrong code ($rc)"; fi

# 12. stdout is greppable and reports the count and status.
out="$(printf 'abcdef' | bash "$SUT" --limit 5 2>/dev/null)"
if [[ "$out" == "chars=6 limit=5 status=over" ]]; then pass "stdout reports chars/limit/status"; else fail "stdout wrong: '$out'"; fi

# 13. Multibyte char counts as one code point, not its byte length.
# Skipped (optional-tool SKIP convention) when perl is absent and `wc -m`
# would fall back to a byte count in a non-UTF-8 locale.
if command -v perl >/dev/null 2>&1; then
# 'héllo' = 5 code points; passes a limit of 5.
expect_exit "multibyte counts as 1 code point -> 0" 0 $'h\xc3\xa9llo' --limit 5
else
printf 'SKIP - multibyte code-point count (perl absent)\n'
fi

# 14. Counter failure is caught, not silently passed. Shadow BOTH counting
# backends (perl and wc) with stubs that emit nothing, so whichever branch
# the script takes yields an empty count. The guard must then exit 2 rather
# than fall through to a false status=ok on a crashed counter.
fake_bin="$TMP/fakebin"
mkdir -p "$fake_bin"
for stub in perl wc; do
printf '#!/usr/bin/env bash\ncat >/dev/null 2>&1\nexit 0\n' >"$fake_bin/$stub"
chmod +x "$fake_bin/$stub"
done
rc=0
printf 'hello' | PATH="$fake_bin:$PATH" bash "$SUT" --limit 50 >/dev/null 2>&1 || rc=$?
if [[ $rc -eq 2 ]]; then pass "counter failure -> 2 (no false pass)"; else fail "counter failure -> wrong code ($rc)"; fi

if [[ "$fails" -ne 0 ]]; then
printf '\n%d test(s) failed.\n' "$fails" >&2
exit 1
fi
printf '\nAll goal-condition-length.sh tests passed.\n'
83 changes: 83 additions & 0 deletions plugins/planning/skills/draft-goal-condition/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
---
name: draft-goal-condition
description: "Craft a paste-ready /goal completion condition from a stated intent — the autonomous-goal / keep-working-toward-a-goal field Claude Code evaluates after every turn. Reads the current official /goal docs live for the condition shape and character limit (never hardcodes them), drafts a transcript-demonstrable condition (measurable end state + stated check + constraints + optional turn/time bound), and proves it fits the limit with a deterministic character counter instead of model guesswork. Use for 'craft a /goal', 'write a goal condition', 'set up an autonomous goal', 'make Claude keep working until X', 'my /goal is too long / over the limit', 'turn this into a completion condition'; skip and route elsewhere when the work is interval-shaped (/loop), cloud/sessionless (routines, /schedule), or a one-shot prompt."
argument-hint: "[intent]"
user-invocable: true
disable-model-invocation: false
---

## Variables

Arguments: `$ARGUMENTS` — the natural-language intent for the autonomous run, plus any constraints or bounds the user volunteers.

## Purpose

Turn a stated intent into a paste-ready `/goal <condition>` string that conforms to the shape the official Claude Code docs prescribe and provably fits the documented character limit. A language model cannot reliably count characters, so length conformance is decided by a deterministic counter, not by estimation.

The `/goal` contract — its condition shape and its character limit — can change between Claude Code versions. This skill therefore reads the **current** official docs at authoring time and never bakes those values into its own text. The number you see in a doc today is not a constant to memorize.

## Step 0 — Lever fit (is `/goal` even the right tool?)

`/goal` starts the next turn when the previous one finishes and stops when a fresh evaluator model confirms a completion condition holds. Before authoring, confirm that fits the intent. If it does not, route instead of drafting:

- **Interval-driven** ("every 5 minutes", "poll until") → `/loop` (a time interval starts each turn), not `/goal`.
- **Cloud / sessionless / scheduled** ("nightly", "each morning", runs with no session open) → routines / `/schedule`.
- **Custom per-turn logic across all sessions** (deterministic script check, settings-scoped) → a prompt-based Stop hook.
- **One-shot** (a single prompt with no across-turn continuation) → just prompt; no goal.

Confirm the current comparison semantics against the live docs (below) rather than this summary — the routing table can drift. Only proceed when the intent genuinely wants "keep working until this condition is met."

## Step 1 — Read the live contract

Fetch the current official `/goal` documentation and extract, from the page itself:

1. the **effective-condition shape** it prescribes, and
2. the **maximum character limit** for a condition.

Primary source: `https://code.claude.com/docs/en/goal`. Cross-check the scheduling comparison via the pages that doc links (`/en/scheduled-tasks`, routines) if Step 0 routing is in question.

**Doc-fetch failure is not silent and never guessed.** If the page cannot be fetched or its structure has shifted so the limit or shape cannot be located, stop and tell the user exactly that, citing the URL. Do not fall back to a remembered number or shape — a stale limit or condition shape baked in here is precisely the drift this skill exists to avoid. Offer the user two ways forward: paste the current condition shape and character limit from that page — the shape drives the Step 2 draft, the limit drives the Step 3 counter — or defer until the docs are reachable. Never finalize a draft on a shape or limit that was not sourced live.

## Step 2 — Draft the condition

The evaluator judges the condition against **what Claude has already surfaced in the transcript** — it does not run commands or read files. Draft accordingly: every claim in the condition must be something Claude's own output can demonstrate.

Structure the draft to the doc-sourced shape. As of the contract this skill targets, that is:
Comment thread
kyle-sexton marked this conversation as resolved.

- **One measurable end state** — a test result, a build exit code, a file count, an empty queue.
- **A stated check** — how Claude proves it (e.g. "`npm test` exits 0", "`git status` is clean").
- **Constraints that must not change** on the way there (e.g. "no other test file is modified").
- **An optional turn/time bound** — e.g. "or stop after 20 turns" — to cap runaway loops.

Avoid conditions the transcript cannot show (subjective quality, external state Claude never surfaces).

## Step 3 — Mechanical length check

Validate the draft's character count against the **live limit from Step 1** with the deterministic counter (no model estimation). Write the draft to a temp file and pass `--file` — this is the robust path, immune to a condition that contains a single quote, backtick, or `$` that would otherwise mangle a piped string:

```shell
bash "${CLAUDE_PLUGIN_ROOT}/scripts/goal-condition-length.sh" --limit <LIMIT_FROM_STEP_1> --file <path-to-draft>
```

For a simple condition with no shell-special characters, stdin also works: `printf '%s' "<condition>" | bash "${CLAUDE_PLUGIN_ROOT}/scripts/goal-condition-length.sh" --limit <LIMIT_FROM_STEP_1>`.

Exit `0` = within limit, `1` = over, `2` = usage/env error (including a counter that failed to produce a number); stdout reports `chars=<n> limit=<N> status=<ok|over>`.

**On `status=over`:** tighten and re-run until it passes — shorten prose, fold the stated check into the end state, drop redundant constraints — **without dropping the measurable end state, the stated check, or the load-bearing constraints**. If the intent genuinely cannot compress into one provable condition under the limit, say so rather than silently shedding a constraint; splitting a goal into sequential per-phase goals is not documented doctrine and is out of scope here.

## Step 4 — Output

Emit the final, counter-passed condition as a paste-ready invocation:

```text
/goal <condition>
```

Note for the user: `/goal` holds for the current session only. A goal survives `--resume` / `--continue` (though its turn count, timer, and token baseline reset), but running `/clear` removes it — so the goal must be re-set after any `/clear`.

## Gotchas

- **The limit is characters, not tokens.** The counter counts Unicode code points; do not substitute a token estimate.
- **Never hardcode the limit or the shape** into a draft, this file, or the script. They are read live each run; that is the whole point.
- **Over-limit submission behavior is undocumented** — there is no stated truncation or rejection semantics, so the pre-submission counter is the only guard. Do not assume the app will trim for you.
- **A goal does not change permissions.** If the stated check runs a command, the user still gets asked unless auto mode or their settings already allow it — worth flagging when the check is a shell command.
Loading