murmur8 4.5.1 → 4.7.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.
Files changed (31) hide show
  1. package/.blueprint/features/feature_pipeline-telemetry/FEATURE_SPEC.md +297 -0
  2. package/.blueprint/features/feature_pipeline-telemetry/IMPLEMENTATION_PLAN.md +34 -0
  3. package/.blueprint/features/feature_pipeline-telemetry/handoff-alex.md +21 -0
  4. package/.blueprint/features/feature_pipeline-telemetry/handoff-cass.md +25 -0
  5. package/.blueprint/features/feature_pipeline-telemetry/handoff-nigel.md +20 -0
  6. package/.blueprint/features/feature_pipeline-telemetry/story-failed-queue-retry.md +53 -0
  7. package/.blueprint/features/feature_pipeline-telemetry/story-identifiers.md +47 -0
  8. package/.blueprint/features/feature_pipeline-telemetry/story-init-integration.md +48 -0
  9. package/.blueprint/features/feature_pipeline-telemetry/story-payload-send.md +54 -0
  10. package/.blueprint/features/feature_pipeline-telemetry/story-telemetry-activation.md +54 -0
  11. package/.blueprint/features/feature_pipeline-telemetry/story-telemetry-config-command.md +52 -0
  12. package/.blueprint/features/feature_refine-feature-skill/FEATURE_SPEC.md +180 -0
  13. package/.blueprint/features/feature_refine-feature-skill/IMPLEMENTATION_PLAN.md +47 -0
  14. package/.blueprint/features/feature_refine-feature-skill/handoff-alex.md +19 -0
  15. package/.blueprint/features/feature_refine-feature-skill/handoff-cass.md +26 -0
  16. package/.blueprint/features/feature_refine-feature-skill/handoff-nigel.md +30 -0
  17. package/.blueprint/features/feature_refine-feature-skill/story-codey-confirmation.md +41 -0
  18. package/.blueprint/features/feature_refine-feature-skill/story-conversation-approval.md +41 -0
  19. package/.blueprint/features/feature_refine-feature-skill/story-initiation.md +42 -0
  20. package/.blueprint/features/feature_refine-feature-skill/story-story-propagation.md +42 -0
  21. package/.blueprint/features/feature_refine-feature-skill/story-telemetry-lineage.md +47 -0
  22. package/.blueprint/features/feature_refine-feature-skill/story-test-propagation.md +42 -0
  23. package/README.md +98 -7
  24. package/bin/cli.js +2 -1
  25. package/package.json +1 -1
  26. package/src/commands/refine.js +37 -0
  27. package/src/commands/telemetry-config.js +16 -0
  28. package/src/index.js +38 -1
  29. package/src/init.js +5 -0
  30. package/src/refine.js +172 -0
  31. package/src/telemetry.js +198 -0
