@slowcook-ai/cli 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -1
- package/dist/cli.js +8 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/refine/agent.d.ts +62 -0
- package/dist/commands/refine/agent.d.ts.map +1 -0
- package/dist/commands/refine/agent.js +209 -0
- package/dist/commands/refine/agent.js.map +1 -0
- package/dist/commands/refine/index.d.ts +2 -0
- package/dist/commands/refine/index.d.ts.map +1 -0
- package/dist/commands/refine/index.js +177 -0
- package/dist/commands/refine/index.js.map +1 -0
- package/dist/commands/refine/llm.d.ts +27 -0
- package/dist/commands/refine/llm.d.ts.map +1 -0
- package/dist/commands/refine/llm.js +33 -0
- package/dist/commands/refine/llm.js.map +1 -0
- package/dist/commands/refine/prompts.d.ts +23 -0
- package/dist/commands/refine/prompts.d.ts.map +1 -0
- package/dist/commands/refine/prompts.js +115 -0
- package/dist/commands/refine/prompts.js.map +1 -0
- package/dist/commands/refine/relationship.d.ts +21 -0
- package/dist/commands/refine/relationship.d.ts.map +1 -0
- package/dist/commands/refine/relationship.js +148 -0
- package/dist/commands/refine/relationship.js.map +1 -0
- package/dist/commands/refine/spec-yaml.d.ts +192 -0
- package/dist/commands/refine/spec-yaml.d.ts.map +1 -0
- package/dist/commands/refine/spec-yaml.js +125 -0
- package/dist/commands/refine/spec-yaml.js.map +1 -0
- package/package.json +7 -3
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* System prompts for the refinement agent.
|
|
3
|
+
*
|
|
4
|
+
* Two distinct calls:
|
|
5
|
+
* 1. RELATIONSHIP_ANALYST — classifies a new issue against existing specs.
|
|
6
|
+
* 2. REFINEMENT_ANALYST — runs the clarifying-question loop and emits
|
|
7
|
+
* the final spec YAML.
|
|
8
|
+
*
|
|
9
|
+
* Both are designed for Claude Opus 4.7 as the default. The relationship
|
|
10
|
+
* analyst can run on Sonnet to keep costs down; refinement proper benefits
|
|
11
|
+
* from Opus-level reasoning.
|
|
12
|
+
*/
|
|
13
|
+
export declare const SPEC_CHECKLIST_MD = "\nA complete, testable spec covers ALL of these items. If any are missing or ambiguous, ask about them:\n\n1. **Actors** \u2014 who performs the action? (e.g., authenticated member, admin, anonymous visitor)\n2. **Preconditions** \u2014 what must be true before the action can happen? Auth status, state of prior data, feature flags, etc.\n3. **Invariants** \u2014 what must remain true regardless of input or timing? (e.g., \"ration never exceeds 15\", \"one vote per user per poll\")\n4. **API contract** (if applicable) \u2014 HTTP method, path, request shape, success response shape, error codes and when each fires\n5. **UI behavior** per relevant viewport \u00D7 color scheme \u2014 what the user sees and how they interact, at minimum: desktop_light, mobile_light, and mobile_dark\n6. **Acceptance scenarios** \u2014 concrete Given/When/Then examples that an engineer can turn into tests. Aim for 3-6, covering happy path AND edge cases.\n7. **Non-goals** \u2014 what is explicitly out of scope for this story? (e.g., \"editing reactions is a separate story\")\n";
|
|
14
|
+
export declare const RELATIONSHIP_ANALYST_SYSTEM = "You are a careful spec analyst for the slowcook brewing harness.\n\nGiven a new GitHub issue and a list of existing specs (summaries + selected full bodies), classify the relationship. The goal is to preserve a ratchet: new decisions must not silently duplicate or contradict earlier decisions.\n\nClassify as one of:\n - \"new_or_independent\": this issue addresses a concern not covered by any active spec\n - \"overlap\": this issue substantially intersects with one or more active specs (same API, same feature, same user journey, same invariant). Could be resolved by merging, scoping to a delta, or closing as duplicate.\n - \"contradiction\": this issue proposes something incompatible with an active spec \u2014 reverses a rule, changes a decision, breaks an invariant. The caller will check for a \"change-of-mind\" label: if present, it is authorized revocation; if absent, it is a blocker.\n\nReturn STRICTLY the following JSON, no prose before or after:\n\n{\n \"kind\": \"new_or_independent\" | \"overlap\" | \"contradiction\",\n \"conflicting_ids\": [\"042\", \"007\"],\n \"reasoning\": \"one-paragraph explanation citing specific spec ids and the exact overlap/contradiction\"\n}\n\n- For \"new_or_independent\", set conflicting_ids to [].\n- For \"overlap\" and \"contradiction\", list every spec id you find, not just the strongest match.\n- \"reasoning\" should name the specs by id and cite what specifically overlaps or contradicts. Be concrete \u2014 \"story-042 defines POST /api/reactions with a 15/week ration; this issue changes the ration to 20/week\" is good.\n- If information is insufficient to classify with confidence, pick the most conservative outcome (contradiction > overlap > independent).";
|
|
15
|
+
export declare const REFINEMENT_ANALYST_SYSTEM: (checklist: string) => string;
|
|
16
|
+
/** Trivial, used only as a title for the draft PR. */
|
|
17
|
+
export declare function draftPrTitle(storyId: string, title: string): string;
|
|
18
|
+
export declare function draftPrBody(args: {
|
|
19
|
+
storyId: string;
|
|
20
|
+
issueNumber: number;
|
|
21
|
+
supersedes: string[];
|
|
22
|
+
}): string;
|
|
23
|
+
//# sourceMappingURL=prompts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompts.d.ts","sourceRoot":"","sources":["../../../src/commands/refine/prompts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,eAAO,MAAM,iBAAiB,kjCAU7B,CAAC;AAEF,eAAO,MAAM,2BAA2B,wsDAoBkG,CAAC;AAE3I,eAAO,MAAM,yBAAyB,GAAI,WAAW,MAAM,WAoDwE,CAAC;AAEpI,sDAAsD;AACtD,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAEnE;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB,GAAG,MAAM,CAaT"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* System prompts for the refinement agent.
|
|
3
|
+
*
|
|
4
|
+
* Two distinct calls:
|
|
5
|
+
* 1. RELATIONSHIP_ANALYST — classifies a new issue against existing specs.
|
|
6
|
+
* 2. REFINEMENT_ANALYST — runs the clarifying-question loop and emits
|
|
7
|
+
* the final spec YAML.
|
|
8
|
+
*
|
|
9
|
+
* Both are designed for Claude Opus 4.7 as the default. The relationship
|
|
10
|
+
* analyst can run on Sonnet to keep costs down; refinement proper benefits
|
|
11
|
+
* from Opus-level reasoning.
|
|
12
|
+
*/
|
|
13
|
+
export const SPEC_CHECKLIST_MD = `
|
|
14
|
+
A complete, testable spec covers ALL of these items. If any are missing or ambiguous, ask about them:
|
|
15
|
+
|
|
16
|
+
1. **Actors** — who performs the action? (e.g., authenticated member, admin, anonymous visitor)
|
|
17
|
+
2. **Preconditions** — what must be true before the action can happen? Auth status, state of prior data, feature flags, etc.
|
|
18
|
+
3. **Invariants** — what must remain true regardless of input or timing? (e.g., "ration never exceeds 15", "one vote per user per poll")
|
|
19
|
+
4. **API contract** (if applicable) — HTTP method, path, request shape, success response shape, error codes and when each fires
|
|
20
|
+
5. **UI behavior** per relevant viewport × color scheme — what the user sees and how they interact, at minimum: desktop_light, mobile_light, and mobile_dark
|
|
21
|
+
6. **Acceptance scenarios** — concrete Given/When/Then examples that an engineer can turn into tests. Aim for 3-6, covering happy path AND edge cases.
|
|
22
|
+
7. **Non-goals** — what is explicitly out of scope for this story? (e.g., "editing reactions is a separate story")
|
|
23
|
+
`;
|
|
24
|
+
export const RELATIONSHIP_ANALYST_SYSTEM = `You are a careful spec analyst for the slowcook brewing harness.
|
|
25
|
+
|
|
26
|
+
Given a new GitHub issue and a list of existing specs (summaries + selected full bodies), classify the relationship. The goal is to preserve a ratchet: new decisions must not silently duplicate or contradict earlier decisions.
|
|
27
|
+
|
|
28
|
+
Classify as one of:
|
|
29
|
+
- "new_or_independent": this issue addresses a concern not covered by any active spec
|
|
30
|
+
- "overlap": this issue substantially intersects with one or more active specs (same API, same feature, same user journey, same invariant). Could be resolved by merging, scoping to a delta, or closing as duplicate.
|
|
31
|
+
- "contradiction": this issue proposes something incompatible with an active spec — reverses a rule, changes a decision, breaks an invariant. The caller will check for a "change-of-mind" label: if present, it is authorized revocation; if absent, it is a blocker.
|
|
32
|
+
|
|
33
|
+
Return STRICTLY the following JSON, no prose before or after:
|
|
34
|
+
|
|
35
|
+
{
|
|
36
|
+
"kind": "new_or_independent" | "overlap" | "contradiction",
|
|
37
|
+
"conflicting_ids": ["042", "007"],
|
|
38
|
+
"reasoning": "one-paragraph explanation citing specific spec ids and the exact overlap/contradiction"
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
- For "new_or_independent", set conflicting_ids to [].
|
|
42
|
+
- For "overlap" and "contradiction", list every spec id you find, not just the strongest match.
|
|
43
|
+
- "reasoning" should name the specs by id and cite what specifically overlaps or contradicts. Be concrete — "story-042 defines POST /api/reactions with a 15/week ration; this issue changes the ration to 20/week" is good.
|
|
44
|
+
- If information is insufficient to classify with confidence, pick the most conservative outcome (contradiction > overlap > independent).`;
|
|
45
|
+
export const REFINEMENT_ANALYST_SYSTEM = (checklist) => `You are a rigorous product analyst for the slowcook brewing harness.
|
|
46
|
+
|
|
47
|
+
Your job is to help the PM turn a GitHub issue into a precise, testable spec. You operate in rounds: each round, you either (a) ask the PM clarifying questions OR (b) emit the final spec as YAML. You do not both ask AND emit in the same round.
|
|
48
|
+
|
|
49
|
+
## The spec must be complete
|
|
50
|
+
|
|
51
|
+
${checklist}
|
|
52
|
+
|
|
53
|
+
## How to decide: ask vs emit
|
|
54
|
+
|
|
55
|
+
**Ask** if:
|
|
56
|
+
- Any checklist item is missing OR ambiguous
|
|
57
|
+
- A stated requirement has implied questions the spec doesn't answer (e.g., "ration" implies: what period? what counts? what happens when exhausted?)
|
|
58
|
+
- Acceptance scenarios leave happy path + edge cases underspecified
|
|
59
|
+
|
|
60
|
+
**Emit** if:
|
|
61
|
+
- Every checklist item is present with concrete, testable language
|
|
62
|
+
- Acceptance scenarios cover happy path + at least 2 edge cases and map cleanly to test cases
|
|
63
|
+
- Non-goals explicitly close off likely scope creep
|
|
64
|
+
|
|
65
|
+
## Output formats
|
|
66
|
+
|
|
67
|
+
When asking: output a SINGLE Markdown comment, numbered list, ≤5 questions per round. Prefer fewer, sharper questions over a long list. Ask the MOST important first. Group related questions if they share context. Address the PM directly ("you"). Begin with a one-line acknowledgment of what you have so far. End with:
|
|
68
|
+
|
|
69
|
+
"Please answer inline by replying to this comment. I'll continue when you do."
|
|
70
|
+
|
|
71
|
+
When emitting the spec: output ONLY the YAML, nothing before or after, starting with \`---\` and ending with the last field. The YAML MUST validate against this schema (\`?\` marks optional):
|
|
72
|
+
|
|
73
|
+
- story_id: string (provided to you — don't invent)
|
|
74
|
+
- title: string (one-line description)
|
|
75
|
+
- status: "active"
|
|
76
|
+
- created_at: ISO-8601 UTC timestamp (provided to you)
|
|
77
|
+
- supersedes: string[] (provided to you)
|
|
78
|
+
- superseded_by: null
|
|
79
|
+
- token_budget_usd?: number
|
|
80
|
+
- estimate?: "small" | "medium" | "large" (small ≤4h, medium ≤12h, large = split it)
|
|
81
|
+
- source_issue: "#N" (the issue number, provided)
|
|
82
|
+
- refined_by: "slowcook-refine@<version>" (provided)
|
|
83
|
+
- actors: [{ name, notes? }]
|
|
84
|
+
- preconditions: string[]
|
|
85
|
+
- invariants: string[]
|
|
86
|
+
- api_contract?: [{ method, path, request_schema?, responses? }]
|
|
87
|
+
- ui_behavior?: { desktop_light: string, mobile_light: string, mobile_dark: string, ... }
|
|
88
|
+
- acceptance_scenarios: string[] (Given/When/Then form)
|
|
89
|
+
- non_goals: string[]
|
|
90
|
+
- related_specs?: [{ id, relationship: "overlap"|"related"|"superseded", note? }]
|
|
91
|
+
|
|
92
|
+
## Constraints
|
|
93
|
+
|
|
94
|
+
- Do NOT hallucinate facts not in the issue or prior conversation. If you infer something, flag it as an assumption in the question round.
|
|
95
|
+
- Keep scope tight: a medium-sized story, not an epic. If the issue feels larger, propose splitting in the question round.
|
|
96
|
+
- Treat PM silence as "please ask again" — summarize where we are and re-ask open questions.
|
|
97
|
+
- The spec is the contract for code-generation agents down the pipeline. Every invariant and acceptance scenario must be testable.`;
|
|
98
|
+
/** Trivial, used only as a title for the draft PR. */
|
|
99
|
+
export function draftPrTitle(storyId, title) {
|
|
100
|
+
return `spec: story-${storyId} — ${title}`;
|
|
101
|
+
}
|
|
102
|
+
export function draftPrBody(args) {
|
|
103
|
+
const supersedesSection = args.supersedes.length > 0
|
|
104
|
+
? `\n## Supersedes\n\nThis spec explicitly supersedes: ${args.supersedes
|
|
105
|
+
.map((id) => `story-${id}`)
|
|
106
|
+
.join(", ")}. The index has been updated to mark those stories as superseded.\n`
|
|
107
|
+
: "";
|
|
108
|
+
return `Spec refined from #${args.issueNumber} by the slowcook refinement agent.
|
|
109
|
+
|
|
110
|
+
Review the YAML, edit anything that needs tightening, then mark this PR ready-for-review and merge to freeze the spec.
|
|
111
|
+
${supersedesSection}
|
|
112
|
+
---
|
|
113
|
+
*Generated by \`slowcook refine\`.*`;
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=prompts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompts.js","sourceRoot":"","sources":["../../../src/commands/refine/prompts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,MAAM,CAAC,MAAM,iBAAiB,GAAG;;;;;;;;;;CAUhC,CAAC;AAEF,MAAM,CAAC,MAAM,2BAA2B,GAAG;;;;;;;;;;;;;;;;;;;;0IAoB+F,CAAC;AAE3I,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,SAAiB,EAAE,EAAE,CAAC;;;;;;EAM9D,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mIA8CwH,CAAC;AAEpI,sDAAsD;AACtD,MAAM,UAAU,YAAY,CAAC,OAAe,EAAE,KAAa;IACzD,OAAO,eAAe,OAAO,MAAM,KAAK,EAAE,CAAC;AAC7C,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,IAI3B;IACC,MAAM,iBAAiB,GACrB,IAAI,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC;QACxB,CAAC,CAAC,uDAAuD,IAAI,CAAC,UAAU;aACnE,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,SAAS,EAAE,EAAE,CAAC;aAC1B,IAAI,CAAC,IAAI,CAAC,qEAAqE;QACpF,CAAC,CAAC,EAAE,CAAC;IACT,OAAO,sBAAsB,IAAI,CAAC,WAAW;;;EAG7C,iBAAiB;;oCAEiB,CAAC;AACrC,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { LlmClient } from "./llm.js";
|
|
2
|
+
import type { Spec, RelationshipVerdict } from "@slowcook-ai/core";
|
|
3
|
+
export interface RelationshipInput {
|
|
4
|
+
issueTitle: string;
|
|
5
|
+
issueBody: string;
|
|
6
|
+
activeSpecs: Spec[];
|
|
7
|
+
}
|
|
8
|
+
export interface RelationshipOptions {
|
|
9
|
+
llm: LlmClient;
|
|
10
|
+
model: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function analyzeRelationship(input: RelationshipInput, options: RelationshipOptions): Promise<RelationshipVerdict>;
|
|
13
|
+
export declare function parseVerdict(raw: string): RelationshipVerdict;
|
|
14
|
+
/** Comment body posted to the issue when relationship analysis surfaces a conflict. */
|
|
15
|
+
export declare function overlapCommentBody(verdict: Extract<RelationshipVerdict, {
|
|
16
|
+
kind: "overlap";
|
|
17
|
+
}>): string;
|
|
18
|
+
export declare function contradictionCommentBody(verdict: Extract<RelationshipVerdict, {
|
|
19
|
+
kind: "contradiction";
|
|
20
|
+
}>, hasChangeOfMind: boolean): string;
|
|
21
|
+
//# sourceMappingURL=relationship.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relationship.d.ts","sourceRoot":"","sources":["../../../src/commands/refine/relationship.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAC1C,OAAO,KAAK,EAAE,IAAI,EAAE,mBAAmB,EAAgB,MAAM,mBAAmB,CAAC;AASjF,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,IAAI,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,mBAAmB;IAClC,GAAG,EAAE,SAAS,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf;AAED,wBAAsB,mBAAmB,CACvC,KAAK,EAAE,iBAAiB,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,mBAAmB,CAAC,CAa9B;AAiDD,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,mBAAmB,CA0B7D;AAmBD,uFAAuF;AACvF,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,CAAC,mBAAmB,EAAE;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC,GAAG,MAAM,CAerG;AAED,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,OAAO,CAAC,mBAAmB,EAAE;IAAE,IAAI,EAAE,eAAe,CAAA;CAAE,CAAC,EAChE,eAAe,EAAE,OAAO,GACvB,MAAM,CAwBR"}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { RELATIONSHIP_ANALYST_SYSTEM } from "./prompts.js";
|
|
3
|
+
const VerdictSchema = z.object({
|
|
4
|
+
kind: z.enum(["new_or_independent", "overlap", "contradiction"]),
|
|
5
|
+
conflicting_ids: z.array(z.string()),
|
|
6
|
+
reasoning: z.string(),
|
|
7
|
+
});
|
|
8
|
+
export async function analyzeRelationship(input, options) {
|
|
9
|
+
const userMessage = buildUserMessage(input);
|
|
10
|
+
const raw = await options.llm.complete({
|
|
11
|
+
system: RELATIONSHIP_ANALYST_SYSTEM,
|
|
12
|
+
cacheSystem: true,
|
|
13
|
+
model: options.model,
|
|
14
|
+
messages: [{ role: "user", content: userMessage }],
|
|
15
|
+
maxTokens: 1024,
|
|
16
|
+
temperature: 0,
|
|
17
|
+
});
|
|
18
|
+
const parsed = parseVerdict(raw);
|
|
19
|
+
return parsed;
|
|
20
|
+
}
|
|
21
|
+
function buildUserMessage(input) {
|
|
22
|
+
const specSection = input.activeSpecs.length === 0
|
|
23
|
+
? "There are no active specs yet."
|
|
24
|
+
: input.activeSpecs
|
|
25
|
+
.map((s) => `### story-${s.story_id} — ${s.title} (status: ${s.status})\n` + specSummary(s))
|
|
26
|
+
.join("\n\n");
|
|
27
|
+
return `## New issue
|
|
28
|
+
|
|
29
|
+
Title: ${input.issueTitle}
|
|
30
|
+
|
|
31
|
+
Body:
|
|
32
|
+
${input.issueBody}
|
|
33
|
+
|
|
34
|
+
## Existing active specs
|
|
35
|
+
|
|
36
|
+
${specSection}
|
|
37
|
+
|
|
38
|
+
## Task
|
|
39
|
+
|
|
40
|
+
Classify the relationship of the new issue to the existing specs. Respond with ONLY JSON per the schema in your system prompt.`;
|
|
41
|
+
}
|
|
42
|
+
function specSummary(spec) {
|
|
43
|
+
const lines = [];
|
|
44
|
+
if (spec.invariants.length) {
|
|
45
|
+
lines.push(`Invariants: ${spec.invariants.join("; ")}`);
|
|
46
|
+
}
|
|
47
|
+
if (spec.api_contract && spec.api_contract.length) {
|
|
48
|
+
const endpoints = spec.api_contract
|
|
49
|
+
.map((e) => {
|
|
50
|
+
const obj = e;
|
|
51
|
+
return `${obj.method ?? "?"} ${obj.path ?? "?"}`;
|
|
52
|
+
})
|
|
53
|
+
.join(", ");
|
|
54
|
+
lines.push(`API: ${endpoints}`);
|
|
55
|
+
}
|
|
56
|
+
if (spec.acceptance_scenarios.length) {
|
|
57
|
+
lines.push(`Acceptance: ${spec.acceptance_scenarios.slice(0, 3).join(" / ")}`);
|
|
58
|
+
}
|
|
59
|
+
if (spec.non_goals.length) {
|
|
60
|
+
lines.push(`Non-goals: ${spec.non_goals.join("; ")}`);
|
|
61
|
+
}
|
|
62
|
+
return lines.join("\n");
|
|
63
|
+
}
|
|
64
|
+
export function parseVerdict(raw) {
|
|
65
|
+
const cleaned = extractJsonObject(raw);
|
|
66
|
+
let json;
|
|
67
|
+
try {
|
|
68
|
+
json = JSON.parse(cleaned);
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
throw new Error(`Relationship analyst returned non-JSON. First 300 chars: ${cleaned.slice(0, 300)}`);
|
|
72
|
+
}
|
|
73
|
+
const parsed = VerdictSchema.safeParse(json);
|
|
74
|
+
if (!parsed.success) {
|
|
75
|
+
throw new Error(`Relationship analyst output failed schema validation: ${parsed.error.issues
|
|
76
|
+
.map((i) => `${i.path.join(".")}: ${i.message}`)
|
|
77
|
+
.join("; ")}`);
|
|
78
|
+
}
|
|
79
|
+
const v = parsed.data;
|
|
80
|
+
if (v.kind === "new_or_independent") {
|
|
81
|
+
return { kind: "new_or_independent", reasoning: v.reasoning };
|
|
82
|
+
}
|
|
83
|
+
if (v.kind === "overlap") {
|
|
84
|
+
return { kind: "overlap", conflicting_ids: v.conflicting_ids, reasoning: v.reasoning };
|
|
85
|
+
}
|
|
86
|
+
return { kind: "contradiction", conflicting_ids: v.conflicting_ids, reasoning: v.reasoning };
|
|
87
|
+
}
|
|
88
|
+
/** Best-effort extraction of a JSON object from LLM output that may contain prose. */
|
|
89
|
+
function extractJsonObject(raw) {
|
|
90
|
+
const trimmed = raw.trim();
|
|
91
|
+
// Direct JSON?
|
|
92
|
+
if (trimmed.startsWith("{"))
|
|
93
|
+
return trimmed;
|
|
94
|
+
// Fenced code block?
|
|
95
|
+
const fence = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
96
|
+
if (fence && fence[1])
|
|
97
|
+
return fence[1].trim();
|
|
98
|
+
// First brace through matching brace?
|
|
99
|
+
const firstBrace = trimmed.indexOf("{");
|
|
100
|
+
const lastBrace = trimmed.lastIndexOf("}");
|
|
101
|
+
if (firstBrace !== -1 && lastBrace > firstBrace) {
|
|
102
|
+
return trimmed.slice(firstBrace, lastBrace + 1);
|
|
103
|
+
}
|
|
104
|
+
return trimmed;
|
|
105
|
+
}
|
|
106
|
+
/** Comment body posted to the issue when relationship analysis surfaces a conflict. */
|
|
107
|
+
export function overlapCommentBody(verdict) {
|
|
108
|
+
const specs = verdict.conflicting_ids.map((id) => `\`story-${id}\``).join(", ");
|
|
109
|
+
return `### slowcook · overlap detected
|
|
110
|
+
|
|
111
|
+
This issue overlaps with existing active specs: ${specs}.
|
|
112
|
+
|
|
113
|
+
**Reasoning:** ${verdict.reasoning}
|
|
114
|
+
|
|
115
|
+
Please choose how to proceed:
|
|
116
|
+
|
|
117
|
+
- **Merge**: close this issue and extend the overlapping spec. Comment \`/slowcook merge-into story-<id>\` and I'll relabel.
|
|
118
|
+
- **Delta**: reframe this issue to cover only what the overlapping spec doesn't (and say so in the body). I'll re-analyze on the next comment.
|
|
119
|
+
- **Duplicate**: close this issue if it's entirely covered.
|
|
120
|
+
|
|
121
|
+
I'll pause refinement until the issue body or labels are updated.`;
|
|
122
|
+
}
|
|
123
|
+
export function contradictionCommentBody(verdict, hasChangeOfMind) {
|
|
124
|
+
const specs = verdict.conflicting_ids.map((id) => `\`story-${id}\``).join(", ");
|
|
125
|
+
if (hasChangeOfMind) {
|
|
126
|
+
return `### slowcook · change-of-mind authorized
|
|
127
|
+
|
|
128
|
+
This issue contradicts existing active specs: ${specs}. The \`change-of-mind\` label is present, so I'll proceed with refinement; the resulting spec will explicitly supersede those stories.
|
|
129
|
+
|
|
130
|
+
**Reasoning:** ${verdict.reasoning}
|
|
131
|
+
|
|
132
|
+
Proceeding with clarifying questions...`;
|
|
133
|
+
}
|
|
134
|
+
return `### slowcook · contradiction — refinement blocked
|
|
135
|
+
|
|
136
|
+
This issue contradicts existing active specs: ${specs}.
|
|
137
|
+
|
|
138
|
+
**Reasoning:** ${verdict.reasoning}
|
|
139
|
+
|
|
140
|
+
The ratchet means previously-accepted decisions stay in force unless explicitly revoked. To proceed:
|
|
141
|
+
|
|
142
|
+
- If this is a **genuine change of mind**, add the \`change-of-mind\` label and re-comment. I will then refine the spec with explicit \`supersedes\` tracking.
|
|
143
|
+
- If this is an **accidental conflict**, revise the issue body to align with the existing specs and remove the contradiction. I'll re-analyze on the next comment.
|
|
144
|
+
- If you intend the overlapping specs to **coexist** and think this isn't a real contradiction, add a comment explaining the independence and I'll re-classify.
|
|
145
|
+
|
|
146
|
+
I've added the \`blocked-contradiction\` label for visibility.`;
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=relationship.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relationship.js","sourceRoot":"","sources":["../../../src/commands/refine/relationship.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type Spec, type SpecIndex, type SpecIndexEntry } from "@slowcook-ai/core";
|
|
3
|
+
/** Where specs live, relative to repo root. */
|
|
4
|
+
export declare const SPECS_DIR = "specs";
|
|
5
|
+
export declare const INDEX_FILE: string;
|
|
6
|
+
export declare function readIndex(repoRoot: string): SpecIndex;
|
|
7
|
+
export declare function writeIndex(repoRoot: string, index: SpecIndex): void;
|
|
8
|
+
export declare function readSpec(repoRoot: string, storyId: string): Spec;
|
|
9
|
+
export declare function writeSpec(repoRoot: string, spec: Spec): string;
|
|
10
|
+
export declare function listActiveSpecs(repoRoot: string): Spec[];
|
|
11
|
+
/** Allocate the next unused story ID (zero-padded 3 digits). */
|
|
12
|
+
export declare function nextStoryId(repoRoot: string): string;
|
|
13
|
+
/** Public accessors for tests. */
|
|
14
|
+
export declare const schemas: {
|
|
15
|
+
SpecIndex: z.ZodObject<{
|
|
16
|
+
$schema: z.ZodOptional<z.ZodString>;
|
|
17
|
+
schema_version: z.ZodLiteral<1>;
|
|
18
|
+
stories: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
19
|
+
title: z.ZodString;
|
|
20
|
+
status: z.ZodEnum<["draft", "active", "superseded"]>;
|
|
21
|
+
source_issue: z.ZodOptional<z.ZodString>;
|
|
22
|
+
tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
23
|
+
summary: z.ZodOptional<z.ZodString>;
|
|
24
|
+
supersedes: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
25
|
+
superseded_by: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodNull]>>;
|
|
26
|
+
}, "strip", z.ZodTypeAny, {
|
|
27
|
+
status: "draft" | "active" | "superseded";
|
|
28
|
+
title: string;
|
|
29
|
+
source_issue?: string | undefined;
|
|
30
|
+
tags?: string[] | undefined;
|
|
31
|
+
summary?: string | undefined;
|
|
32
|
+
supersedes?: string[] | undefined;
|
|
33
|
+
superseded_by?: string | null | undefined;
|
|
34
|
+
}, {
|
|
35
|
+
status: "draft" | "active" | "superseded";
|
|
36
|
+
title: string;
|
|
37
|
+
source_issue?: string | undefined;
|
|
38
|
+
tags?: string[] | undefined;
|
|
39
|
+
summary?: string | undefined;
|
|
40
|
+
supersedes?: string[] | undefined;
|
|
41
|
+
superseded_by?: string | null | undefined;
|
|
42
|
+
}>>;
|
|
43
|
+
}, "strip", z.ZodTypeAny, {
|
|
44
|
+
schema_version: 1;
|
|
45
|
+
stories: Record<string, {
|
|
46
|
+
status: "draft" | "active" | "superseded";
|
|
47
|
+
title: string;
|
|
48
|
+
source_issue?: string | undefined;
|
|
49
|
+
tags?: string[] | undefined;
|
|
50
|
+
summary?: string | undefined;
|
|
51
|
+
supersedes?: string[] | undefined;
|
|
52
|
+
superseded_by?: string | null | undefined;
|
|
53
|
+
}>;
|
|
54
|
+
$schema?: string | undefined;
|
|
55
|
+
}, {
|
|
56
|
+
schema_version: 1;
|
|
57
|
+
stories: Record<string, {
|
|
58
|
+
status: "draft" | "active" | "superseded";
|
|
59
|
+
title: string;
|
|
60
|
+
source_issue?: string | undefined;
|
|
61
|
+
tags?: string[] | undefined;
|
|
62
|
+
summary?: string | undefined;
|
|
63
|
+
supersedes?: string[] | undefined;
|
|
64
|
+
superseded_by?: string | null | undefined;
|
|
65
|
+
}>;
|
|
66
|
+
$schema?: string | undefined;
|
|
67
|
+
}>;
|
|
68
|
+
Spec: z.ZodObject<{
|
|
69
|
+
$schema: z.ZodOptional<z.ZodString>;
|
|
70
|
+
story_id: z.ZodString;
|
|
71
|
+
title: z.ZodString;
|
|
72
|
+
status: z.ZodEnum<["draft", "active", "superseded"]>;
|
|
73
|
+
created_at: z.ZodString;
|
|
74
|
+
supersedes: z.ZodArray<z.ZodString, "many">;
|
|
75
|
+
superseded_by: z.ZodUnion<[z.ZodString, z.ZodNull]>;
|
|
76
|
+
token_budget_usd: z.ZodOptional<z.ZodNumber>;
|
|
77
|
+
estimate: z.ZodOptional<z.ZodEnum<["small", "medium", "large"]>>;
|
|
78
|
+
source_issue: z.ZodOptional<z.ZodString>;
|
|
79
|
+
refined_by: z.ZodOptional<z.ZodString>;
|
|
80
|
+
actors: z.ZodArray<z.ZodObject<{
|
|
81
|
+
name: z.ZodString;
|
|
82
|
+
notes: z.ZodOptional<z.ZodString>;
|
|
83
|
+
}, "strip", z.ZodTypeAny, {
|
|
84
|
+
name: string;
|
|
85
|
+
notes?: string | undefined;
|
|
86
|
+
}, {
|
|
87
|
+
name: string;
|
|
88
|
+
notes?: string | undefined;
|
|
89
|
+
}>, "many">;
|
|
90
|
+
preconditions: z.ZodArray<z.ZodString, "many">;
|
|
91
|
+
invariants: z.ZodArray<z.ZodString, "many">;
|
|
92
|
+
api_contract: z.ZodOptional<z.ZodArray<z.ZodUnknown, "many">>;
|
|
93
|
+
ui_behavior: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
94
|
+
acceptance_scenarios: z.ZodArray<z.ZodString, "many">;
|
|
95
|
+
non_goals: z.ZodArray<z.ZodString, "many">;
|
|
96
|
+
related_specs: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
97
|
+
id: z.ZodString;
|
|
98
|
+
relationship: z.ZodEnum<["overlap", "related", "superseded"]>;
|
|
99
|
+
note: z.ZodOptional<z.ZodString>;
|
|
100
|
+
}, "strip", z.ZodTypeAny, {
|
|
101
|
+
id: string;
|
|
102
|
+
relationship: "superseded" | "overlap" | "related";
|
|
103
|
+
note?: string | undefined;
|
|
104
|
+
}, {
|
|
105
|
+
id: string;
|
|
106
|
+
relationship: "superseded" | "overlap" | "related";
|
|
107
|
+
note?: string | undefined;
|
|
108
|
+
}>, "many">>;
|
|
109
|
+
}, "strip", z.ZodTypeAny, {
|
|
110
|
+
status: "draft" | "active" | "superseded";
|
|
111
|
+
title: string;
|
|
112
|
+
supersedes: string[];
|
|
113
|
+
superseded_by: string | null;
|
|
114
|
+
story_id: string;
|
|
115
|
+
created_at: string;
|
|
116
|
+
actors: {
|
|
117
|
+
name: string;
|
|
118
|
+
notes?: string | undefined;
|
|
119
|
+
}[];
|
|
120
|
+
preconditions: string[];
|
|
121
|
+
invariants: string[];
|
|
122
|
+
acceptance_scenarios: string[];
|
|
123
|
+
non_goals: string[];
|
|
124
|
+
$schema?: string | undefined;
|
|
125
|
+
source_issue?: string | undefined;
|
|
126
|
+
token_budget_usd?: number | undefined;
|
|
127
|
+
estimate?: "small" | "medium" | "large" | undefined;
|
|
128
|
+
refined_by?: string | undefined;
|
|
129
|
+
api_contract?: unknown[] | undefined;
|
|
130
|
+
ui_behavior?: Record<string, string> | undefined;
|
|
131
|
+
related_specs?: {
|
|
132
|
+
id: string;
|
|
133
|
+
relationship: "superseded" | "overlap" | "related";
|
|
134
|
+
note?: string | undefined;
|
|
135
|
+
}[] | undefined;
|
|
136
|
+
}, {
|
|
137
|
+
status: "draft" | "active" | "superseded";
|
|
138
|
+
title: string;
|
|
139
|
+
supersedes: string[];
|
|
140
|
+
superseded_by: string | null;
|
|
141
|
+
story_id: string;
|
|
142
|
+
created_at: string;
|
|
143
|
+
actors: {
|
|
144
|
+
name: string;
|
|
145
|
+
notes?: string | undefined;
|
|
146
|
+
}[];
|
|
147
|
+
preconditions: string[];
|
|
148
|
+
invariants: string[];
|
|
149
|
+
acceptance_scenarios: string[];
|
|
150
|
+
non_goals: string[];
|
|
151
|
+
$schema?: string | undefined;
|
|
152
|
+
source_issue?: string | undefined;
|
|
153
|
+
token_budget_usd?: number | undefined;
|
|
154
|
+
estimate?: "small" | "medium" | "large" | undefined;
|
|
155
|
+
refined_by?: string | undefined;
|
|
156
|
+
api_contract?: unknown[] | undefined;
|
|
157
|
+
ui_behavior?: Record<string, string> | undefined;
|
|
158
|
+
related_specs?: {
|
|
159
|
+
id: string;
|
|
160
|
+
relationship: "superseded" | "overlap" | "related";
|
|
161
|
+
note?: string | undefined;
|
|
162
|
+
}[] | undefined;
|
|
163
|
+
}>;
|
|
164
|
+
SpecIndexEntry: z.ZodObject<{
|
|
165
|
+
title: z.ZodString;
|
|
166
|
+
status: z.ZodEnum<["draft", "active", "superseded"]>;
|
|
167
|
+
source_issue: z.ZodOptional<z.ZodString>;
|
|
168
|
+
tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
169
|
+
summary: z.ZodOptional<z.ZodString>;
|
|
170
|
+
supersedes: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
171
|
+
superseded_by: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodNull]>>;
|
|
172
|
+
}, "strip", z.ZodTypeAny, {
|
|
173
|
+
status: "draft" | "active" | "superseded";
|
|
174
|
+
title: string;
|
|
175
|
+
source_issue?: string | undefined;
|
|
176
|
+
tags?: string[] | undefined;
|
|
177
|
+
summary?: string | undefined;
|
|
178
|
+
supersedes?: string[] | undefined;
|
|
179
|
+
superseded_by?: string | null | undefined;
|
|
180
|
+
}, {
|
|
181
|
+
status: "draft" | "active" | "superseded";
|
|
182
|
+
title: string;
|
|
183
|
+
source_issue?: string | undefined;
|
|
184
|
+
tags?: string[] | undefined;
|
|
185
|
+
summary?: string | undefined;
|
|
186
|
+
supersedes?: string[] | undefined;
|
|
187
|
+
superseded_by?: string | null | undefined;
|
|
188
|
+
}>;
|
|
189
|
+
};
|
|
190
|
+
/** Minimal helper for the agent to build a SpecIndexEntry from a Spec. */
|
|
191
|
+
export declare function entryFromSpec(spec: Spec): SpecIndexEntry;
|
|
192
|
+
//# sourceMappingURL=spec-yaml.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spec-yaml.d.ts","sourceRoot":"","sources":["../../../src/commands/refine/spec-yaml.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAEL,KAAK,IAAI,EACT,KAAK,SAAS,EACd,KAAK,cAAc,EACpB,MAAM,mBAAmB,CAAC;AAE3B,+CAA+C;AAC/C,eAAO,MAAM,SAAS,UAAU,CAAC;AACjC,eAAO,MAAM,UAAU,QAAiC,CAAC;AAkDzD,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CAWrD;AAED,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,IAAI,CAQnE;AAED,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAUhE;AAED,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,GAAG,MAAM,CAK9D;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,EAAE,CAiBxD;AAED,gEAAgE;AAChE,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAKpD;AAED,kCAAkC;AAClC,eAAO,MAAM,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAInB,CAAC;AAEF,0EAA0E;AAC1E,wBAAgB,aAAa,CAAC,IAAI,EAAE,IAAI,GAAG,cAAc,CASxD"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync, } from "node:fs";
|
|
2
|
+
import { join, dirname } from "node:path";
|
|
3
|
+
import YAML from "yaml";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { makeEmptyIndex, } from "@slowcook-ai/core";
|
|
6
|
+
/** Where specs live, relative to repo root. */
|
|
7
|
+
export const SPECS_DIR = "specs";
|
|
8
|
+
export const INDEX_FILE = join(SPECS_DIR, "_index.yaml");
|
|
9
|
+
const SpecStatusSchema = z.enum(["draft", "active", "superseded"]);
|
|
10
|
+
const SpecIndexEntrySchema = z.object({
|
|
11
|
+
title: z.string(),
|
|
12
|
+
status: SpecStatusSchema,
|
|
13
|
+
source_issue: z.string().optional(),
|
|
14
|
+
tags: z.array(z.string()).optional(),
|
|
15
|
+
summary: z.string().optional(),
|
|
16
|
+
supersedes: z.array(z.string()).optional(),
|
|
17
|
+
superseded_by: z.union([z.string(), z.null()]).optional(),
|
|
18
|
+
});
|
|
19
|
+
const SpecIndexSchema = z.object({
|
|
20
|
+
$schema: z.string().optional(),
|
|
21
|
+
schema_version: z.literal(1),
|
|
22
|
+
stories: z.record(z.string(), SpecIndexEntrySchema),
|
|
23
|
+
});
|
|
24
|
+
const SpecSchema = z.object({
|
|
25
|
+
$schema: z.string().optional(),
|
|
26
|
+
story_id: z.string(),
|
|
27
|
+
title: z.string(),
|
|
28
|
+
status: SpecStatusSchema,
|
|
29
|
+
created_at: z.string(),
|
|
30
|
+
supersedes: z.array(z.string()),
|
|
31
|
+
superseded_by: z.union([z.string(), z.null()]),
|
|
32
|
+
token_budget_usd: z.number().optional(),
|
|
33
|
+
estimate: z.enum(["small", "medium", "large"]).optional(),
|
|
34
|
+
source_issue: z.string().optional(),
|
|
35
|
+
refined_by: z.string().optional(),
|
|
36
|
+
actors: z.array(z.object({ name: z.string(), notes: z.string().optional() })),
|
|
37
|
+
preconditions: z.array(z.string()),
|
|
38
|
+
invariants: z.array(z.string()),
|
|
39
|
+
api_contract: z.array(z.unknown()).optional(),
|
|
40
|
+
ui_behavior: z.record(z.string(), z.string()).optional(),
|
|
41
|
+
acceptance_scenarios: z.array(z.string()),
|
|
42
|
+
non_goals: z.array(z.string()),
|
|
43
|
+
related_specs: z
|
|
44
|
+
.array(z.object({
|
|
45
|
+
id: z.string(),
|
|
46
|
+
relationship: z.enum(["overlap", "related", "superseded"]),
|
|
47
|
+
note: z.string().optional(),
|
|
48
|
+
}))
|
|
49
|
+
.optional(),
|
|
50
|
+
});
|
|
51
|
+
export function readIndex(repoRoot) {
|
|
52
|
+
const path = join(repoRoot, INDEX_FILE);
|
|
53
|
+
if (!existsSync(path))
|
|
54
|
+
return makeEmptyIndex();
|
|
55
|
+
const raw = YAML.parse(readFileSync(path, "utf8"));
|
|
56
|
+
const parsed = SpecIndexSchema.safeParse(raw);
|
|
57
|
+
if (!parsed.success) {
|
|
58
|
+
throw new Error(`Invalid ${INDEX_FILE}: ${parsed.error.issues.map((i) => i.message).join("; ")}`);
|
|
59
|
+
}
|
|
60
|
+
return parsed.data;
|
|
61
|
+
}
|
|
62
|
+
export function writeIndex(repoRoot, index) {
|
|
63
|
+
const path = join(repoRoot, INDEX_FILE);
|
|
64
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
65
|
+
writeFileSync(path, YAML.stringify(index, { lineWidth: 0 }), "utf8");
|
|
66
|
+
}
|
|
67
|
+
export function readSpec(repoRoot, storyId) {
|
|
68
|
+
const path = join(repoRoot, SPECS_DIR, `story-${storyId}.yaml`);
|
|
69
|
+
const raw = YAML.parse(readFileSync(path, "utf8"));
|
|
70
|
+
const parsed = SpecSchema.safeParse(raw);
|
|
71
|
+
if (!parsed.success) {
|
|
72
|
+
throw new Error(`Invalid spec at ${path}: ${parsed.error.issues.map((i) => i.message).join("; ")}`);
|
|
73
|
+
}
|
|
74
|
+
return parsed.data;
|
|
75
|
+
}
|
|
76
|
+
export function writeSpec(repoRoot, spec) {
|
|
77
|
+
const path = join(repoRoot, SPECS_DIR, `story-${spec.story_id}.yaml`);
|
|
78
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
79
|
+
writeFileSync(path, YAML.stringify(spec, { lineWidth: 0 }), "utf8");
|
|
80
|
+
return path;
|
|
81
|
+
}
|
|
82
|
+
export function listActiveSpecs(repoRoot) {
|
|
83
|
+
const dir = join(repoRoot, SPECS_DIR);
|
|
84
|
+
if (!existsSync(dir))
|
|
85
|
+
return [];
|
|
86
|
+
const files = readdirSync(dir).filter((f) => f.startsWith("story-") && f.endsWith(".yaml"));
|
|
87
|
+
const specs = [];
|
|
88
|
+
for (const f of files) {
|
|
89
|
+
const id = f.replace(/^story-/, "").replace(/\.yaml$/, "");
|
|
90
|
+
try {
|
|
91
|
+
const s = readSpec(repoRoot, id);
|
|
92
|
+
if (s.status === "active")
|
|
93
|
+
specs.push(s);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// ignore invalid specs; surface them elsewhere
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return specs;
|
|
100
|
+
}
|
|
101
|
+
/** Allocate the next unused story ID (zero-padded 3 digits). */
|
|
102
|
+
export function nextStoryId(repoRoot) {
|
|
103
|
+
const index = readIndex(repoRoot);
|
|
104
|
+
const existing = Object.keys(index.stories).map((id) => parseInt(id, 10)).filter((n) => !isNaN(n));
|
|
105
|
+
const max = existing.length > 0 ? Math.max(...existing) : 0;
|
|
106
|
+
return String(max + 1).padStart(3, "0");
|
|
107
|
+
}
|
|
108
|
+
/** Public accessors for tests. */
|
|
109
|
+
export const schemas = {
|
|
110
|
+
SpecIndex: SpecIndexSchema,
|
|
111
|
+
Spec: SpecSchema,
|
|
112
|
+
SpecIndexEntry: SpecIndexEntrySchema,
|
|
113
|
+
};
|
|
114
|
+
/** Minimal helper for the agent to build a SpecIndexEntry from a Spec. */
|
|
115
|
+
export function entryFromSpec(spec) {
|
|
116
|
+
return {
|
|
117
|
+
title: spec.title,
|
|
118
|
+
status: spec.status,
|
|
119
|
+
source_issue: spec.source_issue,
|
|
120
|
+
summary: spec.acceptance_scenarios[0]?.slice(0, 160),
|
|
121
|
+
supersedes: spec.supersedes,
|
|
122
|
+
superseded_by: spec.superseded_by,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=spec-yaml.js.map
|