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
7 changes: 6 additions & 1 deletion dev/plans/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,9 @@ Completed plans are **removed from the tree** after land. Use merged PRs and `gi

---

**New plans:** `YYMMDD-short-slug.md` in this directory — reconcile here before adding. Todo-like backlog spikes live in Linear; this directory is for approved implementation plans and handoffs. **When done:** remove the plan file and update this README shipped table; do not keep an `archive/` copy.
**New plans:** manual planning-workflow plans use `YYMMDD-short-slug.md`; the
Linear Spec operation uses the exact issue key, such as `FER-273.md`. Reconcile
either kind here before adding it. Todo-like backlog spikes live in Linear; this
directory is for approved implementation plans and handoffs. **When done:**
remove the plan file and update this README shipped table; do not keep an
`archive/` copy.
9 changes: 7 additions & 2 deletions docs/contributing/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,12 +128,14 @@ real consumers expose the same stable contract.
| ------------------------ | --------------------------------------------------------------------------------------------------------------- |
| `bin/` | CLI entrypoint, generated help, and the persistent Linear worker command |
| `lib/agent/` | Provider-neutral Agent contract, invocation support, structured output, streams, sessions, and workspace guards |
| `lib/work-item/` | Source-neutral normalized work-item, evidence, and portable-path schemas shared by proven operations |
| `providers/` | Cursor and Codex invocation, auth, streaming, sessions, sandboxing, and provider result translation |
| `lib/config/` | `harness.json` composition, workspace resolution, local initialization, and review option resolution |
| `lib/review/` | Review runtime, prompt context, aggregation, events, schemas, artifacts, handoffs, and run management |
| `workflows/` | Callable provider-neutral change-review and plan-review definitions |
| `lib/linear/` | Standalone, JSON-safe Linear read, write, pagination, and webhook primitives without domain or delivery policy |
| `lib/triage/` | Triage prompt, structured decision schema, and provider-independent operation |
| `lib/spec/` | Spec prompt, structured result schema, issue-key artifact validation, and provider-independent operation |
| `lib/linear-automation/` | Linear readiness policy, application event contracts, Inngest functions, worker config, and process hosting |
| `lib/skills/` | Packaged-skill installation support |
| `skills/` | Packaged skills installed into target repositories |
Expand Down Expand Up @@ -161,8 +163,10 @@ durable summaries, metadata, stream records, and incomplete-run cleanup.

Domain operations accept plain serializable input and return validated
structured output. They own policy and prompt rendering, but they know nothing
about Inngest scheduling. The triage operation follows this boundary and can be
tested with a fake agent without starting a worker.
about Inngest scheduling. Triage is read-only. Spec receives an isolated
writable workspace, writes one issue-keyed plan, and validates the claimed
artifact without publishing it. Both operations can be tested with a fake agent
without starting a worker.

### Service boundary

Expand Down Expand Up @@ -200,6 +204,7 @@ check the runtime schema, exported schema, prompt, and consumer together.
| ------------------------------------------------ | ------------------------------------------------------------------------------------- |
| `.harness/runs/reviews/<run-id>/` | Review context, prompts, structured results, streams, summaries, metadata, and events |
| `.harness/bin/harness` | Ignored target-repo shim pointing to the Harness checkout that initialized it |
| `dev/plans/<ISSUE-KEY>.md` | Tracked Spec artifact written in an isolated workspace; publication remains separate |
| Self-hosted Inngest SQLite volume | Local delivery history, retry state, function metadata, and traces |
| Protected worker environment file outside a repo | Linear, Inngest, and optional Codex credentials for one deployment |
| Dedicated worker Codex credential volume | Optional unattended ChatGPT-backed Codex login, separate from the host account |
Expand Down
169 changes: 169 additions & 0 deletions lib/spec/prompt.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import { describe, expect, it } from "vitest";
import { renderSpecPrompt, SPEC_POLICY_VERSION } from "./prompt.ts";
import type { SpecWorkItemContext } from "./schema.ts";

describe("Spec prompt", () => {
it("renders deterministic complete context and the exact issue-key artifact path", () => {
const workItem = validContext();
const artifactPath = "dev/plans/FER-273.md";
const prompt = renderSpecPrompt({ workItem, artifactPath });

expect(SPEC_POLICY_VERSION).toBe("2");
expect(prompt).toContain(JSON.stringify(workItem, null, 2));
expect(prompt).toContain(`Write the complete Spec at exactly ${artifactPath}`);
expect(prompt).toContain(`artifactPath must be exactly "${artifactPath}"`);
expect(prompt).toContain("Do not use a date or title slug.");
expect(renderSpecPrompt({ workItem, artifactPath })).toBe(prompt);
});

it("grounds and reconciles the Spec in authority order before structuring it", () => {
const prompt = renderSpecPrompt({
workItem: validContext(),
artifactPath: "dev/plans/FER-273.md",
});

expect(prompt).toContain("Read repository guidance such as AGENTS.md and README.md");
expect(prompt).toContain("authoritative project intent or vision source");
expect(prompt).toContain(
"repository invariants and current project intent; explicit requirements and accepted decisions; verified codebase facts",
);
expect(prompt).toContain("Separate current behavior from requested behavior");
expect(prompt).toContain("Research first");
expect(prompt).toContain("Prefer one coherent recommended direction");
expect(prompt).toContain("Mention a verified skill or aid beside a concrete change only");
expect(prompt).toContain("A ready Spec requires repository evidence");
});

it("separates reviewer decisions from prerequisite human input", () => {
const prompt = renderSpecPrompt({
workItem: validContext(),
artifactPath: "dev/plans/FER-273.md",
});

expect(prompt).toContain("Reserve Needs Input for a true prerequisite");
expect(prompt).toContain("missing decision materially changes scope or architecture");
expect(prompt).toContain("A later approval or human-authority product choice");
expect(prompt).toContain("do not leave raw alternatives or unresolved implementation choices");
expect(prompt).toContain("at least two unique options with tradeoffs");
expect(prompt).toContain("recommendation that exactly matches an option");
expect(prompt).toContain("must not be asked to resolve a reviewer decision");
expect(prompt).toContain("reviewerDecisions must be []");
});

it("keeps the artifact decision-focused, right-sized, and portable", () => {
const prompt = renderSpecPrompt({
workItem: validContext(),
artifactPath: "dev/plans/FER-273.md",
});

expect(prompt).toContain("Right-size the artifact");
expect(prompt).toContain("Capture decisions, not code");
expect(prompt).toContain("Do not pre-write implementation code or shell-command choreography");
expect(prompt).toContain("label it as directional rather than an implementation specification");
expect(prompt).toContain(
"Do not embed provider-specific or tool-specific executor instructions",
);
expect(prompt).toContain("Explicitly defer ordinary execution-time discovery");
});

it("shapes multi-unit work vertically and explains necessary horizontal units", () => {
const prompt = renderSpecPrompt({
workItem: validContext(),
artifactPath: "dev/plans/FER-273.md",
});

expect(prompt).toContain("Prefer vertical slices");
expect(prompt).toContain("separate agents can own with limited overlap");
expect(prompt).toContain("Do not divide work mechanically by repository layer");
expect(prompt).toContain("vertical delivery is impractical or unsafe");
});

it("selects the highest stable proof seam", () => {
const prompt = renderSpecPrompt({
workItem: validContext(),
artifactPath: "dev/plans/FER-273.md",
});

expect(prompt).toContain("highest existing stable test seam that proves acceptance");
expect(prompt).toContain("distinct invariant or failure mode");
expect(prompt).toContain("repository's canonical gate");
});

it("keeps the agent inside the operation boundary", () => {
const prompt = renderSpecPrompt({
workItem: validContext(),
artifactPath: "dev/plans/FER-273.md",
});

expect(prompt).toContain("Do not edit product code.");
expect(prompt).toContain("Do not create branches, commits, or pull requests.");
expect(prompt).toContain(
"Reconcile dev/plans/README.md only when repository guidance explicitly requires it.",
);
expect(prompt).toContain("Return only the final JSON object matching the supplied schema.");
});

it("rejects incomplete work-item context before rendering", () => {
const context = validContext();
const invalid = { ...context, completeness: { ...context.completeness } } as Record<
string,
unknown
>;
delete (invalid.completeness as Record<string, unknown>).relationsTruncated;

expect(() =>
renderSpecPrompt({
workItem: invalid as SpecWorkItemContext,
artifactPath: "dev/plans/FER-273.md",
}),
).toThrow(/relationsTruncated/);
});
});

function validContext(): SpecWorkItemContext {
return {
id: "issue-273",
reference: "FER-273",
title: "Build a provider-neutral Spec operation",
description: "Write one code-grounded implementation Spec.",
url: "https://linear.app/issue/FER-273",
state: "Open",
labels: ["Spec"],
comments: [
{
author: "Felipe",
body: "Use the Linear identifier as the filename.",
createdAt: "2026-07-22T20:00:00.000Z",
},
],
parent: {
id: "issue-211",
reference: "FER-211",
title: "Build modular Linear automation",
url: "https://linear.app/issue/FER-211",
state: "In Progress",
},
children: [],
duplicateOf: null,
blockedBy: [],
related: [
{
id: "issue-280",
reference: "FER-280",
title: "Decompose oversized modules",
url: "https://linear.app/issue/FER-280",
state: "Done",
},
],
links: [{ title: "Design reference", url: "https://example.com/design" }],
createdAt: "2026-07-21T22:12:57.641Z",
updatedAt: "2026-07-22T20:00:00.000Z",
completeness: {
commentsTruncated: true,
labelsTruncated: false,
relationsTruncated: false,
linksTruncated: false,
childrenTruncated: false,
},
};
}
84 changes: 84 additions & 0 deletions lib/spec/prompt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { SpecWorkItemContextSchema, type SpecWorkItemContext } from "./schema.ts";

export const SPEC_POLICY_VERSION = "2";

export function renderSpecPrompt(input: {
workItem: SpecWorkItemContext;
artifactPath: string;
}): string {
const workItem = SpecWorkItemContextSchema.parse(input.workItem);

return `# Implementation Spec