@@ -0,0 +1,42 @@
1
+ # Story: Test Propagation via Nigel
2
+
3
+ **As a** developer whose stories have been updated by Cass
4
+ **I want** Nigel to update only the affected test cases and produce a `test-changes.md` summary
5
+ **So that** the test suite reflects the refined spec without replacing tests that are still valid
6
+
7
+ ## Acceptance Criteria
8
+
9
+ ### AC-1: Nigel reads story-changes.md to scope its work
10
+ **Given** Cass has produced a `story-changes.md` (or Cass was skipped and Alex produced a spec diff)
11
+ **When** Nigel starts
12
+ **Then** Nigel reads `story-changes.md` (or the spec diff directly if Cass was skipped) before reading any test files
13
+
14
+ ### AC-2: Only affected test cases are modified
15
+ **Given** an existing test file with multiple test cases
16
+ **When** Nigel has identified which test cases correspond to changed stories or spec sections
17
+ **Then** Nigel modifies only those test cases; test cases unrelated to the diff are left unchanged
18
+
19
+ ### AC-3: Nigel produces a `test-changes.md` file
20
+ **Given** Nigel has completed updating tests
21
+ **When** the Nigel stage finishes
22
+ **Then** a `test-changes.md` file exists in the feature directory listing which test cases were added, modified, or removed and why
23
+
24
+ ### AC-4: New test cases are created for new acceptance criteria
25
+ **Given** a new acceptance criterion exists in an updated or new story
26
+ **When** Nigel processes the changes
27
+ **Then** Nigel creates a new test case covering that criterion and records it in `test-changes.md`
28
+
29
+ ### AC-5: Nigel works from spec diff when no stories exist
30
+ **Given** the original feature was technical and Cass was skipped
31
+ **When** Nigel runs
32
+ **Then** Nigel uses the before/after spec diff produced by Alex as the sole input for determining which tests to update
33
+
34
+ ### AC-6: Passing tests are not regressed
35
+ **Given** an existing test that covers behaviour not changed by the diff
36
+ **When** Nigel completes
37
+ **Then** that test case remains present and semantically equivalent to its pre-refinement version
38
+
39
+ ## Out of Scope
40
+ - Nigel running the tests (execution happens during the Codey stage)
41
+ - Nigel modifying story files
42
+ - Nigel engaging in conversation with the user about test scope
package/README.md CHANGED
@@ -1,19 +1,54 @@
1
1
  # murmur8
2
2
 
3
- A multi-agent workflow framework for automated feature development. Four specialised AI agents collaborate in sequence to take features from specification to implementation, with built-in feedback loops and self-improvement capabilities.
3
+ Most AI coding tools are a black box. You describe what you want, something happens, and code appears. If it's wrong, you describe it again and hope for better. There's no process, no trail, no shared understanding of why decisions were made.
4
4
 
5
- Like a murmuration of starlings, individual agents move together as one, each responding to its neighbours to create something greater than the sum of its parts.
5
+ murmur8 is different. It runs a structured, documented pipeline — the kind a good engineering team would run naturally. Each agent produces real, readable artefacts: a feature spec, user stories, a test plan, an implementation. You can read every one of them, understand the reasoning, and step in at any point. It's not magic. It's a repeatable process that happens to move very fast.
6
6
 
7
- # TLDR - Using murmur8
7
+ Like a murmuration of starlings, the agents move together — each one responding to what came before, building something greater than any of them could produce alone.
8
8
 
9
- ## Using murmur8 inside Claude Code or Copilot CLI
10
- Initialize with `npx murmur8 init`, then run `/implement-feature your-feature` in Claude Code or Copilot CLI. Four AI agents collaborate to turn your idea into tested, working code — from spec to implementation. Add up to 3 feature slugs and the murmuration magic will build them in paralell in an isolated git worktree.
9
+ ## The Workflow
11
10
 
