What you’ll leave with
A completed product brief, a URL policy with tests, and a reusable skill for the next ambiguous request.
Before you begin. You need a feature idea and a place to take notes. The optional code exercise uses Node.js 22 or later; it needs no packages, API keys, or running application.
“Let people save interesting links” sounds like a small feature. It is also enough room for two developers to build two different products. One creates browser bookmarks. Another adds accounts, extracts page titles, generates summaries, and synchronizes across devices. Both can honestly say they followed the request.
A useful brief closes that gap before it becomes code. It records the decisions that would otherwise be made quietly during implementation: what counts as saved, what happens on a second click, where the data lives, and what the reader sees when something fails. Its value comes from those decisions, not its length.
We will build that brief for a fictional reading-list feature. The example is deliberately modest so you can inspect the entire chain from an assumption to a test. You can apply the same method to a research assistant, a design tool, or a support workflow. The completed brief and executable URL exercise below are original teaching examples, not claims about measured customer behavior.
1. Start with the moment, not the feature list
Write down the moment that makes the feature useful. Our reader is in the middle of another task, encounters a promising article, and wants to return later. Their current workaround is an open tab. That tells us something an abstract requirement misses: saving must be quick, and returning must work after the browser page has been closed.
Now separate what you know from what you are assuming. We know the request asks for saved links. We are choosing a same-browser prototype. We do not know whether readers actually return to their lists. If you collapse these into one confident product statement, the team may mistake a delivery shortcut for a researched user need.
Ask questions that can change the first implementation. Does the link need to appear on another device? Is this a personal list or a shared workspace? May the application fetch the submitted page? A yes to any of these changes storage, ownership, or network behavior. The exact button radius does not change the initial contract. Resolve the former first.
| Statement | Type | What we do with it |
|---|---|---|
| Readers leave tabs open to return later | Scenario assumption | Validate with a reader; do not present as research |
| Save and return in one browser | Chosen boundary | Write into the brief and the interface |
| Everyone wants automatic summaries | Unverified expansion | Exclude from the first slice |
| The repository already has a URL parser | Implementation fact, if inspected | Reuse it after checking its behavior |
2. Choose one loop that can succeed on its own
The smallest useful loop is paste, save, leave, return, open. A form that adds a row but loses it on reload is a demonstration of rendering, not a complete version of this loop. Conversely, synchronization is unnecessary for a prototype whose promise explicitly says one browser. Scope by the user's outcome rather than by the number of components.
Make the non-goals equally concrete. We exclude accounts, AI summaries, tags, and metadata scraping. Excluding metadata scraping matters technically: the service will not make a network request to an arbitrary user-supplied address. The label is simply the saved URL. This is less polished than a rich preview, but it lets us evaluate saving without introducing a fetch service and its failure modes.
Choose a capacity limit and acknowledge its consequence. The example caps the list at 100 entries and defers deletion. That is acceptable for a disposable trial, but an awkward long-term product. Put the limit in the interface and make deletion a follow-up if readers reach it. A prototype becomes misleading when its constraints disappear from the brief.
- CaptureReader pastes a complete URL.
- DecideValidate and check the canonical duplicate key.
- PersistWrite browser storage before confirming success.
- ReturnReload the same profile and open the saved source.
3. Turn ambiguous words into rules
The word duplicate deserves a decision. Are a page and its #setup anchor the same saved item? What about two query strings? For our example, fragments are removed, while path and query are retained. This makes a second section of the same article a duplicate without accidentally merging a site's page=1 and page=2 views.
That policy has a tradeoff: on a site that uses fragments as navigation, two distinct views will collapse into one entry. We accept that limitation for this example. If your audience saves those applications, keep fragments instead and change both the brief and tests. There is no universal URL identity rule hiding in a library; there is a product choice that needs to be explicit.
Also define success at the storage boundary. A row appears as saved only after the write succeeds. If storage is full or unavailable, the old rows and pasted input remain. Otherwise the interface can announce success for data it will lose on reload. These small failure decisions are often more useful to an implementer than another page of aspirational requirements.
| Vague | Observable |
|---|---|
| Handle errors gracefully | If storage throws, retain the input and old rows; show a retry message |
| Prevent duplicate links | Remove the fragment, serialize the URL, and use that string as the duplicate key |
| Make it accessible | Complete save using a keyboard, retain visible focus, associate input errors, and announce status |
| Make saving fast | Keep this version local and record interaction latency during the trial; set a target from evidence |
4. Read the finished brief as the implementer
Here is the actual handoff, filled in. Notice the absence of placeholders such as “handle edge cases.” Every boundary changes either an implementation branch or a review action. The document is short enough to keep beside the code, but specific enough that a reviewer can disagree with a decision before work begins.
Read it once as a developer and once as a tester. The developer should be able to identify the data transformation and storage operation. The tester should be able to provoke a failure without asking what failure means. If either person must invent a rule, add the smallest missing decision rather than expanding the document indiscriminately.
# Reading list: one browser, one useful loop
Revision: 1 | Status: ready for a local prototype
## Problem and evidence
A reader discovers an article during work but cannot read it now.
They currently leave a browser tab open. Our assumption is that a
short saved list will help them return to the article. This is a
hypothesis; no user interview or retention result is claimed.
## Outcome
Save an HTTP(S) URL, see it in the list, and reopen it after reload
in the same browser profile. The saved address is the display label.
## Journey
Paste https://example.com/guide#setup. Choose Save link.
See https://example.com/guide and the confirmation “Link saved.”
Reload. The row is still present. Choose its link to open the source.
## Rules and boundaries
Reject inputs longer than 2,048 characters before trimming.
Trim surrounding whitespace. Parse using the platform URL parser.
Allow only http: and https:. Reject embedded username/password.
Remove the fragment. Keep path, query parameters and query order.
Use the resulting URL string as the duplicate key.
Saving a duplicate keeps one row and says “Already in your list.”
Storage: browser-local data, key reading-list-v1, maximum 100 rows.
Write storage successfully before displaying a new row as saved.
A failed write keeps the entered URL and existing list unchanged.
Do not fetch the submitted URL or infer its title in this iteration.
## States
Empty: explain what to paste; leave the form available.
Invalid: associate an HTTP(S) URL error with the input.
Saved: clear input, show row and a status message.
Duplicate: keep one row; explain the result without an error alert.
Storage unavailable: keep input; say “Could not save. Try again.”
Corrupt stored data: do not overwrite it; offer an explicit reset.
Full: keep 100 rows and explain that the prototype is full.
## Acceptance cases
A1 Given an empty list, when a valid URL is saved, then one row appears.
A2 Given A1, when the page reloads, then that same row remains.
A3 Given A1, when its URL plus #details is saved, then one row remains.
A4 Given any list, when javascript:alert(1) is entered, then nothing is stored.
A5 Given a failed storage write, when Save is used, then the input and old rows remain.
A6 Given corrupt stored data, when the page opens, then an actionable recovery state appears.
A7 Given 100 rows, when another URL is saved, then the count stays 100.
A8 Given only a keyboard, when the form is used, then focus is visible and feedback is available.
## Non-goals
Accounts, cross-device sync, metadata scraping, tags, recommendations,
AI summaries, publishing, and subscriptions. Removing rows is deferred;
this prototype tests save-and-return and exposes its capacity limit.
## Delivery and review
Implement the URL rules first, then storage, then form states.
Developer owns behavior and failure tests. Reviewer completes A1–A8
in the prototype. Report browser and assistive-technology coverage.
## Revisit triggers
If test readers need another device, review identity and synchronization.
If URL labels prevent recognition, evaluate optional user-entered titles
before introducing a network fetch. Observe save-and-return behavior
in a consented trial; do not invent a success percentage in advance.
5. Make the riskiest rule executable
You do not need a whole application to test a consequential assumption. Here the duplicate policy is a small pure function: given a string, return its canonical form or reject it. Keeping that policy independent from the form lets the interface and storage code share one interpretation.
The function below enforces a 2,048-character input limit, accepts only HTTP(S), rejects embedded credentials, and removes fragments. That limit is a choice for this prototype, not an Internet standard. The parser also performs its own serialization, such as lowercasing the hostname. We preserve those results rather than building a URL parser ourselves.
export function canonicalUrl(input) {
if (typeof input !== "string" || input.length > 2048) {
throw new Error("Enter a URL of at most 2048 characters.");
}
let url;
try { url = new URL(input.trim()); }
catch { throw new Error("Enter a complete HTTP(S) URL."); }
if (!["http:", "https:"].includes(url.protocol) ||
url.username || url.password) {
throw new Error("Use HTTP(S) without embedded credentials.");
}
url.hash = "";
return url.href;
}
Tests should distinguish outcomes the product cares about. The fragment test proves our chosen identity rule. The query test protects against over-aggressive deduplication. The rejection test prevents an unsafe scheme from reaching a clickable saved link. A test that merely checks the function exists would not protect any of these promises.
import test from "node:test";
import assert from "node:assert/strict";
import { canonicalUrl } from "./url-policy.mjs";
test("the product's duplicate rule removes fragments", () => {
assert.equal(canonicalUrl(" https://EXAMPLE.com/guide#setup "),
"https://example.com/guide");
assert.equal(canonicalUrl("https://example.com/guide#two"),
canonicalUrl("https://example.com/guide#one"));
});
test("queries and paths remain meaningful", () => {
assert.notEqual(canonicalUrl("https://example.com/?page=1"),
canonicalUrl("https://example.com/?page=2"));
assert.notEqual(canonicalUrl("https://example.com/guide"),
canonicalUrl("https://example.com/guide/"));
});
test("invalid and credential-bearing inputs are rejected", () => {
for (const value of ["", "hello", "javascript:alert(1)",
"https://name:password@example.com", null, "x".repeat(2049)]) {
assert.throws(() => canonicalUrl(value));
}
});
# Download both .mjs files into the same empty folder.
node --version
node --test url-policy.test.mjs6. Use the agent to expose gaps, then revise
An agent is useful here as an impatient implementer. Give it the repository context and ask which decisions prevent it from writing a correct first version. Avoid a general request to “make the PRD comprehensive,” which can produce an impressive inventory of unrelated features. Constrain the review to the chosen journey and observable failures.
Read the product notes and relevant existing code. Scope a browser-local
reading list for saving a URL and reopening it after reload.
Before coding, return:
1. Facts from the repository, assumptions, and chosen defaults separately.
2. One complete journey with literal input and visible output.
3. URL identity, storage, duplicate, and failure rules.
4. Given/When/Then acceptance cases and explicit non-goals.
Ask only questions that change this first slice. Do not invent user research.Imagine the first draft says “persist links locally and show a success toast.” Challenge it with one concrete counterexample: the browser storage write throws. If the agent responds by adding a generic error section, ask it to amend the journey itself. The corrected sequence should be validate, attempt write, then confirm success. This is a revision to behavior, not a stylistic improvement.
Act as the developer implementing this brief. Walk through a duplicate,
a storage exception, corrupt stored data, and a keyboard-only save.
Point to the exact rule that determines each outcome.
If a rule is missing, propose one bounded default and explain its tradeoff.
Return the revised brief plus the decisions you changed.Review the resulting choices yourself. The agent can identify contradictions, but it cannot discover your customers' preferences from an empty repository. Keep the assumptions labeled, and move unresolved research questions into a separate follow-up so they do not masquerade as implemented requirements.
7. Hand off a decision trail that can change
Give the implementation owner the brief, the URL module, and its tests together. Start with the shared URL rule, then build the storage adapter, then connect the visible form states. This order avoids polishing an interaction around a contract that is still moving. Record which acceptance cases are automated and which require a browser review.
When someone requests automatic page titles, return to the non-goal and the reason behind it. A title field entered by the reader and a server that fetches arbitrary URLs are different changes. Choose deliberately, revise the brief, add the new failure cases, and then implement. The old brief is a record of a boundary, not a prohibition on learning.
A useful completion review asks three things: can a reader finish the loop, can they recover when it fails, and does the evidence match the claims? A passing URL test answers only part of the first question. A browser recording of the happy path answers nothing about a storage exception. Assemble evidence against the actual acceptance cases instead of counting green checks.
For your next feature, keep the method and replace the example. Name the moment, choose the loop, specify the ambiguous words, and make one risky rule executable. The skill below packages that process into reusable instructions. Read the resulting brief before allowing it to drive implementation; its purpose is to make decisions visible, not to remove your judgment.
Take it into your project
Files, a full article, and an agent skill.
Download the files you need into the same folder. Each article explains which examples run on their own and which are reference documents.
- brief.md ↓
The completed reading-list brief, including eight acceptance cases.
- url-policy.mjs ↓
The standalone URL identity rule used in the article.
- url-policy.test.mjs ↓
Three executable cases; run with Node.js 22+.
- article.md ↓
The complete guide, with its inline examples and references, for offline reading.
Use the method with your agent
The article explains the decisions; the skill gives your agent reusable instructions. Review it, create a folder named frame-a-feature in your client’s configured skills directory, and save the file there as SKILL.md. Installation and activation vary by client. In a client without skill support, paste the instructions into your task.
Use frame-a-feature to scope a source-linked reading list. Readers save a URL and return to it later. Keep the first version local; produce a brief and acceptance cases before implementation.Inspect the complete skill before using it
---
name: frame-a-feature
description: "Define an implementable feature brief with observable acceptance cases. Use before building a new feature or narrowing an ambiguous product request."
---
# Turn an idea into a buildable brief
A scoped brief, an explicit non-goal list, and examples a developer can turn into tests.
## Inputs
The user problem, existing product, delivery constraints, and one example of success.
## Understand the existing product
Read relevant product notes and the current implementation. Name the user, triggering situation, present workaround, and desired result. Reuse decisions already made. Ask only about missing information that would materially change the implementation.
## Make the slice testable
Write one happy-path example using concrete input and expected visible output. Add the most consequential failure and repeat-action cases. Distinguish missing information from a chosen default. Define what is deliberately excluded from this iteration.
## Produce the handoff
Return: problem; example journey; input/output contract; affected screens and states; ownership/storage boundary; acceptance cases; unknowns. Each acceptance case must contain Given, When, and Then. Avoid specifying a framework or paid service unless the project requires it.
## Check the brief
A reader must be able to determine whether the example succeeded without asking what 'good' means. If an unknown blocks implementation, identify the smallest decision needed. Do not turn the brief into authorization to publish, purchase, or change access.
## Example request
Use frame-a-feature to scope a source-linked reading list. Readers save a URL and return to it later. Keep the first version local; produce a brief and acceptance cases before implementation.
References & scope
The worked examples and recommendations are original instructional material. These references support the specific formats and APIs used; they do not establish a benchmark or endorse the example architecture.
- Node.js test runner ↗
Reference for the built-in test runner used in the optional exercise.
- Agent Skills specification ↗
File format for the accompanying reusable instruction.