Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
19b680a
AM-1321 Add context-economy as a single-copy reference and cut the sp…
bretttully Aug 6, 2026
f22919b
AM-1321 - Refuse an oversized tracker body before the write
bretttully Aug 6, 2026
4c1d6f0
AM-1321 - Trim cross-part restatement and close a truncated quote
bretttully Aug 6, 2026
85348f6
AM-1321 - Correct two sourcing defects in the shipped documentation
bretttully Aug 6, 2026
08514d5
AM-1321 - Fix a citation mis-mapping in the Jira body-limit provenance
bretttully Aug 6, 2026
67a03c9
AM-1321 - Re-attribute the Jira body-limit citation to a Cloud-scoped…
bretttully Aug 6, 2026
91d9dfe
AM-1321 - Fix a residual citation mis-binding and a voicing overclaim
bretttully Aug 6, 2026
7fe13ef
AM-1321 - Rewrap the body_limit docstring paragraph
bretttully Aug 6, 2026
8ad2340
AM-1321 - Bind each body_limit constant to its ADAPTER.md figure
bretttully Aug 6, 2026
3bd2cc4
AM-1321 - Bind the body_limit Protocol docstring figures to the const…
bretttully Aug 6, 2026
0da3fa1
Merge remote-tracking branch 'origin/main' into AM-1321/context-economy
bretttully Aug 6, 2026
22f6386
AM-1321 - Bump the plugin version past main and close three body_limi…
bretttully Aug 7, 2026
8142ef2
AM-1321 - Merge main and re-point the guard test at the collapsed com…
bretttully Aug 7, 2026
d301a9e
AM-1321 - Make the body_limit Protocol docstring tracker-neutral and …
bretttully Aug 7, 2026
60e58ac
AM-1321 - Correct a false docstring claim and unblind the figure extr…
bretttully Aug 7, 2026
54cfe5c
AM-1321 - Record why the figure extractor's boundaries are asymmetric
bretttully Aug 7, 2026
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
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "sy",
"displayName": "Shipyard",
"version": "1.20.0",
"version": "1.21.0",
"description": "Disciplined plan -> spec -> ship workflow for Claude Code: adversarial review against pinned commits, briefs-not-transcripts context hygiene, and a full paper trail on a pluggable issue tracker (Jira or GitHub Projects).",
"author": {
"name": "Brett Tully"
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Contributions are small, verifiable, and keep the tracker seam clean. This guide
4. Load the plugin locally to try it: `claude --plugin-dir /path/to/shipyard`.
5. Commands are namespaced by the plugin name (`sy`): `/sy:plan`, `/sy:spec`, `/sy:ship`, `/sy:spike`, `/sy:pr`, `/sy:ci`, `/sy:explain`, `/sy:help`, `/sy:init-repo`, `/sy:config`.

Keep prose (READMEs, roadmaps, docs) clear and unwrapped; keep machine-facing text (agent briefs, contracts, JSON logs) terse and structured.
Keep prose (READMEs, roadmaps, docs) clear and unwrapped; keep machine-facing text (agent briefs, contracts, JSON logs) terse and structured. How hard to cut either, and the two tests to cut by, is `skills/shared/references/context-economy.md`.

## Comments and docstrings

Expand Down
2 changes: 1 addition & 1 deletion docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Re-enter later with `/sy:plan <epic>` to read what shipped since the last checkp

`<task>` is an issue ID — a Jira key like `PROJ-123` or a GitHub issue like `#123`. `/sy:spec` reads the ticket and the code, resolves the repo's engineering standards, pulls representative data when shape matters, and asks you only when research cannot settle a decision. It writes a complete plan — the approach, the strongest rejected alternative (pressure-tested by the same debate, which runs on every plan before sign-off), ordered changes with file anchors, tests, acceptance criteria, a verification obligation for every activated risk lens, and the docs, visual, and pre-gate-checkpoint obligations the change makes necessary — then has a separate `sy:spec-gate` reviewer read that drafted plan for architecture, simplicity, correctness, and those last three fields before you ever see it.

You approve the plan before anything is built, and what you are asked to approve is a short prose summary of the judgment calls rather than the full file inventory. On approval the mechanical plan lands on the ticket as the single ACTIVE execution plan, stamped with the commit it was planned against. That same prose summary is posted as a comment on the ticket — the body is left untouched, so a reporter's repro steps or a PM's acceptance notes are never a target of the write. The task moves to `ready`, and the plan ends with a `/sy:ship` kickoff and a ship profile that names each phase's model explicitly (`START <model> / BUILD <model> / GATE <model> / effort <tier> / process <full|light>`), so `/sy:ship` passes each phase's model through as an actual override instead of inferring one.
You approve the plan before anything is built, and what you are asked to approve is a short prose summary of the judgment calls rather than the full file inventory. On approval the mechanical plan lands on the ticket as the single ACTIVE execution plan, stamped with the commit it was planned against. That plan comment is the only thing written — the ticket body is left untouched, so a reporter's repro steps or a PM's acceptance notes are never a target of the write, and the prose summary you just approved is not posted back as a second comment restating it. The task moves to `ready`, and the plan ends with a `/sy:ship` kickoff and a ship profile that names each phase's model explicitly (`START <model> / BUILD <model> / GATE <model> / effort <tier> / process <full|light>`), so `/sy:ship` passes each phase's model through as an actual override instead of inferring one.

Not every spec ends in a plan. When research shows the premise is already delivered, invalidated, or superseded, spec shelves the task instead: it posts the decisive evidence as a comment and closes the task rather than producing a plan for work that should not ship.

Expand Down
113 changes: 113 additions & 0 deletions scripts/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@
# Every agent must be able to ask whether a credential is present without ever reading its value;
# `sy_tools/guards/secret_guard.py` names this tool as the remedy it steers shell probes toward.
CHECK_ENV_TOOL = "check_env"
# The lowest number an adapter's `body_limit` could plausibly be. Both real limits are 32k and 64k, and
# no tracker documents anything near this floor, so a declaration under it is a typo, not a limit.
BODY_LIMIT_FLOOR = 8_192
_SCRATCH_HINT = "the `sy` server's `scratch_dir` tool"
_SCRATCH_REF_SUFFIXES = {".md", ".py", ".sh", ".json", ".yml", ".yaml", ".toml"}
_SCRATCH_REF_PATTERN = re.compile(r"(?<![\w.-])\.scratch\b")
Expand Down Expand Up @@ -132,6 +135,7 @@
"skills/shared/references/model-dispatch.md",
"skills/shared/references/config-values.md",
"skills/shared/references/transcript-attach.md",
"skills/shared/references/context-economy.md",
"skills/plan/references/new-objective.md",
"skills/plan/references/reentry.md",
"skills/plan/references/roadmap-shaping.md",
Expand Down Expand Up @@ -561,6 +565,31 @@ def check_invariants(errors: list[str]) -> None:
def read(rel: str) -> str:
return (ROOT / rel).read_text(encoding="utf-8")

# Read out of the source rather than imported: `python scripts/validate.py` puts `scripts/` on
# `sys.path`, not the repo root, so importing the adapters needs a `sys.path` shim this file has
# never needed for anything else. The comment block directly above the declaration comes back with
# it, because that block is a copy of the figure too and the same anchor already locates it.
# `None`, not a sentinel figure: a `0` here fell through into the floor leg and reported a second,
# fabricated fault ("this is a dropped digit") about a declaration that was never read.
def declared_body_limit(rel: str) -> tuple[int, str] | None:
match = re.search(r"((?:^ *#.*\n)*)^ {4}body_limit: int = ([\d_]+)$", read(rel), re.MULTILINE)
if match is None:
fail(f"{rel} must declare `body_limit: int = <literal>`; its ADAPTER.md figure cannot be checked", errors)
return None
return int(match.group(2)), match.group(1)

# Every grouped or ungrouped spelling of a four-digit-or-longer number in `target`. Used instead of
# substring containment, which a target satisfies for the wrong reason whenever the declared digits
# happen to appear inside some unrelated number in its prose.
#
# The boundaries are deliberately asymmetric: the lookbehind rejects a leading hyphen so a citation
# id cannot be read as a figure, but the lookahead rejects a trailing hyphen only when a digit
# follows it. Making them symmetric loses `32,767-character`, which is how prose actually writes the
# figure, and a target stating only that reads as figure-free — so the staleness leg passes over
# stale provenance and the whole check goes quietly vacuous.
def numeric_tokens(target: str) -> set[str]:
return set(re.findall(r"(?<![\w-])\d[\d,_]{3,}(?!\w|-\d)", target))

ship = read("skills/ship/SKILL.md")
handoff = read("skills/ship/references/handoff-accounting.md")
merge = read("skills/ship/references/merge-accounting.md")
Expand All @@ -582,6 +611,8 @@ def read(rel: str) -> str:
preflight_ref = read("skills/shared/references/preflight.md")
debate_ref = read("skills/shared/references/debate.md")
spec_gate_ref = read("skills/shared/references/spec-gate.md")
economy_ref = read("skills/shared/references/context-economy.md")
contributing = read("CONTRIBUTING.md")
roadmap_shaping = read("skills/plan/references/roadmap-shaping.md")
tracker_skill = read("skills/tracker/SKILL.md")
jira_adapter = read("skills/tracker/jira/ADAPTER.md")
Expand Down Expand Up @@ -812,6 +843,88 @@ def read(rel: str) -> str:
fail("spec §7's Step 2 procedure must state it never writes the Task body", errors)
if "Step 2 — after approval" not in spec:
fail("spec must keep the staged reveal: full plan posted only in Step 2, after approval", errors)
# §7 Step 2's summary comment was removed: it restated, on the ticket, a summary the user had just
# read and approved. Both spellings are pinned because the instruction lived in two places — Step 2's
# numbered procedure and Step 1's auto-mode consent sentence — and a consent sentence still naming a
# write the run no longer performs states a false authorization.
if "post the Step-1 summary" in spec_s7:
fail("spec §7 must not post the Step-1 summary back as a second comment restating the plan", errors)
if "post this summary as a comment on the Task" in spec:
fail("spec's sign-off consent sentence names a summary-comment write §7 Step 2 no longer performs", errors)

# Context economy is a single copy: the two cut tests are phrased once, in the reference, and every
# authoring surface carries a pointer to it. A consumer that spells a cut test out has forked the rule.
cut_tests = (
"Does removing this sentence change what its reader does?",
"Would a pointer do the work this text is doing?",
)
for cut_test in cut_tests:
if cut_test not in economy_ref:
fail(f"context-economy.md must state the cut test {cut_test!r} verbatim", errors)
economy_consumers = (
("spec", spec),
("plan", plan),
("pr", pr),
("handoff-accounting", handoff),
("CONTRIBUTING.md", contributing),
)
for name, text in economy_consumers:
if "context-economy.md" not in text:
fail(f"{name} authors an agent-facing artifact and must cite context-economy.md", errors)
for cut_test in cut_tests:
if cut_test in text:
fail(f"{name} restates a context-economy cut test; cite context-economy.md instead", errors)
economy_axis_phrase = "narrates what a cited anchor already shows"
if economy_axis_phrase not in spec_gate_ref:
fail(f"spec-gate reference's Simplicity axis must state the prose trigger {economy_axis_phrase!r}", errors)
if economy_axis_phrase in spec_gate:
fail("spec-gate agent restates the prose-economy trigger; cite spec-gate.md instead of copying it", errors)
if economy_axis_phrase in spec:
fail("spec restates the prose-economy trigger; cite spec-gate.md instead of copying it", errors)

# Each adapter's body limit lives in the constant, again in the comment above it carrying that
# figure's provenance, and again in its agent-facing ADAPTER.md prose. The Protocol docstring is no
# longer a target: it states the contract and names no figure, because a core module may not name a
# concrete tracker (CONTRIBUTING.md) and a figure is worthless without its tracker-specific
# provenance. Both spellings count everywhere, because the prose groups thousands and the code does not.
for name, source, doc_rel, doc in (
("jira", "sy_tools/tracker/jira/adapter.py", "skills/tracker/jira/ADAPTER.md", jira_adapter),
("github", "sy_tools/tracker/github/adapter.py", "skills/tracker/github/ADAPTER.md", github_adapter),
):
declared = declared_body_limit(source)
if declared is None:
continue
limit, note = declared
spellings = {str(limit), f"{limit:,}", f"{limit:_}"}
# A dropped leading digit used to survive substring containment by colliding with the grouped
# spelling it came from — `2_767` occurs inside every target stating `32,767` — cutting the limit
# tenfold with every doc stale. Whole-token matching closes that, and a floor closes the rest of
# the class without parsing prose: no tracker limit is anywhere near this low.
if limit < BODY_LIMIT_FLOOR:
fail(
f"{name} adapter's body_limit is {limit} ({source}), under the {BODY_LIMIT_FLOOR} floor; no "
"tracker limit is that low, so this is a dropped digit, not a limit",
errors,
)
if not spellings & numeric_tokens(doc):
fail(f"{name} adapter's body_limit is {limit} ({source}); {doc_rel} states no such figure", errors)
# The comment above the declaration is the copy the doc leg never reads, so it is checked for two
# faults. Detachment: the anchor only reaches a block sitting immediately above the declaration, so
# one blank line between them emptied `note` and left this leg passing on a stale figure. Staleness:
# every figure the block does state is the declared one.
if not note.strip():
fail(
f"{name} adapter's body_limit is {limit} ({source}) with no comment directly above it; the "
"provenance comment must sit on the lines immediately preceding the declaration",
errors,
)
stale = sorted(numeric_tokens(note) - spellings)
if stale:
fail(
f"{name} adapter's body_limit is {limit} ({source}); the comment above the declaration "
f"still states {stale[0]}",
errors,
)

# `post-comment` takes `human` and `agent_detail`, both required, and assembles the boundary itself.
# These are the highest-traffic call sites, so a reference still describing one hand-composed body
Expand Down
2 changes: 1 addition & 1 deletion skills/plan/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Read durable cross-session memory early — `memory_list` (or `memory_search` on

Seed every agent prompt with known anchors — paths, symbols, entry points, keys — and name ground already covered; agents must not rediscover what the caller knows.

Machine-facing agent briefs stay pointer-dense. Human-facing Epic maps and decision logs remain clear prose.
Machine-facing agent briefs stay pointer-dense. Human-facing Epic maps and decision logs remain clear prose. Both, and the roadmap and `# SEAMS` comments this skill posts, are written under `${CLAUDE_PLUGIN_ROOT}/skills/shared/references/context-economy.md`.

## State router

Expand Down
2 changes: 2 additions & 0 deletions skills/pr/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ Base on real diff/log. The mutable description is the PR's human-attention secti

**Acceptance criteria/evidence never live only in the mutable description.** `/sy:ship` posts them as a dedicated PR comment so promotion/refresh cannot erase them.

What belongs in which of those two, and how hard to cut each, is `${CLAUDE_PLUGIN_ROOT}/skills/shared/references/context-economy.md`.

Title: `<TICKET> - <imperative summary>` when branch carries a ticket key.

## 3. Review threads
Expand Down
30 changes: 30 additions & 0 deletions skills/shared/references/context-economy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Context economy

Every agent-facing artifact Shipyard writes is loaded into a finite budget and read by a model that gets worse at using it the fuller it gets. Anthropic's guidance names the resource directly — "LLMs have an 'attention budget' that they draw on when parsing large volumes of context" — and sets the target as finding "the smallest possible set of high-signal tokens that maximize the likelihood of some desired outcome" (https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents). This file is the single copy of that rule for Shipyard's own artifacts: skills, agent briefs, plans, roadmaps, PR descriptions, state briefs, handoff records. Cite it from wherever an artifact gets authored; never restate it there.

The cost is measured, not stylistic. Chroma's *Context Rot* evaluated 18 LLMs across 194,480 calls and found that "even under these minimal conditions, model performance degrades as input length increases…" — on tasks trivial enough that length was the only variable (https://www.trychroma.com/research/context-rot). Position compounds volume: performance "is often highest when relevant information occurs at the beginning or end of the input context, and significantly degrades when models must access relevant information in the middle of long contexts, even for explicitly long-context models" (Liu et al., https://arxiv.org/abs/2307.03172). And instructions dilute each other — IFScale reports that "even the best frontier models only achieve 68% accuracy at the max density of 500 instructions", with later instructions dropped more often than earlier ones (https://arxiv.org/abs/2507.11538). A sentence added to a brief is not free: it competes with every other sentence there, and it pushes something else toward the middle.

## The two cut tests

Apply both to every paragraph before it ships:

1. **Does removing this sentence change what its reader does?** If nothing downstream changes, it is commentary, and it goes.
2. **Would a pointer do the work this text is doing?** If the content already lives somewhere the reader can reach, cite that instead of copying it.

## Write to the actor

A settled decision is stated, not re-argued. Once the choice is made, the artifact tells its reader what to do; the reasoning that produced it belongs to the human-facing half of the record, or nowhere. Rationale in machine-facing text is the most common form of dilution here precisely because it reads as thoroughness — but an implementer cannot act on *why*, and every line of it displaces a line they could have acted on.

## No cross-part restatement

An artifact with two labeled parts — a human half and an agent half, a sign-off summary and a mechanical plan — carries each fact in exactly one of them. Repetition across that boundary is not redundancy for safety; it is two copies that drift, and a reader of either half cannot tell which one is current. Cross-reference across the boundary instead of copying over it.

## Evidence is not instruction

Forensic detail — the trace that established a fact, the counts, the hypotheses ruled out — is evidence for a claim, not an instruction to anybody. It earns a place inline only where a reader acts on it; otherwise it belongs in a companion record that nothing depends on.

In Shipyard that has a hard edge. A pointer to another tracker comment resolves to nothing for every phase after START, which is the only phase that reads the ticket. A fact a later phase needs is carried in that phase's own brief or it is not available at all — so "it's in the investigation comment" is not a way to keep detail without paying for it. Either the detail earns its place inline, or it goes in a companion comment the plan does not depend on.

## The instance already in the tree

Shipyard applies this to itself: the `## Return contract — target ≤N tokens` block in every `agents/*.md` is this principle made enforceable at the one point where an agent's output enters someone else's budget.
Loading
Loading