12
- ## Using murmur8 outside of Claude Code or Copilot CLI
13
- Initialize with `npx murmur8 init`, then run `npx murmur8 murm feature-a feature-b` from your terminal. Each feature gets an isolated git worktree and runs its own pipeline. Successful features auto-merge to main. Use `--dry-run` to preview the plan first.
11
+ ### Start with a conversation
12
+
13
+ Every feature starts with intent. If you're setting up a new project, murmur8 will walk you through creating a system specification interactively — Alex asks questions, you answer, and together you produce a document that grounds everything that follows. If a feature spec doesn't exist yet, the same thing happens at the feature level.
14
+
15
+ You can also trigger this explicitly with `--interactive`. This is useful when an idea is still fuzzy. Rather than writing a spec yourself, you have a conversation with Alex until the shape of the feature becomes clear. The spec that comes out the other side is yours to review and approve before anything else runs.
16
+
17
+ ### The pipeline runs
18
+
19
+ Once there's a spec, the pipeline takes over. Alex hands off to Cass, who writes user stories with explicit acceptance criteria. Cass hands off to Nigel, who turns those stories into a test plan and executable tests. Nigel hands off to Codey, who implements until the tests pass.
20
+
21
+ At every handoff, the agent writes a summary of what it did, what it decided, and what the next agent needs to know. These aren't logs — they're readable documents. If something goes wrong, or you want to understand why a decision was made, you can read the trail. The spec, the stories, the test plan, the implementation plan: they all live in your repository alongside the code.
22
+
23
+ ### Refine it
24
+
25
+ The first run won't always land exactly right. Requirements shift, something was misunderstood, or the implementation reveals a gap in the spec. That's normal.
26
+
27
+ `/refine-feature` reopens the conversation. Alex reads what was built, you tell it what needs to change, and it proposes an updated spec diff for your approval. From there Cass updates only the affected stories, Nigel updates only the affected tests, and Codey reimplements. The pipeline pauses before Codey runs — you always see the full picture of what's changing before any code is touched.
28
+
29
+ Every refinement is linked to the run it came from, so the history of a feature — original intent, what changed, and why — is always traceable.
30
+
31
+ ## Quick Start
32
+
33
+ **Inside Claude Code or Copilot CLI:**
34
+ ```bash
35
+ npx murmur8 init
36
+ /implement-feature your-feature
37
+ ```
38
+
39
+ **From the terminal (parallel execution):**
40
+ ```bash
41
+ npx murmur8 init
42
+ npx murmur8 murm feature-a feature-b feature-c
43
+ ```
44
+
45
+ Add up to three slugs and each feature runs in an isolated git worktree simultaneously. Successful features auto-merge to main. Use `--dry-run` to preview the plan before committing.
14
46
 
15
47
  ## Upgrading to v4.0
16
48
 
49
+ <details>
50
+ <summary>Breaking changes and migration notes</summary>
51
+
17
52
  v4.0 completes the murmuration theming by renaming all parallel internals. Existing users should be aware of the following breaking changes.
18
53
 
19
54
  ### Breaking changes