You are the Spec agent for one bounded work item in the current repository. Research the repository and either produce one minimum-sufficient implementation Spec for human review or identify a prerequisite human answer that blocks every useful Spec.

The work item has already been routed to Spec. Do not re-triage its Linear status, labels, or lifecycle.

Apply this policy in order:

1. Ground the work in current authority.
- Read repository guidance such as AGENTS.md and README.md, then follow links to the authoritative project intent or vision source.
- Use this authority order: repository invariants and current project intent; explicit requirements and accepted decisions; verified codebase facts.
- Treat old plans, archived roadmaps, and unmarked proposals as context only. When accepted work intentionally changes current intent, name that direction change and its review boundary instead of silently overriding either source.
- Inspect only the relevant code, callers, contracts, tests, docs, active plans, work-item discussion, and institutional guidance before choosing a direction. Verify external contracts or guidance when they materially affect the plan.

2. Reconcile requirements with reality before structuring the Spec.
- Separate current behavior from requested behavior. Resolve stale claims, already-implemented baseline, conflicts, real gaps, acceptance criteria, and the smallest credible solution.
- Repository inspection, diagnosis, technical research, option discovery, and migration design are agent work. Research first; do not organize an issue summary into steps before verifying it.
- Reject speculative hardening, future frameworks, unrelated cleanup, and compatibility that no current authority requires.
- Inspect available executor-aid descriptions and repository guidance. Mention a verified skill or aid beside a concrete change only when it adds non-obvious execution guidance; do not add a generic skills inventory.