@@ -32,6 +67,8 @@ Legacy on-disk files (`.claude/parallel-config.json`, `.claude/parallel-queue.js
32
67
 
33
68
  The CLI commands `parallel`, `murmuration`, and `parallel-config` continue to work as aliases for `murm` and `murm-config` respectively.
34
69
 
70
+ </details>
71
+
35
72
  ## Agents
36
73
 
37
74
  | Agent | Role |
@@ -148,6 +185,7 @@ This updates `.blueprint/agents/`, `.blueprint/templates/`, `.blueprint/ways_of_
148
185
  | `npx murmur8 feedback-config set <key> <value>` | Modify feedback settings |
149
186
  | `npx murmur8 murm-config` | View murmuration pipeline configuration |
150
187
  | `npx murmur8 murm-config set <key> <value>` | Modify murmuration settings |
188
+ | `npx murmur8 telemetry-config` | View telemetry configuration and failed queue depth |
151
189
 
152
190
  ## Skill usage
153
191
 
@@ -298,6 +336,7 @@ murmur8 includes these built-in modules for observability and self-improvement:
298
336
  | **stack** | Configurable tech stack detection and configuration |
299
337
  | **cost** | Token usage tracking and cost estimation per stage |
300
338
  | **diff-preview** | Pre-commit change review with user confirmation |
339
+ | **telemetry** | Opt-in audit layer — POSTs structured run data to a configured endpoint for corpus building and enterprise usage monitoring |
301
340
 
302
341
  ### How They Work Together
303
342
 
@@ -515,6 +554,58 @@ npx murmur8 cost-config reset
515
554
 
516
555
  Cost data is stored in `pipeline-history.json` alongside timing and feedback data.
517
556
 
557
+ ## Pipeline Telemetry
558
+
559
+ An opt-in audit and data collection layer. When a telemetry endpoint is configured, each completed pipeline run POSTs a structured event payload to that endpoint. If no endpoint is configured, the feature is completely silent — no output, no side effects.
560
+
561
+ ### Activation
562
+
563
+ The `murmur8 init` command creates a `.env` file at your project root with a commented-out telemetry template:
564
+
565
+ ```bash
566
+ # Remove # to enable
567
+ MURMUR8_TELEMETRY_URL=https://your-ingest-endpoint.com/events
568
+ MURMUR8_TELEMETRY_KEY=your-api-key # optional — sent as Authorization: Bearer
569
+ ```
570
+
571
+ Real environment variables take precedence over `.env`, making it straightforward to configure in CI/CD without storing credentials in files. `.env` is automatically added to `.gitignore` during init.
572
+
573
+ ### What gets sent
574
+
575
+ | Field | Description |
576
+ |-------|-------------|
577
+ | `runId` | UUID v4 unique to this pipeline execution |
578
+ | `featureId` | UUID stable across retries — written into FEATURE_SPEC.md frontmatter once by Alex |
579
+ | `identity` | Git user name/email, repo remote URL, org ID, murmur8 version |
580
+ | `run` | Slug, status, start/end timestamps, per-stage timings and statuses, feedback ratings |
581
+ | `artifacts` | Feature spec and story files, gzip + base64 encoded |
582
+
583
+ `featureId` enables cross-run correlation: all retries of the same feature share one `featureId` while each execution gets a unique `runId`. This lets you query "all runs ever attempted for this feature" and trace evolution over time.
584
+
585
+ ### Reliability
586
+
587
+ Sends are non-blocking and never interrupt the pipeline. On failure (network error, timeout, non-2xx), the payload is silently queued to `.claude/telemetry-failed.json` and retried at the start of the next run.
588
+
589
+ ### Viewing configuration
590
+
591
+ ```bash
592
+ npx murmur8 telemetry-config
593
+
594
+ # Example output:
595
+ # status: active
596
+ # url: https://your-endpoint.com/events
597
+ # api key: ****1234
598
+ # failed queue: 0 entries
599
+ ```
600
+
601
+ ### Enterprise use
602
+
603
+ murmur8 telemetry is designed for self-hosted enterprise endpoints — there is no central collection service. Each organisation configures its own ingest URL. This enables:
604
+
605
+ - **Usage monitoring** — who ran what pipeline, on which repo, when
606
+ - **Audit trail** — `runId` + `commitHash` + `gitUser` provides an immutable trace of AI-assisted code changes
607
+ - **Corpus building** — aggregate feature specs and stories across teams to analyse and improve pipeline quality
608
+
518
609
  ## Murmuration
519
610
 
520
611
  Run multiple feature pipelines simultaneously using git worktrees for isolation. Each feature gets its own worktree and branch, allowing true parallel development without conflicts.
package/bin/cli.js CHANGED
@@ -10,7 +10,8 @@ const command = args[0];
10
10
  const aliases = {
11
11
  'parallel': 'murm',
12
12
  'murmuration': 'murm',
13
- 'parallel-config': 'murm-config'
13
+ 'parallel-config': 'murm-config',
14
+ 'refine-feature': 'refine'
14
15
  };
15
16
 
16
17
  const resolvedCommand = aliases[command] || command;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "murmur8",
3
- "version": "4.5.1",
3
+ "version": "4.7.0",
4
4
  "description": "Multi-agent workflow framework for automated feature development",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -0,0 +1,37 @@
1
+ 'use strict';
2
+
3
+ const { loadRefinementContext, isPauseBypassable } = require('../refine');
4
+
5
+ const description = 'Refine an existing feature spec and propagate changes through stories, tests, and implementation';
6
+
7
+ async function run(argv) {
8
+ const { parseRefinementArgs } = require('../refine');
9
+ const { slug } = parseRefinementArgs(argv);
10
+
11
+ if (!slug) {
12
+ console.error('Usage: murmur8 refine-feature <slug>');
13
+ console.error('Example: murmur8 refine-feature user-auth');
14
+ process.exit(1);
15
+ }
16
+
17
+ console.log(`Loading refinement context for "${slug}"...`);
18
+
19
+ let ctx;
20
+ try {
21
+ ctx = await loadRefinementContext(slug, process.cwd());
22
+ } catch (err) {
23
+ console.error(`Error: ${err.message}`);
24
+ process.exit(1);
25
+ }
26
+
27
+ const storyCount = ctx.stories.length;
28
+ const lastStatus = ctx.lastRunStatus ? ` (last run: ${ctx.lastRunStatus})` : '';
29
+ console.log(`Feature: ${ctx.slug}${lastStatus}`);
30
+ console.log(`Stories: ${storyCount}`);
31
+ console.log(`Feature ID: ${ctx.featureId}`);
32
+ console.log('');
33
+ console.log('To refine this feature, use the /refine-feature skill in Claude Code:');
34
+ console.log(` /refine-feature "${slug}"`);
35
+ }
36
+
37
+ module.exports = { run, description };
@@ -0,0 +1,16 @@
1
+ /**
2
+ * telemetry-config command - View telemetry configuration and queue status
3
+ */
4
+ const { loadConfig, formatTelemetryConfig } = require('../telemetry');
5
+
6
+ const description = 'View telemetry configuration and failed-send queue status';
7
+
8
+ const QUEUE_PATH = '.claude/telemetry-failed.json';
9
+
10
+ async function run(_args) {
11
+ const config = loadConfig('.env');
12
+ const output = formatTelemetryConfig(config, QUEUE_PATH);
13
+ console.log(output);
14
+ }
15
+
16
+ module.exports = { run, description };
package/src/index.js CHANGED
@@ -1,4 +1,16 @@
1
1
  const { init } = require('./init');
2
+ const {
3
+ parseRefinementArgs,
4
+ loadRefinementContext,
5
+ applySpecDiff,
6
+ buildRefinementPayload,
7
+ linkParentRun,
8
+ isTechnicalFeature,
9
+ filterAffectedStories,
10
+ buildStoryChanges,
11
+ buildChangeSummary,
12
+ isPauseBypassable,
13
+ } = require('./refine');
2
14
  const { update } = require('./update');
3
15
  const { validate, formatOutput, checkNodeVersion } = require('./validate');
4
16
  const { recordHistory, displayHistory, showStats, clearHistory, storeStageFeedback, updateStage } = require('./history');
@@ -94,6 +106,9 @@ const {
94
106
  markWorktreeAborted,
95
107
  getPromptText
96
108
  } = require('./diff-preview');
109
+ const { loadConfig, generateRunId, ensureFeatureId, buildPayload, compressArtifact,
110
+ enqueueFailure, retryQueue, ensureDotenv, ensureGitignore, formatTelemetryConfig
111
+ } = require('./telemetry');
97
112
  const tools = require('./tools');
98
113
  const theme = require('./theme');
99
114
 
@@ -159,6 +174,17 @@ module.exports = {
159
174
  setStackConfigValue,
160
175
  detectStackConfig,
161
176
  displayStackConfig,
177
+ // Telemetry module exports
178
+ loadConfig,
179
+ generateRunId,
180
+ ensureFeatureId,
181
+ buildPayload,
182
+ compressArtifact,
183
+ enqueueFailure,
184
+ retryQueue,
185
+ ensureDotenv,
186
+ ensureGitignore,
187
+ formatTelemetryConfig,
162
188
  // Tools module (model native features)
163
189
  tools,
164
190
  // Theme module (murmuration visual theming)
@@ -195,5 +221,16 @@ module.exports = {
195
221
  SESSION_STATES,
196
222
  SECTION_ORDER,
197
223
  MIN_REQUIRED_SECTIONS,
198
- SYSTEM_SPEC_QUESTIONS
224
+ SYSTEM_SPEC_QUESTIONS,
225
+ // Refine module exports
226
+ parseRefinementArgs,
227
+ loadRefinementContext,
228
+ applySpecDiff,
229
+ buildRefinementPayload,
230
+ linkParentRun,
231
+ isTechnicalFeature,
232
+ filterAffectedStories,
233
+ buildStoryChanges,
234
+ buildChangeSummary,
235
+ isPauseBypassable,
199
236
  };
package/src/init.js CHANGED
@@ -3,6 +3,7 @@ const path = require('path');
3
3
 
4
4
  const { detectStackConfig, writeStackConfig, CONFIG_FILE: STACK_CONFIG_FILE } = require('./stack');
5
5
  const { prompt } = require('./utils');
6
+ const { ensureDotenv, ensureGitignore } = require('./telemetry');
6
7
 
7
8
  const PACKAGE_ROOT = path.resolve(__dirname, '..');
8
9
  const TARGET_DIR = process.cwd();
@@ -176,6 +177,10 @@ async function init() {
176
177
  console.log('Stack config already exists, skipping detection');
177
178
  }
178
179
 
180
+ // Ensure .env template and .gitignore entry for telemetry
181
+ ensureDotenv(TARGET_DIR);
182
+ ensureGitignore(TARGET_DIR);
183
+
179
184
  console.log(`
180
185
  murmur8 initialized successfully!
181
186
 
package/src/refine.js ADDED
@@ -0,0 +1,172 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const crypto = require('crypto');
6
+
7
+ function parseRefinementArgs(argv) {
8
+ const slug = argv[3] || null;
9
+ return { slug };
10
+ }
11
+
12
+ function _extractFeatureId(content) {
13
+ const match = content.match(/^---[\s\S]*?featureId:\s*([^\s\n]+)[\s\S]*?---/m);
14
+ return match ? match[1] : null;
15
+ }
16
+
17
+ function _writeFeatureId(specPath, content, featureId) {
18
+ let updated;
19
+ if (content.startsWith('---')) {
20
+ const endIdx = content.indexOf('---', 3);
21
+ if (endIdx !== -1) {
22
+ const frontmatter = content.slice(3, endIdx);
23
+ if (frontmatter.includes('featureId:')) {
24
+ updated = content.replace(/featureId:\s*[^\s\n]+/, `featureId: ${featureId}`);
25
+ } else {
26
+ updated = `---${frontmatter}featureId: ${featureId}\n${content.slice(endIdx)}`;
27
+ }
28
+ } else {
29
+ updated = `---\nfeatureId: ${featureId}\n---\n${content}`;
30
+ }
31
+ } else {
32
+ updated = `---\nfeatureId: ${featureId}\n---\n${content}`;
33
+ }
34
+ fs.writeFileSync(specPath, updated, 'utf8');
35
+ return updated;
36
+ }
37
+
38
+ async function loadRefinementContext(slug, baseDir) {
39
+ const base = baseDir || process.cwd();
40
+ const featDir = path.join(base, '.blueprint', 'features', `feature_${slug}`);
41
+ const specPath = path.join(featDir, 'FEATURE_SPEC.md');
42
+
43
+ if (!fs.existsSync(specPath)) {
44
+ throw new Error(`FEATURE_SPEC.md not found for slug "${slug}" — run /implement-feature first`);
45
+ }
46
+
47
+ let specContent = fs.readFileSync(specPath, 'utf8');
48
+ let featureId = _extractFeatureId(specContent);
49
+ if (!featureId) {
50
+ featureId = crypto.randomUUID();
51
+ specContent = _writeFeatureId(specPath, specContent, featureId);
52
+ }
53
+
54
+ const storyFiles = fs.existsSync(featDir)
55
+ ? fs.readdirSync(featDir).filter(f => f.startsWith('story-') && f.endsWith('.md'))
56
+ : [];
57
+
58
+ const stories = storyFiles.map(f => ({
59
+ file: f,
60
+ content: fs.readFileSync(path.join(featDir, f), 'utf8'),
61
+ }));
62
+
63
+ const historyPath = path.join(base, '.claude', 'pipeline-history.json');
64
+ let history = [];
65
+ if (fs.existsSync(historyPath)) {
66
+ try {
67
+ history = JSON.parse(fs.readFileSync(historyPath, 'utf8'));
68
+ } catch (_) {
69
+ history = [];
70
+ }
71
+ }
72
+
73
+ const lastRun = history.filter(e => e.slug === slug).sort((a, b) =>
74
+ (b.completedAt || '').localeCompare(a.completedAt || '')
75
+ )[0];
76
+
77
+ return {
78
+ slug,
79
+ featureId,
80
+ spec: specContent,
81
+ stories,
82
+ history: history.filter(e => e.slug === slug),
83
+ lastRunStatus: lastRun ? lastRun.status : null,
84
+ featureName: slug,
85
+ };
86
+ }
87
+
88
+ async function applySpecDiff(specPath, newContent, featureId) {
89
+ if (newContent === null || newContent === undefined) {
90
+ throw new Error('abort: null diff provided — no changes applied');
91
+ }
92
+ _writeFeatureId(specPath, newContent, featureId);
93
+ }
94
+
95
+ function buildRefinementPayload(opts) {
96
+ const {
97
+ slug,
98
+ featureId,
99
+ storyChangesPath = null,
100
+ testChangesPath = null,
101
+ specDiff = null,
102
+ noCommit = false,
103
+ } = opts || {};
104
+
105
+ return {
106
+ slug,
107
+ featureId,
108
+ storyChangesPath: storyChangesPath || null,
109
+ testChangesPath: testChangesPath || null,
110
+ specDiff: specDiff || null,
111
+ commitSkipped: Boolean(noCommit),
112
+ };
113
+ }
114
+
115
+ function linkParentRun(slug, history) {
116
+ const slugRuns = (history || [])
117
+ .filter(e => e.slug === slug)
118
+ .sort((a, b) => (b.completedAt || '').localeCompare(a.completedAt || ''));
119
+
120
+ const latest = slugRuns[0] || null;
121
+
122
+ return {
123
+ parentRunId: latest ? latest.runId : null,
124
+ type: 'refinement',
125
+ featureId: latest ? latest.featureId : null,
126
+ };
127
+ }
128
+
129
+ function isTechnicalFeature(stories) {
130
+ return stories.length === 0;
131
+ }
132
+
133
+ function filterAffectedStories(stories, changedSlugs) {
134
+ return stories.filter(f => {
135
+ const name = path.basename(f, '.md').replace(/^story-/, '');
136
+ return changedSlugs.some(s => name === s || f.includes(s));
137
+ });
138
+ }
139
+
140
+ function buildStoryChanges(entries) {
141
+ return (entries || []).map(e => ({
142
+ file: e.file,
143
+ reason: e.reason,
144
+ isNew: Boolean(e.isNew),
145
+ }));
146
+ }
147
+
148
+ function buildChangeSummary(opts) {
149
+ const { specPath, affectedStories, testChangesPath } = opts || {};
150
+ return {
151
+ specPath: specPath || null,
152
+ affectedStories: Array.isArray(affectedStories) ? affectedStories : [],
153
+ testChangesPath: testChangesPath || null,
154
+ };
155
+ }
156
+
157
+ function isPauseBypassable(_flags) {
158
+ return false;
159
+ }
160
+
161
+ module.exports = {
162
+ parseRefinementArgs,
163
+ loadRefinementContext,
164
+ applySpecDiff,
165
+ buildRefinementPayload,
166
+ linkParentRun,
167
+ isTechnicalFeature,
168
+ filterAffectedStories,
169
+ buildStoryChanges,
170
+ buildChangeSummary,
171
+ isPauseBypassable,
172
+ };