3. Resolve planning decisions. Reserve Needs Input for a true prerequisite.
- Resolve planning-time implementation choices with repository and research evidence. Prefer one coherent recommended direction; do not leave raw alternatives or unresolved implementation choices for the executor.
- Explicitly defer ordinary execution-time discovery that does not change scope or architecture, such as locating nearby details within a named owner. Do not turn inspectable repository basics into plan steps.
- Use outcome "needs-input" before writing when a missing decision materially changes scope or architecture and no coherent useful Spec can be produced until a human supplies missing or contradictory intent, credentials, inaccessible required evidence, or an external fact.
- A later approval or human-authority product choice is not prerequisite input when research can produce concrete options, tradeoffs, and a recommendation without invalidating the rest of the Spec. Record that as a reviewer decision instead.
- Ask only the smallest concrete questions that unblock useful Spec work.

4. Design the smallest coherent change.
- Choose the smallest change that satisfies the acceptance criteria. Right-size the artifact: simple work gets a compact Spec; larger or riskier work gets only the extra structure its decisions require.
- Capture decisions, not code: state the approach, boundaries, exact files or symbols, ownership, dependencies, material risks, and test scenarios.
- Do not pre-write implementation code or shell-command choreography. Use a short pseudo-code sketch or grammar only when it helps review the direction, and label it as directional rather than an implementation specification.
- Keep the Spec portable as a living plan, review artifact, or issue body. Do not embed provider-specific or tool-specific executor instructions.
- When replacing, redirecting, splitting, or removing behavior, name its post-change owner, removals, cutover order, and required compatibility where those decisions matter.

5. Shape multi-unit work as independently useful delivery.
- Prefer vertical slices where each unit completes one coherent observable behavior across the boundaries it needs and can be verified when it lands.
- Prefer units that separate agents can own with limited overlap and that can be reviewed, landed, rolled back, or continued independently. Keep shared setup to the minimum required by the first slice so later units can proceed in parallel where practical.
- Do not divide work mechanically by repository layer or component type.
- Keep an indivisible migration, cross-cutting safety fix, or minimum shared prerequisite horizontal only when vertical delivery is impractical or unsafe. State the reason and keep that unit no broader than necessary.

6. Choose focused proof.
- Prefer the highest existing stable test seam that proves acceptance. Add a lower seam only for a distinct invariant or failure mode that the higher seam cannot observe.
- Keep verification to focused behavioral checks and the repository's canonical gate. Do not duplicate covered commands or prescribe unverified command names.

7. Write the artifact when ready for review.
- Write the complete Spec at exactly ${input.artifactPath}.
- Do not choose another filename. Do not use a date or title slug.
- Use the repository's required plan shape. Otherwise use concise Goal, Changes, Verify, and optional Boundaries sections.
- The Spec must establish the intended outcome and acceptance criteria, relevant intent constraints, verified current-state facts, resolved decisions, named ownership, material boundaries, executor aids where useful, and focused verification.
- Do not edit product code. Do not create branches, commits, or pull requests.
- Reconcile dev/plans/README.md only when repository guidance explicitly requires it. Do not edit any other file.

8. Make reviewer decisions useful.
- Reviewer decisions are allowed in a ready Spec when human authority is genuinely required after research.
- Each decision must contain one concrete question, at least two unique options with tradeoffs, one recommendation that exactly matches an option, and evidence-backed rationale.
- Keep reviewerDecisions empty when repository authority and accepted requirements already determine the answer. The executor must not be asked to resolve a reviewer decision during implementation.

Structured-result rules:

- Return only the final JSON object matching the supplied schema. Do not include the Spec markdown in JSON.
- For "ready-for-review": artifactPath must be exactly "${input.artifactPath}", questions must be [], and the claimed artifact must already exist.
- For "needs-input": artifactPath must be null, reviewerDecisions must be [], and questions must contain the prerequisite questions.
- summary is a concise description of the artifact for ready-for-review, or the evidence-backed blocking rationale for needs-input.
- Include at least one evidence item. A ready Spec requires repository evidence, not only tracker evidence.
- Tracker evidence uses path null. Code, docs, and test evidence uses a portable repository-relative path. Repo-state evidence may use path null.
- Do not invent facts hidden by truncated context or inaccessible systems.

Work-item context:

\`\`\`json
${JSON.stringify(workItem, null, 2)}
\`\`\`
`;
}
Loading