From b914fe31d7ce671b539907f91a24f8448310c20c Mon Sep 17 00:00:00 2001
From: Nixon Cheaz <6854716+ncheaz@users.noreply.github.com>
Date: Wed, 22 Apr 2026 02:53:51 -0400
Subject: [PATCH 1/2] Remove duplicated OpenSpec artifact and tasks injection
from prompt template
Eliminate redundant re-read instructions and {{tasks}} placeholder; rely on
{{base_prompt}} as the single OpenSpec surface and {{task_context}} for the
active task to reduce prompt bloat and context duplication per iteration.
---
scripts/ralph-run.sh | 13 -------
tests/helpers/test-functions.sh | 13 -------
tests/integration/test-complex-workflow.bats | 4 --
tests/integration/test-simple-workflow.bats | 4 --
.../bash/test-create-prompt-template.bats | 39 +++++++++++++++++--
5 files changed, 35 insertions(+), 38 deletions(-)
diff --git a/scripts/ralph-run.sh b/scripts/ralph-run.sh
index 5291fda..016a95f 100755
--- a/scripts/ralph-run.sh
+++ b/scripts/ralph-run.sh
@@ -750,21 +750,10 @@ create_prompt_template() {
Change directory: {{change_dir}}
-## OpenSpec Artifacts Context
-
-Include full context from openspec artifacts in {{change_dir}}:
-- Read {{change_dir}}/proposal.md for the overall project goal
-- Read {{change_dir}}/design.md for the technical design approach
-- Read {{change_dir}}/specs/*/spec.md for the detailed specifications
-
## Invocation-Time PRD Snapshot
{{base_prompt}}
-## Task List
-
-{{tasks}}
-
## Fresh Task Context
{{task_context}}
@@ -777,7 +766,6 @@ Include full context from openspec artifacts in {{change_dir}}:
- Mark the task as [/] in the tasks file before starting work
2. **Implement** the current task directly:
- - Read the relevant OpenSpec artifacts for context (proposal.md, design.md, specs)
- Make the smallest maintainable change that fully satisfies the current task
- Run the most relevant validation or tests for the task before claiming completion
@@ -793,7 +781,6 @@ Include full context from openspec artifacts in {{change_dir}}:
## Critical Rules
- Work on ONE task at a time from the task list
-- Read the full tasks file every iteration; do not rely on memory from prior iterations
- Do not rely on editor-specific slash commands or local-only skills; follow this prompt directly
- Treat tasks.md as the only source of truth for task state
- ONLY output `{{task_promise}} ` when the current task is complete and marked as [x]
diff --git a/tests/helpers/test-functions.sh b/tests/helpers/test-functions.sh
index 98608f5..5a6e025 100644
--- a/tests/helpers/test-functions.sh
+++ b/tests/helpers/test-functions.sh
@@ -718,21 +718,10 @@ create_prompt_template() {
Change directory: {{change_dir}}
-## OpenSpec Artifacts Context
-
-Include full context from openspec artifacts in {{change_dir}}:
-- Read {{change_dir}}/proposal.md for the overall project goal
-- Read {{change_dir}}/design.md for the technical design approach
-- Read {{change_dir}}/specs/*/spec.md for the detailed specifications
-
## Invocation-Time PRD Snapshot
{{base_prompt}}
-## Task List
-
-{{tasks}}
-
## Fresh Task Context
{{task_context}}
@@ -745,7 +734,6 @@ Include full context from openspec artifacts in {{change_dir}}:
- Mark the task as [/] in the tasks file before starting work
2. **Implement** the current task directly:
- - Read the relevant OpenSpec artifacts for context (proposal.md, design.md, specs)
- Make the smallest maintainable change that fully satisfies the current task
- Run the most relevant validation or tests for the task before claiming completion
@@ -761,7 +749,6 @@ Include full context from openspec artifacts in {{change_dir}}:
## Critical Rules
- Work on ONE task at a time from the task list
-- Read the full tasks file every iteration; do not rely on memory from prior iterations
- Do not rely on editor-specific slash commands or local-only skills; follow this prompt directly
- Treat tasks.md as the only source of truth for task state
- ONLY output `{{task_promise}} ` when the current task is complete and marked as [x]
diff --git a/tests/integration/test-complex-workflow.bats b/tests/integration/test-complex-workflow.bats
index 34a4248..e12992e 100644
--- a/tests/integration/test-complex-workflow.bats
+++ b/tests/integration/test-complex-workflow.bats
@@ -99,10 +99,6 @@ teardown() {
if [ -d "$ralph_dir" ]; then
[ -f "$ralph_dir/PRD.md" ] || true
-
- if [ -f "$ralph_dir/PRD.md" ]; then
- grep -q "## OpenSpec Artifacts Context" "$ralph_dir/PRD.md" || true
- fi
fi
}
diff --git a/tests/integration/test-simple-workflow.bats b/tests/integration/test-simple-workflow.bats
index 7b46e27..d78cc5d 100644
--- a/tests/integration/test-simple-workflow.bats
+++ b/tests/integration/test-simple-workflow.bats
@@ -99,10 +99,6 @@ teardown() {
if [ -d "$ralph_dir" ]; then
[ -f "$ralph_dir/PRD.md" ] || true
-
- if [ -f "$ralph_dir/PRD.md" ]; then
- grep -q "## OpenSpec Artifacts Context" "$ralph_dir/PRD.md" || true
- fi
fi
}
diff --git a/tests/unit/bash/test-create-prompt-template.bats b/tests/unit/bash/test-create-prompt-template.bats
index 7f70a91..ea89430 100644
--- a/tests/unit/bash/test-create-prompt-template.bats
+++ b/tests/unit/bash/test-create-prompt-template.bats
@@ -75,7 +75,7 @@ teardown() {
rm -rf "$test_dir"
}
-@test "create_prompt_template: includes task list placeholder" {
+@test "create_prompt_template: does not include raw task list placeholder" {
local test_dir
test_dir=$(setup_test_dir)
cd "$test_dir" || return 1
@@ -89,7 +89,7 @@ teardown() {
[ "$status" -eq 0 ]
- grep -q "{{tasks}}" "$template_file"
+ ! grep -q "{{tasks}}" "$template_file"
cd - > /dev/null
rm -rf "$test_dir"
@@ -156,7 +156,7 @@ teardown() {
rm -rf "$test_dir"
}
-@test "create_prompt_template: includes OpenSpec artifacts context section" {
+@test "create_prompt_template: does not include OpenSpec artifacts context section" {
local test_dir
test_dir=$(setup_test_dir)
cd "$test_dir" || return 1
@@ -170,7 +170,7 @@ teardown() {
[ "$status" -eq 0 ]
- grep -q "## OpenSpec Artifacts Context" "$template_file"
+ ! grep -q "## OpenSpec Artifacts Context" "$template_file"
cd - > /dev/null
rm -rf "$test_dir"
@@ -432,6 +432,37 @@ teardown() {
rm -rf "$test_dir"
}
+@test "create_prompt_template: template does not duplicate OpenSpec artifacts or tasks content" {
+ local test_dir
+ test_dir=$(setup_test_dir)
+ cd "$test_dir" || return 1
+
+ local change_dir="$test_dir/openspec/changes/test-change"
+ local template_file="$test_dir/template.txt"
+
+ mkdir -p "$change_dir/specs"
+
+ run create_prompt_template "$change_dir" "$template_file"
+
+ [ "$status" -eq 0 ]
+
+ # Must NOT contain re-read instructions — artifacts are already inlined via {{base_prompt}}
+ ! grep -q "Read the relevant OpenSpec artifacts" "$template_file"
+ # Must NOT contain the critical-rule that told the agent to re-read tasks every iteration
+ ! grep -q "Read the full tasks file every iteration" "$template_file"
+ # Must NOT contain a raw {{tasks}} dump
+ ! grep -q "{{tasks}}" "$template_file"
+ # Must NOT contain the separate "OpenSpec Artifacts Context" section
+ ! grep -q "## OpenSpec Artifacts Context" "$template_file"
+ # MUST contain the single OpenSpec source surface
+ grep -q "{{base_prompt}}" "$template_file"
+ # MUST contain the single tasks surface
+ grep -q "{{task_context}}" "$template_file"
+
+ cd - > /dev/null
+ rm -rf "$test_dir"
+}
+
@test "create_prompt_template: template file is readable" {
local test_dir
test_dir=$(setup_test_dir)
From f1c9da31a8ebc6c5b52baf6746efa8b179570782 Mon Sep 17 00:00:00 2001
From: Nixon Cheaz <6854716+ncheaz@users.noreply.github.com>
Date: Wed, 22 Apr 2026 04:02:57 -0400
Subject: [PATCH 2/2] Optmization of feeders to context
---
OPENSPEC-RALPH-BP.md | 6 +-
RALPH-METHODOLOGY-ASSESSMENT.md | 38 +-
lib/mini-ralph/runner.js | 97 ++-
lib/mini-ralph/tasks.js | 15 +-
scripts/ralph-run.sh | 95 +--
tests/helpers/test-functions.sh | 86 ++-
tests/integration/test-complex-workflow.bats | 18 +
tests/integration/test-simple-workflow.bats | 18 +
.../bash/test-create-prompt-template.bats | 186 +++++-
tests/unit/bash/test-generate-prd.bats | 50 +-
.../bash/test-prd-content-validation.bats | 33 +
.../bash/test-prd-omits-task-context.bats | 369 ++++++++++++
.../bash/test-prd-task-context-injection.bats | 562 ------------------
.../unit/javascript/mini-ralph-prompt.test.js | 5 +-
.../unit/javascript/mini-ralph-runner.test.js | 123 +++-
.../unit/javascript/mini-ralph-status.test.js | 7 +-
.../unit/javascript/mini-ralph-tasks.test.js | 59 +-
17 files changed, 1024 insertions(+), 743 deletions(-)
create mode 100644 tests/unit/bash/test-prd-omits-task-context.bats
delete mode 100644 tests/unit/bash/test-prd-task-context-injection.bats
diff --git a/OPENSPEC-RALPH-BP.md b/OPENSPEC-RALPH-BP.md
index ff53e4c..78c5990 100644
--- a/OPENSPEC-RALPH-BP.md
+++ b/OPENSPEC-RALPH-BP.md
@@ -343,13 +343,13 @@ Authoring rules:
- **Resolve or explicitly defer policy before writing tasks.** Phrases like "may be shared or tenant-specific," "one option is," or "could support later" are fine while exploring; they are blockers once the loop starts. Resolve algorithms, fallback behavior, retention math, config shape, failure taxonomy, and compatibility-window behavior in `design.md`.
- **Specs must be deterministic.** If two good implementers could read the spec and make materially different choices, the spec is not loop-safe yet.
- **If a dedicated coverage artifact exists** (such as a `figma-route-map.md`), route and shared-surface tasks should reuse it as the durable source of truth instead of rediscovering coverage each iteration.
-- **Run with full OpenSpec context when available.** Repo guidance favors `./scripts/ralph-run.sh tasks ` over raw `tasks.md` mode because `opsx-apply` reloads proposal, design, specs, and tasks each iteration. If you run raw `prd.json` or raw `tasks.md` mode, push more detail down into each item because the companion docs will not be reloaded.
+- **Run with full OpenSpec context when available.** Repo guidance favors `./scripts/ralph-run.sh tasks ` over raw `tasks.md` mode because `opsx-apply` provides the agent with a manifest of OpenSpec artifact paths (`## OpenSpec Artifacts`) so the agent can read proposal, design, and specs as needed each iteration. If you run raw `prd.json` or raw `tasks.md` mode, push more detail down into each item because the companion docs will not be listed in the manifest.
### Loop-prompt / wrapper instructions
At minimum, the loop prompt must tell the agent to:
-- Read `proposal.md`, `design.md`, `specs/**`, and `tasks.md` at the start of every iteration.
+- Read the OpenSpec artifacts listed in `## OpenSpec Artifacts` (proposal, design, specs) before implementing the current task.
- Inspect prior iteration state before starting new work.
- Implement exactly one task per iteration.
- Run the exact validators relevant to that task.
@@ -508,7 +508,7 @@ The `tenant-scoped-content-versioning` example and subsequent reviews produced t
4. **Every wide task needs explicit "done when" signals.** Verbs like `ensure`, `validate`, `keep`, or `support` are too soft on their own.
-5. **Full OpenSpec context is better than raw task-file mode.** Repo guidance favors `./scripts/ralph-run.sh tasks ` over raw `tasks.md` mode because `opsx-apply` reloads proposal, design, specs, and tasks. A task list can be shorter when the design/specs fully resolve tricky decisions, but only if the loop actually reloads those artifacts each iteration.
+5. **Full OpenSpec context is better than raw task-file mode.** Repo guidance favors `./scripts/ralph-run.sh tasks ` over raw `tasks.md` mode because `opsx-apply` provides a manifest (`## OpenSpec Artifacts`) listing artifact paths so the agent can read proposal, design, and specs as needed. A task list can be shorter when the design/specs fully resolve tricky decisions, but only if the loop actually references those artifacts each iteration.
6. **"Done when" gates are hard stops, not soft guidelines.** The most common single-task quality failure is a loop marking a task complete after a `Done when` check failed, with a rationalization note. The gate is self-authorizing; the loop decides the gate does not apply, bypasses it, and moves on, recording a completion claim the stated verifier never confirmed.
diff --git a/RALPH-METHODOLOGY-ASSESSMENT.md b/RALPH-METHODOLOGY-ASSESSMENT.md
index e495b01..65eab89 100644
--- a/RALPH-METHODOLOGY-ASSESSMENT.md
+++ b/RALPH-METHODOLOGY-ASSESSMENT.md
@@ -60,7 +60,7 @@ OpenSpec specs → docs (README/QUICKSTART/BOTW) → archived artifacts.
| P2 | Iterative loop with limits | verified | high |
| P3 | tasks.md as single source of truth | verified | high |
| P4 | Symlink architecture for task sharing | verified | high |
-| P5 | Fresh context per iteration (PRD snapshot + live task context)| verified | high |
+| P5 | Fresh context per iteration (manifest-style OpenSpec Artifacts + bounded task context)| verified | high |
| P6 | Iteration numbering aligned with tasks| partially-verified | medium |
| P7 | Structured git commit format | verified | high |
| P8 | Auto-resume on restart | verified | high |
@@ -137,18 +137,23 @@ file state simultaneously" confirms the shared-access invariant holds at
runtime. `tests/integration/test-symlink-macos.bats` provides platform-specific
end-to-end coverage.
-#### P5 — Fresh context per iteration (PRD snapshot + live task context)
+#### P5 — Fresh context per iteration (manifest-style OpenSpec Artifacts + bounded task context)
`lib/mini-ralph/runner.js:95` calls `prompt.render(options, iterationCount)`
-inside the loop on every iteration. `lib/mini-ralph/prompt.js:82-89` reads
-`tasksFile` content fresh on every call and exposes the loop-start prompt body as
-`{{base_prompt}}`; `lib/mini-ralph/tasks.js:152-180` — `taskContext()` always
-reads live `tasks.md`. The bash side generates the PRD once at loop start in
-`scripts/ralph-run.sh:968-979`, then reuses it for the rest of the run.
+inside the loop on every iteration. The iteration prompt uses a manifest shape:
+`scripts/ralph-run.sh:create_prompt_template()` writes a `## OpenSpec Artifacts`
+section that lists artifact file paths (proposal, design, specs, plus
+`.ralph/PRD.md` as a convenience copy), and when a repo-root `AGENTS.md` is
+present, includes it in the same manifest. The task-context surface is bounded to
+`## Current Task` + `## Progress: N of M tasks complete` via
+`lib/mini-ralph/tasks.js:taskContext()`. The bash side generates `.ralph/PRD.md`
+once at loop start in `scripts/ralph-run.sh`, then reuses it for the rest of the
+run as a pre-concatenated convenience copy of the artifacts.
`tests/unit/javascript/mini-ralph-prompt.test.js:149` — "injects fresh
task_context when tasksFile is present" and
-`tests/unit/bash/test-prd-task-context-injection.bats` confirm that each
-iteration receives up-to-date task state with no stale context carry-over.
+`tests/unit/bash/test-prd-omits-task-context.bats` confirm that the PRD does not
+carry task-context injection, and that each iteration receives only the bounded
+current-task and progress context with no stale carry-over.
#### P7 — Structured git commit format with task numbers
@@ -449,17 +454,20 @@ same file.
---
-### P5 — Fresh context per iteration via PRD snapshot + live task context
+### P5 — Fresh context per iteration via manifest-style OpenSpec Artifacts + bounded task context
**Full claim:** The loop re-renders prompt context every iteration from a
-loop-start PRD snapshot plus live `tasks.md`, current-task context, recent loop
-signals, and pending injected context.
+manifest that lists OpenSpec artifact paths (agent reads them as needed), a
+bounded task-context surface (`## Current Task` + `## Progress`), recent loop
+signals, and pending injected context. `.ralph/PRD.md` is still generated once
+at loop start as a pre-concatenated convenience copy of proposal/specs/design.
+When a repo-root `AGENTS.md` is present it is surfaced in the same manifest.
| Field | Value |
|-------|-------|
-| Verdict | `verified` — `prompt.render()` is called inside the runner loop on every iteration and reads live `tasks.md` content each time, while `PRD.md` is generated once at loop start and then reused. Confirmed by unit tests for prompt rendering and PRD generation. Confidence: **high**. |
-| Implementation evidence | `scripts/ralph-run.sh:404-444` — `generate_prd()` reads proposal, specs, and design and writes `$ralph_dir/PRD.md`; `ralph-run.sh:968-979` — PRD is generated before the loop starts; `lib/mini-ralph/prompt.js:83-107` — `render()` reads `tasksFile` content and `taskContext` fresh on every iteration call, exposes `{{base_prompt}}`, and injects commit-contract text; `lib/mini-ralph/tasks.js:152-180` — `taskContext()` always reads live `tasks.md`; `lib/mini-ralph/runner.js:95` — `prompt.render(options, iterationCount)` called inside the while loop |
-| Test evidence | `tests/unit/javascript/mini-ralph-prompt.test.js` — `render()` suite (lines 104–217): `renders template with iteration variables` (line 110), `injects tasks content when tasksFile is present` (line 131), `injects fresh task_context when tasksFile is present` (line 149); `tests/unit/bash/test-generate-prd.bats` — `generate_prd: generates PRD with all required sections` (line 16), `generate_prd: includes current task context when available` (line 162), `generate_prd: includes completed tasks in context` (line 377); `tests/unit/bash/test-prd-task-context-injection.bats` — validates task context is injected per-call |
+| Verdict | `verified` — `prompt.render()` is called inside the runner loop on every iteration and reads live `tasks.md` content each time; the iteration prompt lists OpenSpec artifact paths in `## OpenSpec Artifacts` rather than inlining their content; `taskContext()` emits only current-task + progress. Confirmed by unit tests for prompt rendering, PRD generation, and task-context shape. Confidence: **high**. |
+| Implementation evidence | `scripts/ralph-run.sh:create_prompt_template()` — manifest heredoc lists artifact paths under `## OpenSpec Artifacts` and probes `AGENTS.md` via `probe_agents_md()`; `ralph-run.sh:generate_prd()` — generates `.ralph/PRD.md` before the loop as a convenience copy, no task-context appended; `lib/mini-ralph/tasks.js:taskContext()` — emits `## Current Task` + `## Progress: N of M tasks complete` only; `lib/mini-ralph/runner.js:95` — `prompt.render(options, iterationCount)` called inside the while loop |
+| Test evidence | `tests/unit/javascript/mini-ralph-tasks.test.js` — `taskContext()` suite: `returns current task heading` (bounded shape); `tests/unit/bash/test-create-prompt-template.bats` — `includes OpenSpec Artifacts manifest section`, `AGENTS.md present adds entry to manifest`; `tests/unit/bash/test-prd-omits-task-context.bats` — asserts PRD does NOT contain `## Current Task Context` or `## Completed Tasks for Git Commit`; `tests/unit/bash/test-generate-prd.bats` — `does not include current task context section` |
---
diff --git a/lib/mini-ralph/runner.js b/lib/mini-ralph/runner.js
index a2fbf80..765de28 100644
--- a/lib/mini-ralph/runner.js
+++ b/lib/mini-ralph/runner.js
@@ -557,12 +557,48 @@ function _formatAutoCommitMessage(iteration, completedTasks) {
* @param {Array} recentHistory
* @returns {string}
*/
+function _firstNonEmptyLine(text, limit) {
+ if (!text) return '';
+ const lines = text.split('\n');
+ for (const line of lines) {
+ const trimmed = line.trim();
+ if (trimmed.length > 0) {
+ return trimmed.slice(0, limit);
+ }
+ }
+ return '';
+}
+
+function _failureFingerprint(entry, errorEntries) {
+ let stderrHead = '';
+ if (errorEntries) {
+ const match = errors.matchIteration(errorEntries, entry.iteration);
+ stderrHead = _firstNonEmptyLine(match && match.stderr, 120);
+ }
+ return JSON.stringify({
+ failureStage: entry.failureStage || '',
+ exitCode: entry.exitCode,
+ stderrHead,
+ });
+}
+
+function _isEmptyFingerprint(fingerprint) {
+ try {
+ const obj = JSON.parse(fingerprint);
+ return !obj.failureStage && obj.exitCode === 0 && !obj.stderrHead;
+ } catch {
+ return false;
+ }
+}
+
function _buildIterationFeedback(recentHistory, errorEntries) {
if (!Array.isArray(recentHistory) || recentHistory.length === 0) {
return '';
}
const problemLines = [];
+ // Track fingerprint -> first iteration number for dedup
+ const fingerprintSeen = new Map();
for (const entry of recentHistory) {
const issues = [];
@@ -579,37 +615,46 @@ function _buildIterationFeedback(recentHistory, errorEntries) {
issues.push(`commit anomaly: ${entry.commitAnomaly}`);
}
- if (!entry.filesChanged || entry.filesChanged.length === 0) {
- issues.push('no files changed');
- }
-
if (!entry.completionDetected && !entry.taskDetected) {
issues.push('no loop promise emitted');
}
if (issues.length > 0) {
- let line = `- Iteration ${entry.iteration}: ${issues.join('; ')}.`;
-
- if (_isFailedIteration(entry) && errorEntries) {
- const errorDetails = _extractErrorForIteration(errorEntries, entry.iteration);
- if (errorDetails) {
- line += '\n Error output:';
- if (errorDetails.signal) {
- line += `\n signal: ${errorDetails.signal}`;
- }
- if (errorDetails.failureStage) {
- line += `\n failure stage: ${errorDetails.failureStage}`;
- }
- if (errorDetails.stderr) {
- line += `\n ${errorDetails.stderr}`;
- }
- if (errorDetails.stdout) {
- line += `\n stdout: ${errorDetails.stdout}`;
+ // Compute fingerprint for dedup
+ const fp = _failureFingerprint(entry, errorEntries);
+ const isRealFailure = !_isEmptyFingerprint(fp);
+
+ if (isRealFailure && fingerprintSeen.has(fp)) {
+ const firstIteration = fingerprintSeen.get(fp);
+ problemLines.push(
+ `- Iteration ${entry.iteration}: same failure as iteration ${firstIteration} (see above).`
+ );
+ } else {
+ if (isRealFailure) fingerprintSeen.set(fp, entry.iteration);
+
+ let line = `- Iteration ${entry.iteration}: ${issues.join('; ')}.`;
+
+ if (_isFailedIteration(entry) && errorEntries) {
+ const errorDetails = _extractErrorForIteration(errorEntries, entry.iteration);
+ if (errorDetails) {
+ line += '\n Error output:';
+ if (errorDetails.signal) {
+ line += `\n signal: ${errorDetails.signal}`;
+ }
+ if (errorDetails.failureStage) {
+ line += `\n failure stage: ${errorDetails.failureStage}`;
+ }
+ if (errorDetails.stderr) {
+ line += `\n ${errorDetails.stderr}`;
+ }
+ if (errorDetails.stdout) {
+ line += `\n stdout: ${errorDetails.stdout}`;
+ }
}
}
- }
- problemLines.push(line);
+ problemLines.push(line);
+ }
}
}
@@ -632,8 +677,8 @@ function _extractErrorForIteration(errorEntries, iteration) {
let stderr = match.stderr || '';
let stdout = match.stdout || '';
- if (stderr.length > 2000) stderr = stderr.substring(0, 2000) + '...';
- if (stdout.length > 500) stdout = stdout.substring(0, 500) + '...';
+ if (stderr.length > 500) stderr = stderr.substring(0, 500) + '...';
+ if (stdout.length > 200) stdout = stdout.substring(0, 200) + '...';
return {
stderr,
@@ -799,4 +844,6 @@ module.exports = {
_failureStageForError,
_errorText,
_appendFatalIterationFailure,
+ _failureFingerprint,
+ _firstNonEmptyLine,
};
diff --git a/lib/mini-ralph/tasks.js b/lib/mini-ralph/tasks.js
index e47a441..379962a 100644
--- a/lib/mini-ralph/tasks.js
+++ b/lib/mini-ralph/tasks.js
@@ -157,24 +157,19 @@ function taskContext(tasksFile) {
all.find((task) => task.status === 'in_progress') ||
all.find((task) => task.status === 'incomplete') ||
null;
- const completed = all.filter((task) => task.status === 'completed');
+ const completedCount = all.filter((task) => task.status === 'completed').length;
+ const total = all.length;
const sections = [];
if (current) {
sections.push('## Current Task');
sections.push(`- ${current.fullDescription || current.description}`);
+ sections.push('');
}
- if (completed.length > 0) {
- if (sections.length > 0) {
- sections.push('');
- }
- sections.push('## Completed Tasks for Git Commit');
- sections.push(
- ...completed.map((task) => `- [x] ${task.fullDescription || task.description}`)
- );
- }
+ sections.push('## Progress');
+ sections.push(`- ${completedCount} of ${total} tasks complete`);
return sections.join('\n');
}
diff --git a/scripts/ralph-run.sh b/scripts/ralph-run.sh
index 016a95f..051c163 100755
--- a/scripts/ralph-run.sh
+++ b/scripts/ralph-run.sh
@@ -421,13 +421,7 @@ generate_prd() {
prd_content+="$OPENSPEC_DESIGN"$'\n'$'\n'
# Add current task context for Ralph to use in commits
- local task_context
- task_context=$(get_current_task_context "$change_dir")
-
- if [[ -n "$task_context" ]]; then
- prd_content+="## Current Task Context"$'\n'$'\n'
- prd_content+="$task_context"$'\n'$'\n'
- fi
+ # (Removed: task context is now provided via {{task_context}} template variable only)
echo "$prd_content"
}
@@ -750,9 +744,9 @@ create_prompt_template() {
Change directory: {{change_dir}}
-## Invocation-Time PRD Snapshot
+## OpenSpec Artifacts
-{{base_prompt}}
+{{_openspec_manifest}}
## Fresh Task Context
@@ -760,35 +754,9 @@ Change directory: {{change_dir}}
## Instructions
-1. **Identify** current task:
- - Find any task marked as [/] (in progress)
- - If no task is in progress, pick the first task marked as [ ] (incomplete)
- - Mark the task as [/] in the tasks file before starting work
-
-2. **Implement** the current task directly:
- - Make the smallest maintainable change that fully satisfies the current task
- - Run the most relevant validation or tests for the task before claiming completion
-
-3. **Complete** task:
- - Verify that the implementation meets the requirements
- - When the task is successfully completed, mark it as [x] in the tasks file
- - Output: `{{task_promise}} `
-
-4. **Continue** to the next task:
- - The loop will continue with the next iteration
- - Find the next incomplete task and repeat the process
+Before implementing, read the OpenSpec artifacts listed above that are relevant to the current task.
-## Critical Rules
-
-- Work on ONE task at a time from the task list
-- Do not rely on editor-specific slash commands or local-only skills; follow this prompt directly
-- Treat tasks.md as the only source of truth for task state
-- ONLY output `{{task_promise}} ` when the current task is complete and marked as [x]
-- ONLY output `{{completion_promise}} ` when ALL tasks are [x]
-- Output promise tags DIRECTLY - do not quote them, explain them, or say you "will" output them
-- Do NOT lie or output false promises to exit the loop
-- If stuck, try a different approach
-- Check your work before claiming completion
+Pick the first [ ] or [/] task in tasks.md, mark it [/], implement it (smallest change that fully satisfies the Done-when conditions), run the task's verification command, mark it [x] on success, then output `{{task_promise}} `. Output `{{completion_promise}} ` only when every task is [x]. Output promise tags on their own line, literal; do not quote or describe them. Do not fabricate a promise to exit the loop. If an approach fails twice, try a different one.
## Commit Contract
@@ -797,14 +765,65 @@ Change directory: {{change_dir}}
{{context}}
EOF
- # Use a portable inplace replace: write to temp file then move into place
+ # Determine repo root for AGENTS.md probe
+ local repo_root
+ repo_root=$(git rev-parse --show-toplevel 2>/dev/null) || repo_root=""
+
+ # Build the manifest body
+ local manifest_body
+ manifest_body="Read these as needed (source of truth for this change):"$'\n'$'\n'
+ manifest_body+="- $abs_change_dir/proposal.md"$'\n'
+ manifest_body+="- $abs_change_dir/design.md"$'\n'
+
+ # Pre-expand specs/*/spec.md into concrete paths
+ if [[ -d "$abs_change_dir/specs" ]]; then
+ while IFS= read -r spec_path; do
+ [[ -n "$spec_path" ]] && manifest_body+="- $spec_path"$'\n'
+ done < <(find "$abs_change_dir/specs" -name spec.md -type f 2>/dev/null | sort)
+ fi
+
+ manifest_body+="- .ralph/PRD.md (pre-concatenated convenience copy of the above)"
+
+ # Optionally append AGENTS.md reference
+ local agents_line
+ agents_line=$(probe_agents_md "$repo_root")
+ if [[ -n "$agents_line" ]]; then
+ manifest_body+=$'\n'"$agents_line"
+ fi
+
+ # Substitute {{_openspec_manifest}} using awk with a manifest temp file
+ # (awk -v cannot handle multi-line values; use getline from a file instead)
+ local _manifest_file
+ _manifest_file=$(mktemp 2>/dev/null || mktemp -t ralph-manifest)
+ printf '%s' "$manifest_body" > "$_manifest_file"
local _tmpfile
_tmpfile=$(mktemp 2>/dev/null || mktemp -t ralph-template)
+ awk -v mf="$_manifest_file" '
+ {
+ if ($0 == "{{_openspec_manifest}}") {
+ while ((getline line < mf) > 0) { print line }
+ close(mf)
+ } else { print }
+ }
+ ' "$template_file" > "$_tmpfile" && mv "$_tmpfile" "$template_file"
+ rm -f "$_manifest_file"
+
+ # Substitute {{change_dir}}
+ _tmpfile=$(mktemp 2>/dev/null || mktemp -t ralph-template)
sed "s|{{change_dir}}|$abs_change_dir|g" "$template_file" > "$_tmpfile" && mv "$_tmpfile" "$template_file"
log_verbose "Prompt template created: $template_file"
}
+probe_agents_md() {
+ local repo_root="$1"
+ if [[ -n "$repo_root" && -r "$repo_root/AGENTS.md" ]]; then
+ echo "- AGENTS.md (project-level build/test guide)"
+ else
+ echo ""
+ fi
+}
+
restore_ralph_state_from_tasks() {
local tasks_file="$1"
local ralph_loop_file=".ralph/ralph-loop.state.json"
diff --git a/tests/helpers/test-functions.sh b/tests/helpers/test-functions.sh
index 5a6e025..b5f4807 100644
--- a/tests/helpers/test-functions.sh
+++ b/tests/helpers/test-functions.sh
@@ -718,9 +718,9 @@ create_prompt_template() {
Change directory: {{change_dir}}
-## Invocation-Time PRD Snapshot
+## OpenSpec Artifacts
-{{base_prompt}}
+{{_openspec_manifest}}
## Fresh Task Context
@@ -728,35 +728,9 @@ Change directory: {{change_dir}}
## Instructions
-1. **Identify** current task:
- - Find any task marked as [/] (in progress)
- - If no task is in progress, pick the first task marked as [ ] (incomplete)
- - Mark the task as [/] in the tasks file before starting work
+Before implementing, read the OpenSpec artifacts listed above that are relevant to the current task.
-2. **Implement** the current task directly:
- - Make the smallest maintainable change that fully satisfies the current task
- - Run the most relevant validation or tests for the task before claiming completion
-
-3. **Complete** task:
- - Verify that the implementation meets the requirements
- - When the task is successfully completed, mark it as [x] in the tasks file
- - Output: `{{task_promise}} `
-
-4. **Continue** to the next task:
- - The loop will continue with the next iteration
- - Find the next incomplete task and repeat the process
-
-## Critical Rules
-
-- Work on ONE task at a time from the task list
-- Do not rely on editor-specific slash commands or local-only skills; follow this prompt directly
-- Treat tasks.md as the only source of truth for task state
-- ONLY output `{{task_promise}} ` when the current task is complete and marked as [x]
-- ONLY output `{{completion_promise}} ` when ALL tasks are [x]
-- Output promise tags DIRECTLY - do not quote them, explain them, or say you "will" output them
-- Do NOT lie or output false promises to exit the loop
-- If stuck, try a different approach
-- Check your work before claiming completion
+Pick the first [ ] or [/] task in tasks.md, mark it [/], implement it (smallest change that fully satisfies the Done-when conditions), run the task's verification command, mark it [x] on success, then output `{{task_promise}} `. Output `{{completion_promise}} ` only when every task is [x]. Output promise tags on their own line, literal; do not quote or describe them. Do not fabricate a promise to exit the loop. If an approach fails twice, try a different one.
## Commit Contract
@@ -764,15 +738,65 @@ Change directory: {{change_dir}}
{{context}}
EOF
+
+ # Determine repo root for AGENTS.md probe
+ local repo_root
+ repo_root=$(git rev-parse --show-toplevel 2>/dev/null) || repo_root=""
+
+ # Build the manifest body
+ local manifest_body
+ manifest_body="Read these as needed (source of truth for this change):"$'\n'$'\n'
+ manifest_body+="- $abs_change_dir/proposal.md"$'\n'
+ manifest_body+="- $abs_change_dir/design.md"$'\n'
+
+ # Pre-expand specs/*/spec.md into concrete paths
+ if [[ -d "$abs_change_dir/specs" ]]; then
+ while IFS= read -r spec_path; do
+ [[ -n "$spec_path" ]] && manifest_body+="- $spec_path"$'\n'
+ done < <(find "$abs_change_dir/specs" -name spec.md -type f 2>/dev/null | sort)
+ fi
+
+ manifest_body+="- .ralph/PRD.md (pre-concatenated convenience copy of the above)"
- # Use a portable inplace replace: write to temp file then move into place
+ # Optionally append AGENTS.md reference
+ local agents_line
+ agents_line=$(probe_agents_md "$repo_root")
+ if [[ -n "$agents_line" ]]; then
+ manifest_body+=$'\n'"$agents_line"
+ fi
+
+ # Substitute {{_openspec_manifest}} using awk with a manifest temp file
+ local _manifest_file
+ _manifest_file=$(mktemp 2>/dev/null || mktemp -t ralph-manifest)
+ printf '%s' "$manifest_body" > "$_manifest_file"
local _tmpfile
_tmpfile=$(mktemp 2>/dev/null || mktemp -t ralph-template)
+ awk -v mf="$_manifest_file" '
+ {
+ if ($0 == "{{_openspec_manifest}}") {
+ while ((getline line < mf) > 0) { print line }
+ close(mf)
+ } else { print }
+ }
+ ' "$template_file" > "$_tmpfile" && mv "$_tmpfile" "$template_file"
+ rm -f "$_manifest_file"
+
+ # Substitute {{change_dir}}
+ _tmpfile=$(mktemp 2>/dev/null || mktemp -t ralph-template)
sed "s|{{change_dir}}|$abs_change_dir|g" "$template_file" > "$_tmpfile" && mv "$_tmpfile" "$template_file"
log_verbose "Prompt template created: $template_file"
}
+probe_agents_md() {
+ local repo_root="$1"
+ if [[ -n "$repo_root" && -r "$repo_root/AGENTS.md" ]]; then
+ echo "- AGENTS.md (project-level build/test guide)"
+ else
+ echo ""
+ fi
+}
+
restore_ralph_state_from_tasks() {
local tasks_file="$1"
local ralph_loop_file=".ralph/ralph-loop.state.json"
diff --git a/tests/integration/test-complex-workflow.bats b/tests/integration/test-complex-workflow.bats
index e12992e..d665af5 100644
--- a/tests/integration/test-complex-workflow.bats
+++ b/tests/integration/test-complex-workflow.bats
@@ -156,6 +156,24 @@ teardown() {
fi
}
+@test "complex workflow: rendered prompt does not inline spec content" {
+ create_git_repo
+
+ mkdir -p openspec/changes
+ cp -r "$FIXTURES_DIR" openspec/changes/
+
+ local ralph_dir=".ralph"
+
+ run bash "$SCRIPT_PATH" --change complex-feature --max-iterations 2 2>&1 || true
+
+ if [ -f "$ralph_dir/prompt-template.md" ]; then
+ # The spec bodies must NOT appear verbatim in the prompt template.
+ # All complex-feature specs contain "## ADDED Requirements"; its presence = inline regression.
+ ! grep -q "## ADDED Requirements" "$ralph_dir/prompt-template.md"
+ ! grep -q "REST API provides standard HTTP methods" "$ralph_dir/prompt-template.md"
+ fi
+}
+
@test "complex workflow: handles multiple spec files" {
create_git_repo
diff --git a/tests/integration/test-simple-workflow.bats b/tests/integration/test-simple-workflow.bats
index d78cc5d..c1fbf10 100644
--- a/tests/integration/test-simple-workflow.bats
+++ b/tests/integration/test-simple-workflow.bats
@@ -156,6 +156,24 @@ teardown() {
fi
}
+@test "simple workflow: rendered prompt does not inline spec content" {
+ create_git_repo
+
+ mkdir -p openspec/changes
+ cp -r "$FIXTURES_DIR" openspec/changes/
+
+ local ralph_dir=".ralph"
+
+ run bash "$SCRIPT_PATH" --change simple-feature --max-iterations 1 2>&1 || true
+
+ if [ -f "$ralph_dir/prompt-template.md" ]; then
+ # The spec body text must NOT appear verbatim in the prompt template.
+ # The spec contains this distinctive phrase; if it appears the inline regression has occurred.
+ ! grep -q "Project structure follows standard conventions" "$ralph_dir/prompt-template.md"
+ ! grep -q "## ADDED Requirements" "$ralph_dir/prompt-template.md"
+ fi
+}
+
@test "simple workflow: change directory detected correctly" {
create_git_repo
diff --git a/tests/unit/bash/test-create-prompt-template.bats b/tests/unit/bash/test-create-prompt-template.bats
index ea89430..7cf022b 100644
--- a/tests/unit/bash/test-create-prompt-template.bats
+++ b/tests/unit/bash/test-create-prompt-template.bats
@@ -170,7 +170,10 @@ teardown() {
[ "$status" -eq 0 ]
+ # Old section name must be gone
! grep -q "## OpenSpec Artifacts Context" "$template_file"
+ # New section must be present
+ grep -q "## OpenSpec Artifacts" "$template_file"
cd - > /dev/null
rm -rf "$test_dir"
@@ -190,8 +193,11 @@ teardown() {
[ "$status" -eq 0 ]
- grep -q "## Invocation-Time PRD Snapshot" "$template_file"
- grep -q "{{base_prompt}}" "$template_file"
+ # The old inline PRD section must be gone; manifest style is now used instead
+ ! grep -q "## Invocation-Time PRD Snapshot" "$template_file"
+ ! grep -q "{{base_prompt}}" "$template_file"
+ # Manifest section reference to .ralph/PRD.md must be present
+ grep -q ".ralph/PRD.md" "$template_file"
cd - > /dev/null
rm -rf "$test_dir"
@@ -231,7 +237,12 @@ teardown() {
[ "$status" -eq 0 ]
- grep -q "## Critical Rules" "$template_file"
+ # The verbose Critical Rules section is replaced by a compact Instructions block
+ ! grep -q "## Critical Rules" "$template_file"
+ # The compressed Instructions block and promise contract must still be present
+ grep -q "## Instructions" "$template_file"
+ grep -q "{{task_promise}}" "$template_file"
+ grep -q "{{completion_promise}}" "$template_file"
cd - > /dev/null
rm -rf "$test_dir"
@@ -256,7 +267,6 @@ teardown() {
! grep -q "Create a git commit using the required format below" "$template_file"
! grep -q "## CRITICAL: Git Commit Format" "$template_file"
! grep -q "When making git commits, you MUST use this EXACT format" "$template_file"
- grep -q "When the task is successfully completed, mark it as \[x\] in the tasks file" "$template_file"
! grep -q "Create a git commit" "$template_file"
cd - > /dev/null
@@ -278,7 +288,6 @@ teardown() {
[ "$status" -eq 0 ]
! grep -q "/opsx-apply" "$template_file"
- grep -q "Do not rely on editor-specific slash commands" "$template_file"
cd - > /dev/null
rm -rf "$test_dir"
@@ -446,17 +455,15 @@ teardown() {
[ "$status" -eq 0 ]
- # Must NOT contain re-read instructions — artifacts are already inlined via {{base_prompt}}
- ! grep -q "Read the relevant OpenSpec artifacts" "$template_file"
- # Must NOT contain the critical-rule that told the agent to re-read tasks every iteration
+ # Must NOT contain legacy inline/duplication markers
! grep -q "Read the full tasks file every iteration" "$template_file"
- # Must NOT contain a raw {{tasks}} dump
! grep -q "{{tasks}}" "$template_file"
- # Must NOT contain the separate "OpenSpec Artifacts Context" section
! grep -q "## OpenSpec Artifacts Context" "$template_file"
- # MUST contain the single OpenSpec source surface
- grep -q "{{base_prompt}}" "$template_file"
- # MUST contain the single tasks surface
+ ! grep -q "{{base_prompt}}" "$template_file"
+ ! grep -q "{{_openspec_manifest}}" "$template_file"
+ ! grep -q "## Invocation-Time PRD Snapshot" "$template_file"
+ # MUST contain the manifest section and task context surface
+ grep -q "## OpenSpec Artifacts" "$template_file"
grep -q "{{task_context}}" "$template_file"
cd - > /dev/null
@@ -484,3 +491,156 @@ teardown() {
cd - > /dev/null
rm -rf "$test_dir"
}
+
+@test "create_prompt_template: manifest contains absolute path lines for proposal, design, and PRD" {
+ local test_dir
+ test_dir=$(setup_test_dir)
+ cd "$test_dir" || return 1
+
+ local change_dir="$test_dir/openspec/changes/test-change"
+ local template_file="$test_dir/template.txt"
+
+ mkdir -p "$change_dir/specs/my-spec"
+ touch "$change_dir/specs/my-spec/spec.md"
+
+ run create_prompt_template "$change_dir" "$template_file"
+
+ [ "$status" -eq 0 ]
+
+ local abs_change_dir
+ abs_change_dir=$(get_realpath "$change_dir")
+
+ grep -q "^- $abs_change_dir/proposal.md$" "$template_file"
+ grep -q "^- $abs_change_dir/design.md$" "$template_file"
+ grep -q "^- $abs_change_dir/specs/my-spec/spec.md$" "$template_file"
+ grep -q "^- .ralph/PRD.md" "$template_file"
+ # Glob must NOT be present in final rendered file
+ ! grep -q "specs/\*/spec.md" "$template_file"
+ # Internal token must be fully expanded
+ ! grep -q "{{_openspec_manifest}}" "$template_file"
+
+ cd - > /dev/null
+ rm -rf "$test_dir"
+}
+
+@test "create_prompt_template: with AGENTS.md at repo root, manifest includes AGENTS.md reference" {
+ local test_dir
+ test_dir=$(setup_test_dir)
+ # Do NOT cd into test_dir — stay in project root so git rev-parse works
+
+ local change_dir="$test_dir/openspec/changes/test-change"
+ local template_file="$test_dir/template.txt"
+
+ mkdir -p "$change_dir/specs"
+
+ local project_root
+ project_root=$(git rev-parse --show-toplevel 2>/dev/null) || project_root=""
+
+ if [[ -z "$project_root" ]]; then
+ skip "Not in a git repository; cannot test AGENTS.md probe"
+ fi
+
+ local agents_created=false
+ if [[ ! -f "$project_root/AGENTS.md" ]]; then
+ echo "# Project build/test guide" > "$project_root/AGENTS.md"
+ agents_created=true
+ fi
+
+ run create_prompt_template "$change_dir" "$template_file"
+
+ if [[ "$agents_created" == "true" ]]; then
+ rm -f "$project_root/AGENTS.md"
+ fi
+
+ [ "$status" -eq 0 ]
+ grep -q "AGENTS.md" "$template_file"
+
+ rm -rf "$test_dir"
+}
+
+@test "create_prompt_template: without AGENTS.md, manifest omits AGENTS.md reference" {
+ local test_dir
+ test_dir=$(setup_test_dir)
+ # Do NOT cd into test_dir — stay in project root so git rev-parse works
+
+ local change_dir="$test_dir/openspec/changes/test-change"
+ local template_file="$test_dir/template.txt"
+
+ mkdir -p "$change_dir/specs"
+
+ local project_root
+ project_root=$(git rev-parse --show-toplevel 2>/dev/null) || project_root=""
+
+ if [[ -z "$project_root" ]]; then
+ skip "Not in a git repository; cannot test AGENTS.md probe"
+ fi
+
+ # Temporarily hide AGENTS.md if it exists
+ local backup_done=false
+ if [[ -f "$project_root/AGENTS.md" ]]; then
+ mv "$project_root/AGENTS.md" "$project_root/AGENTS.md.bak_test"
+ backup_done=true
+ fi
+
+ run create_prompt_template "$change_dir" "$template_file"
+
+ if [[ "$backup_done" == "true" ]]; then
+ mv "$project_root/AGENTS.md.bak_test" "$project_root/AGENTS.md"
+ fi
+
+ [ "$status" -eq 0 ]
+ ! grep -q "AGENTS.md" "$template_file"
+
+ rm -rf "$test_dir"
+}
+
+@test "create_prompt_template: explicit-read sentence precedes task-selection in Instructions section" {
+ local test_dir
+ test_dir=$(setup_test_dir)
+ # Stay in project root so git rev-parse works
+
+ local change_dir="$test_dir/openspec/changes/test-change"
+ local template_file="$test_dir/template.txt"
+
+ mkdir -p "$change_dir/specs/test-spec"
+ echo "## ADDED Requirements" > "$change_dir/specs/test-spec/spec.md"
+
+ run create_prompt_template "$change_dir" "$template_file"
+
+ [ "$status" -eq 0 ]
+
+ # 1. Sentence appears exactly once
+ local sentence="Before implementing, read the OpenSpec artifacts listed above that are relevant to the current task."
+ local count
+ count=$(grep -c "$sentence" "$template_file" || true)
+ [ "$count" -eq 1 ]
+
+ # 2. Sentence line number is less than the first "Pick the first" line number
+ local read_line pick_line
+ read_line=$(grep -n "$sentence" "$template_file" | head -n1 | cut -d: -f1)
+ pick_line=$(grep -n "Pick the first" "$template_file" | head -n1 | cut -d: -f1)
+ [ -n "$read_line" ]
+ [ -n "$pick_line" ]
+ [ "$read_line" -lt "$pick_line" ]
+
+ # 3. Sentence appears inside the ## Instructions section
+ # (after a line matching "^## Instructions" and before the next "^## " header)
+ local in_instructions=false
+ local found_sentence=false
+ while IFS= read -r line; do
+ if [[ "$line" =~ ^##[[:space:]]Instructions ]]; then
+ in_instructions=true
+ continue
+ fi
+ if [[ "$in_instructions" == "true" && "$line" =~ ^##[[:space:]] ]]; then
+ break
+ fi
+ if [[ "$in_instructions" == "true" && "$line" == *"$sentence"* ]]; then
+ found_sentence=true
+ break
+ fi
+ done < "$template_file"
+ [ "$found_sentence" = "true" ]
+
+ rm -rf "$test_dir"
+}
diff --git a/tests/unit/bash/test-generate-prd.bats b/tests/unit/bash/test-generate-prd.bats
index 85abc64..5ea33c7 100644
--- a/tests/unit/bash/test-generate-prd.bats
+++ b/tests/unit/bash/test-generate-prd.bats
@@ -159,7 +159,7 @@ EOF
[[ "$output" == *"Generated from OpenSpec artifacts"* ]] || true
}
-@test "generate_prd: includes current task context when available" {
+@test "generate_prd: does not include current task context section" {
# Create a test directory with OpenSpec change structure
local test_dir
test_dir=$(setup_test_dir)
@@ -177,17 +177,21 @@ EOF
- [/] 1.2 Current task (in progress)
- [ ] 1.3 Pending task
EOF
-
+
# Read artifacts
read_openspec_artifacts "$change_dir"
# Generate PRD
run generate_prd "$change_dir"
- # Output should contain current task context section
- [[ "$output" == *"## Current Task Context"* ]] || true
- [[ "$output" == *"1.2 Current task"* ]] || true
- [[ "$output" == *"1.1 Completed task"* ]] || true
+ # Output must NOT contain task context sections
+ [[ "$output" != *"## Current Task Context"* ]]
+ [[ "$output" != *"## Completed Tasks for Git Commit"* ]]
+
+ # Output must still contain core artifact sections
+ [[ "$output" == *"## Proposal"* ]]
+ [[ "$output" == *"## Specifications"* ]]
+ [[ "$output" == *"## Design"* ]]
}
@test "generate_prd: handles missing task context gracefully" {
@@ -212,8 +216,9 @@ EOF
# Function should complete without error
[ "$status" -eq 0 ]
- # Output should not contain current task context section if no context
- [[ "$output" != *"## Current Task Context"* ]] || true
+ # Output must never contain task context sections
+ [[ "$output" != *"## Current Task Context"* ]]
+ [[ "$output" != *"## Completed Tasks for Git Commit"* ]]
}
@test "generate_prd: generates valid markdown format" {
@@ -374,7 +379,7 @@ EOF
[ "$first_prd" = "$second_prd" ]
}
-@test "generate_prd: includes completed tasks in context" {
+@test "generate_prd: does not include completed tasks in PRD output" {
# Create a test directory with OpenSpec change structure
local test_dir
test_dir=$(setup_test_dir)
@@ -394,17 +399,21 @@ EOF
- [/] 1.4 Current task
- [ ] 1.5 Pending task
EOF
-
+
# Read artifacts
read_openspec_artifacts "$change_dir"
# Generate PRD
run generate_prd "$change_dir"
- # Output should include all completed tasks
- [[ "$output" == *"1.1 First completed task"* ]] || true
- [[ "$output" == *"1.2 Second completed task"* ]] || true
- [[ "$output" == *"1.3 Third completed task"* ]] || true
+ # Output must NOT contain task context sections
+ [[ "$output" != *"## Current Task Context"* ]]
+ [[ "$output" != *"## Completed Tasks for Git Commit"* ]]
+
+ # Core artifact sections must be present
+ [[ "$output" == *"## Proposal"* ]]
+ [[ "$output" == *"## Specifications"* ]]
+ [[ "$output" == *"## Design"* ]]
}
@test "generate_prd: maintains proper section order" {
@@ -423,30 +432,29 @@ EOF
- [ ] 1.1 Task
EOF
-
+
# Read artifacts
read_openspec_artifacts "$change_dir"
# Generate PRD
run generate_prd "$change_dir"
+ # Output must NOT contain task context sections
+ [[ "$output" != *"## Current Task Context"* ]]
+ [[ "$output" != *"## Completed Tasks for Git Commit"* ]]
+
# Extract section positions
local proposal_pos
local specs_pos
local design_pos
- local context_pos
proposal_pos=$(echo "$output" | grep -n "## Proposal" | cut -d: -f1)
specs_pos=$(echo "$output" | grep -n "## Specifications" | cut -d: -f1)
design_pos=$(echo "$output" | grep -n "## Design" | cut -d: -f1)
- context_pos=$(echo "$output" | grep -n "## Current Task Context" | cut -d: -f1)
- # Check order: Proposal < Specifications < Design < Context
+ # Check order: Proposal < Specifications < Design
[ "$proposal_pos" -lt "$specs_pos" ] || true
[ "$specs_pos" -lt "$design_pos" ] || true
- if [[ -n "$context_pos" ]]; then
- [ "$design_pos" -lt "$context_pos" ] || true
- fi
}
@test "generate_prd: logs verbose message during generation" {
diff --git a/tests/unit/bash/test-prd-content-validation.bats b/tests/unit/bash/test-prd-content-validation.bats
index c6523a1..adb501f 100644
--- a/tests/unit/bash/test-prd-content-validation.bats
+++ b/tests/unit/bash/test-prd-content-validation.bats
@@ -587,6 +587,39 @@ EOF
[[ "$prd" == *"const greeting"* ]] || true
}
+@test "prd validation: does not contain Current Task Context or Completed Tasks sections" {
+ # Create a test directory with OpenSpec change structure
+ local test_dir
+ test_dir=$(setup_test_dir)
+ cd "$test_dir" || return 1
+
+ # Create OpenSpec change structure with tasks in various states
+ local change_dir
+ change_dir=$(create_openspec_change)
+
+ cat > "$change_dir/tasks.md" <<'EOF'
+## Test Tasks
+
+- [x] 1.1 Completed task
+- [/] 1.2 Current task (in progress)
+- [ ] 1.3 Pending task
+EOF
+
+ # Read artifacts and generate PRD
+ read_openspec_artifacts "$change_dir"
+ local prd
+ prd=$(generate_prd "$change_dir")
+
+ # PRD must NOT contain task context injection sections
+ [[ "$prd" != *"## Current Task Context"* ]]
+ [[ "$prd" != *"## Completed Tasks for Git Commit"* ]]
+
+ # PRD must still contain the three core artifact sections
+ [[ "$prd" == *"## Proposal"* ]]
+ [[ "$prd" == *"## Specifications"* ]]
+ [[ "$prd" == *"## Design"* ]]
+}
+
@test "prd validation: contains complete and valid markdown" {
# Create a test directory with OpenSpec change structure
local test_dir
diff --git a/tests/unit/bash/test-prd-omits-task-context.bats b/tests/unit/bash/test-prd-omits-task-context.bats
new file mode 100644
index 0000000..30d4369
--- /dev/null
+++ b/tests/unit/bash/test-prd-omits-task-context.bats
@@ -0,0 +1,369 @@
+#!/usr/bin/env bats
+
+# Test suite for PRD task context omission
+# Tests that generated PRD does NOT include ## Current Task Context or
+# ## Completed Tasks for Git Commit, but DOES include ## Proposal, ## Specifications,
+# ## Design — regardless of tasks.md checkbox state.
+
+setup() {
+ # Load the main script
+ load '../../helpers/test-common'
+ source tests/helpers/test-functions.sh
+}
+
+teardown() {
+ cleanup_test_dir
+}
+
+@test "prd omits task context: does not include Current Task Context when task marked in progress" {
+ local test_dir
+ test_dir=$(setup_test_dir)
+ cd "$test_dir" || return 1
+
+ local change_dir
+ change_dir=$(create_openspec_change)
+
+ cat > "$change_dir/tasks.md" <<'EOF'
+## Test Tasks
+
+- [x] 1.1 Completed task
+- [/] 1.2 Current task (in progress)
+- [ ] 1.3 Pending task
+EOF
+
+ read_openspec_artifacts "$change_dir"
+ local prd
+ prd=$(generate_prd "$change_dir")
+
+ # PRD must NOT contain task context sections
+ [[ "$prd" != *"## Current Task Context"* ]]
+ [[ "$prd" != *"## Completed Tasks for Git Commit"* ]]
+
+ # PRD must still contain core artifact sections
+ [[ "$prd" == *"## Proposal"* ]]
+ [[ "$prd" == *"## Specifications"* ]]
+ [[ "$prd" == *"## Design"* ]]
+}
+
+@test "prd omits task context: does not include completed tasks section with multiple completed" {
+ local test_dir
+ test_dir=$(setup_test_dir)
+ cd "$test_dir" || return 1
+
+ local change_dir
+ change_dir=$(create_openspec_change)
+
+ cat > "$change_dir/tasks.md" <<'EOF'
+## Test Tasks
+
+- [x] 1.1 First completed task
+- [x] 1.2 Second completed task
+- [x] 1.3 Third completed task
+- [/] 1.4 Current task
+- [ ] 1.5 Pending task
+EOF
+
+ read_openspec_artifacts "$change_dir"
+ local prd
+ prd=$(generate_prd "$change_dir")
+
+ [[ "$prd" != *"## Current Task Context"* ]]
+ [[ "$prd" != *"## Completed Tasks for Git Commit"* ]]
+
+ [[ "$prd" == *"## Proposal"* ]]
+ [[ "$prd" == *"## Specifications"* ]]
+ [[ "$prd" == *"## Design"* ]]
+}
+
+@test "prd omits task context: does not include context with pending tasks only" {
+ local test_dir
+ test_dir=$(setup_test_dir)
+ cd "$test_dir" || return 1
+
+ local change_dir
+ change_dir=$(create_openspec_change)
+
+ cat > "$change_dir/tasks.md" <<'EOF'
+## Test Tasks
+
+- [x] 1.1 Completed task
+- [/] 1.2 Current task
+- [ ] 1.3 Pending task 1
+- [ ] 1.4 Pending task 2
+- [ ] 1.5 Pending task 3
+EOF
+
+ read_openspec_artifacts "$change_dir"
+ local prd
+ prd=$(generate_prd "$change_dir")
+
+ [[ "$prd" != *"## Current Task Context"* ]]
+ [[ "$prd" != *"## Completed Tasks for Git Commit"* ]]
+
+ [[ "$prd" == *"## Proposal"* ]]
+ [[ "$prd" == *"## Specifications"* ]]
+ [[ "$prd" == *"## Design"* ]]
+}
+
+@test "prd omits task context: does not include context with tasks from multiple sections" {
+ local test_dir
+ test_dir=$(setup_test_dir)
+ cd "$test_dir" || return 1
+
+ local change_dir
+ change_dir=$(create_openspec_change)
+
+ cat > "$change_dir/tasks.md" <<'EOF'
+## 1. Infrastructure Setup
+
+- [x] 1.1 Setup task 1
+- [x] 1.2 Setup task 2
+
+## 2. Implementation
+
+- [x] 2.1 Implement feature A
+- [/] 2.2 Implement feature B
+- [ ] 2.3 Implement feature C
+
+## 3. Testing
+
+- [x] 3.1 Write unit tests
+- [ ] 3.2 Write integration tests
+EOF
+
+ read_openspec_artifacts "$change_dir"
+ local prd
+ prd=$(generate_prd "$change_dir")
+
+ [[ "$prd" != *"## Current Task Context"* ]]
+ [[ "$prd" != *"## Completed Tasks for Git Commit"* ]]
+
+ [[ "$prd" == *"## Proposal"* ]]
+ [[ "$prd" == *"## Specifications"* ]]
+ [[ "$prd" == *"## Design"* ]]
+}
+
+@test "prd omits task context: does not include context with single completed task" {
+ local test_dir
+ test_dir=$(setup_test_dir)
+ cd "$test_dir" || return 1
+
+ local change_dir
+ change_dir=$(create_openspec_change)
+
+ cat > "$change_dir/tasks.md" <<'EOF'
+## Test Tasks
+
+- [x] 1.1 Only completed task
+- [/] 1.2 Current task
+EOF
+
+ read_openspec_artifacts "$change_dir"
+ local prd
+ prd=$(generate_prd "$change_dir")
+
+ [[ "$prd" != *"## Current Task Context"* ]]
+ [[ "$prd" != *"## Completed Tasks for Git Commit"* ]]
+
+ [[ "$prd" == *"## Proposal"* ]]
+ [[ "$prd" == *"## Specifications"* ]]
+ [[ "$prd" == *"## Design"* ]]
+}
+
+@test "prd omits task context: does not include context when no tasks are marked in progress" {
+ local test_dir
+ test_dir=$(setup_test_dir)
+ cd "$test_dir" || return 1
+
+ local change_dir
+ change_dir=$(create_openspec_change)
+
+ cat > "$change_dir/tasks.md" <<'EOF'
+## Test Tasks
+
+- [x] 1.1 Completed task
+- [ ] 1.2 Pending task
+- [ ] 1.3 Another pending task
+EOF
+
+ read_openspec_artifacts "$change_dir"
+ local prd
+ prd=$(generate_prd "$change_dir")
+
+ [[ "$prd" != *"## Current Task Context"* ]]
+ [[ "$prd" != *"## Completed Tasks for Git Commit"* ]]
+
+ [[ "$prd" == *"## Proposal"* ]]
+ [[ "$prd" == *"## Specifications"* ]]
+ [[ "$prd" == *"## Design"* ]]
+}
+
+@test "prd omits task context: does not include context when tasks.md is empty" {
+ local test_dir
+ test_dir=$(setup_test_dir)
+ cd "$test_dir" || return 1
+
+ local change_dir
+ change_dir=$(create_openspec_change)
+
+ > "$change_dir/tasks.md"
+
+ read_openspec_artifacts "$change_dir"
+ local prd
+ prd=$(generate_prd "$change_dir")
+
+ [ -n "$prd" ]
+
+ [[ "$prd" != *"## Current Task Context"* ]]
+ [[ "$prd" != *"## Completed Tasks for Git Commit"* ]]
+
+ [[ "$prd" == *"## Proposal"* ]]
+ [[ "$prd" == *"## Specifications"* ]]
+ [[ "$prd" == *"## Design"* ]]
+}
+
+@test "prd omits task context: does not include context with first task in progress" {
+ local test_dir
+ test_dir=$(setup_test_dir)
+ cd "$test_dir" || return 1
+
+ local change_dir
+ change_dir=$(create_openspec_change)
+
+ cat > "$change_dir/tasks.md" <<'EOF'
+## Test Tasks
+
+- [/] 1.1 First task (in progress)
+- [ ] 1.2 Pending task
+EOF
+
+ read_openspec_artifacts "$change_dir"
+ local prd
+ prd=$(generate_prd "$change_dir")
+
+ [[ "$prd" != *"## Current Task Context"* ]]
+ [[ "$prd" != *"## Completed Tasks for Git Commit"* ]]
+
+ [[ "$prd" == *"## Proposal"* ]]
+ [[ "$prd" == *"## Specifications"* ]]
+ [[ "$prd" == *"## Design"* ]]
+}
+
+@test "prd omits task context: does not include context with detailed task descriptions" {
+ local test_dir
+ test_dir=$(setup_test_dir)
+ cd "$test_dir" || return 1
+
+ local change_dir
+ change_dir=$(create_openspec_change)
+
+ cat > "$change_dir/tasks.md" <<'EOF'
+## Implementation Tasks
+
+- [x] 1.1 Create user authentication module with JWT tokens and password hashing
+- [/] 1.2 Implement user profile CRUD operations with database integration
+- [ ] 1.3 Add email verification for new user accounts
+EOF
+
+ read_openspec_artifacts "$change_dir"
+ local prd
+ prd=$(generate_prd "$change_dir")
+
+ [[ "$prd" != *"## Current Task Context"* ]]
+ [[ "$prd" != *"## Completed Tasks for Git Commit"* ]]
+
+ [[ "$prd" == *"## Proposal"* ]]
+ [[ "$prd" == *"## Specifications"* ]]
+ [[ "$prd" == *"## Design"* ]]
+}
+
+@test "prd omits task context: does not include context with many completed tasks" {
+ local test_dir
+ test_dir=$(setup_test_dir)
+ cd "$test_dir" || return 1
+
+ local change_dir
+ change_dir=$(create_openspec_change)
+
+ cat > "$change_dir/tasks.md" <<'EOF'
+## Test Tasks
+
+- [x] 1.1 Completed 1
+- [x] 1.2 Completed 2
+- [x] 1.3 Completed 3
+- [x] 1.4 Completed 4
+- [x] 1.5 Completed 5
+- [/] 1.6 Current task
+EOF
+
+ read_openspec_artifacts "$change_dir"
+ local prd
+ prd=$(generate_prd "$change_dir")
+
+ [[ "$prd" != *"## Current Task Context"* ]]
+ [[ "$prd" != *"## Completed Tasks for Git Commit"* ]]
+
+ [[ "$prd" == *"## Proposal"* ]]
+ [[ "$prd" == *"## Specifications"* ]]
+ [[ "$prd" == *"## Design"* ]]
+}
+
+@test "prd omits task context: does not include context with iteration header in tasks" {
+ local test_dir
+ test_dir=$(setup_test_dir)
+ cd "$test_dir" || return 1
+
+ local change_dir
+ change_dir=$(create_openspec_change)
+
+ cat > "$change_dir/tasks.md" <<'EOF'
+# Ralph Wiggum Task Execution - Iteration 12 / 50
+
+## Test Tasks
+
+- [x] 1.1 Completed task
+- [/] 1.2 Current task
+EOF
+
+ read_openspec_artifacts "$change_dir"
+ local prd
+ prd=$(generate_prd "$change_dir")
+
+ [[ "$prd" != *"## Current Task Context"* ]]
+ [[ "$prd" != *"## Completed Tasks for Git Commit"* ]]
+
+ [[ "$prd" == *"## Proposal"* ]]
+ [[ "$prd" == *"## Specifications"* ]]
+ [[ "$prd" == *"## Design"* ]]
+}
+
+@test "prd omits task context: does not include context with tasks using subtasks" {
+ local test_dir
+ test_dir=$(setup_test_dir)
+ cd "$test_dir" || return 1
+
+ local change_dir
+ change_dir=$(create_openspec_change)
+
+ cat > "$change_dir/tasks.md" <<'EOF'
+## Implementation
+
+- [x] 2.1 Main task A
+ - [x] 2.1.1 Subtask A1
+ - [x] 2.1.2 Subtask A2
+- [/] 2.2 Main task B
+ - [ ] 2.2.1 Subtask B1
+ - [ ] 2.2.2 Subtask B2
+EOF
+
+ read_openspec_artifacts "$change_dir"
+ local prd
+ prd=$(generate_prd "$change_dir")
+
+ [[ "$prd" != *"## Current Task Context"* ]]
+ [[ "$prd" != *"## Completed Tasks for Git Commit"* ]]
+
+ [[ "$prd" == *"## Proposal"* ]]
+ [[ "$prd" == *"## Specifications"* ]]
+ [[ "$prd" == *"## Design"* ]]
+}
diff --git a/tests/unit/bash/test-prd-task-context-injection.bats b/tests/unit/bash/test-prd-task-context-injection.bats
deleted file mode 100644
index 5755f34..0000000
--- a/tests/unit/bash/test-prd-task-context-injection.bats
+++ /dev/null
@@ -1,562 +0,0 @@
-#!/usr/bin/env bats
-
-# Test suite for PRD task context injection
-# Tests that generated PRD includes current task and completed tasks from tasks.md
-
-setup() {
- # Load the main script
- load '../../helpers/test-common'
- source tests/helpers/test-functions.sh
-}
-
-teardown() {
- cleanup_test_dir
-}
-
-@test "prd task context: includes current task when marked as in progress" {
- # Create a test directory with OpenSpec change structure
- local test_dir
- test_dir=$(setup_test_dir)
- cd "$test_dir" || return 1
-
- # Create OpenSpec change structure
- local change_dir
- change_dir=$(create_openspec_change)
-
- # Create tasks.md with current task marked as [/]
- cat > "$change_dir/tasks.md" <<'EOF'
-## Test Tasks
-
-- [x] 1.1 Completed task
-- [/] 1.2 Current task (in progress)
-- [ ] 1.3 Pending task
-EOF
-
- # Read artifacts and generate PRD
- read_openspec_artifacts "$change_dir"
- local prd
- prd=$(generate_prd "$change_dir")
-
- # PRD should contain current task context section
- [[ "$prd" == *"## Current Task Context"* ]] || true
-
- # PRD should include the current task
- [[ "$prd" == *"1.2 Current task"* ]] || true
-}
-
-@test "prd task context: includes all completed tasks" {
- # Create a test directory with OpenSpec change structure
- local test_dir
- test_dir=$(setup_test_dir)
- cd "$test_dir" || return 1
-
- # Create OpenSpec change structure
- local change_dir
- change_dir=$(create_openspec_change)
-
- # Create tasks.md with multiple completed tasks
- cat > "$change_dir/tasks.md" <<'EOF'
-## Test Tasks
-
-- [x] 1.1 First completed task
-- [x] 1.2 Second completed task
-- [x] 1.3 Third completed task
-- [/] 1.4 Current task
-- [ ] 1.5 Pending task
-EOF
-
- # Read artifacts and generate PRD
- read_openspec_artifacts "$change_dir"
- local prd
- prd=$(generate_prd "$change_dir")
-
- # PRD should include all completed tasks
- [[ "$prd" == *"1.1 First completed task"* ]] || true
- [[ "$prd" == *"1.2 Second completed task"* ]] || true
- [[ "$prd" == *"1.3 Third completed task"* ]] || true
-}
-
-@test "prd task context: does not include pending tasks" {
- # Create a test directory with OpenSpec change structure
- local test_dir
- test_dir=$(setup_test_dir)
- cd "$test_dir" || return 1
-
- # Create OpenSpec change structure
- local change_dir
- change_dir=$(create_openspec_change)
-
- # Create tasks.md with current and pending tasks
- cat > "$change_dir/tasks.md" <<'EOF'
-## Test Tasks
-
-- [x] 1.1 Completed task
-- [/] 1.2 Current task
-- [ ] 1.3 Pending task 1
-- [ ] 1.4 Pending task 2
-- [ ] 1.5 Pending task 3
-EOF
-
- # Read artifacts and generate PRD
- read_openspec_artifacts "$change_dir"
- local prd
- prd=$(generate_prd "$change_dir")
-
- # PRD should include completed and current tasks
- [[ "$prd" == *"1.1 Completed task"* ]] || true
- [[ "$prd" == *"1.2 Current task"* ]] || true
-
- # PRD should not include pending tasks in the current task context
- # (pending tasks are only included if they're in the "## Completed Tasks for Git Commit" section)
- if [[ "$prd" == *"## Completed Tasks for Git Commit"* ]]; then
- [[ "$prd" != *"1.3 Pending task 1"* ]] || true
- [[ "$prd" != *"1.4 Pending task 2"* ]] || true
- [[ "$prd" != *"1.5 Pending task 3"* ]] || true
- fi
-}
-
-@test "prd task context: includes completed tasks from different sections" {
- # Create a test directory with OpenSpec change structure
- local test_dir
- test_dir=$(setup_test_dir)
- cd "$test_dir" || return 1
-
- # Create OpenSpec change structure
- local change_dir
- change_dir=$(create_openspec_change)
-
- # Create tasks.md with multiple sections and completed tasks
- cat > "$change_dir/tasks.md" <<'EOF'
-## 1. Infrastructure Setup
-
-- [x] 1.1 Setup task 1
-- [x] 1.2 Setup task 2
-
-## 2. Implementation
-
-- [x] 2.1 Implement feature A
-- [/] 2.2 Implement feature B
-- [ ] 2.3 Implement feature C
-
-## 3. Testing
-
-- [x] 3.1 Write unit tests
-- [ ] 3.2 Write integration tests
-EOF
-
- # Read artifacts and generate PRD
- read_openspec_artifacts "$change_dir"
- local prd
- prd=$(generate_prd "$change_dir")
-
- # PRD should include all completed tasks from all sections
- [[ "$prd" == *"1.1 Setup task 1"* ]] || true
- [[ "$prd" == *"1.2 Setup task 2"* ]] || true
- [[ "$prd" == *"2.1 Implement feature A"* ]] || true
- [[ "$prd" == *"3.1 Write unit tests"* ]] || true
-
- # PRD should include current task
- [[ "$prd" == *"2.2 Implement feature B"* ]] || true
-}
-
-@test "prd task context: handles single completed task" {
- # Create a test directory with OpenSpec change structure
- local test_dir
- test_dir=$(setup_test_dir)
- cd "$test_dir" || return 1
-
- # Create OpenSpec change structure
- local change_dir
- change_dir=$(create_openspec_change)
-
- # Create tasks.md with single completed task
- cat > "$change_dir/tasks.md" <<'EOF'
-## Test Tasks
-
-- [x] 1.1 Only completed task
-- [/] 1.2 Current task
-EOF
-
- # Read artifacts and generate PRD
- read_openspec_artifacts "$change_dir"
- local prd
- prd=$(generate_prd "$change_dir")
-
- # PRD should include the single completed task
- [[ "$prd" == *"1.1 Only completed task"* ]] || true
-
- # PRD should include current task
- [[ "$prd" == *"1.2 Current task"* ]] || true
-}
-
-@test "prd task context: handles no completed tasks" {
- # Create a test directory with OpenSpec change structure
- local test_dir
- test_dir=$(setup_test_dir)
- cd "$test_dir" || return 1
-
- # Create OpenSpec change structure
- local change_dir
- change_dir=$(create_openspec_change)
-
- # Create tasks.md with no completed tasks (only current task)
- cat > "$change_dir/tasks.md" <<'EOF'
-## Test Tasks
-
-- [/] 1.1 First task (in progress)
-- [ ] 1.2 Pending task
-EOF
-
- # Read artifacts and generate PRD
- read_openspec_artifacts "$change_dir"
- local prd
- prd=$(generate_prd "$change_dir")
-
- # PRD should contain current task context section
- [[ "$prd" == *"## Current Task Context"* ]] || true
-
- # PRD should include current task
- [[ "$prd" == *"1.1 First task"* ]] || true
-}
-
-@test "prd task context: preserves task numbering and descriptions" {
- # Create a test directory with OpenSpec change structure
- local test_dir
- test_dir=$(setup_test_dir)
- cd "$test_dir" || return 1
-
- # Create OpenSpec change structure
- local change_dir
- change_dir=$(create_openspec_change)
-
- # Create tasks.md with detailed task descriptions
- cat > "$change_dir/tasks.md" <<'EOF'
-## Implementation Tasks
-
-- [x] 1.1 Create user authentication module with JWT tokens and password hashing
-- [/] 1.2 Implement user profile CRUD operations with database integration
-- [ ] 1.3 Add email verification for new user accounts
-EOF
-
- # Read artifacts and generate PRD
- read_openspec_artifacts "$change_dir"
- local prd
- prd=$(generate_prd "$change_dir")
-
- # PRD should preserve task numbers
- [[ "$prd" == *"1.1"* ]] || true
- [[ "$prd" == *"1.2"* ]] || true
-
- # PRD should preserve full task descriptions
- [[ "$prd" == *"Create user authentication module with JWT tokens"* ]] || true
- [[ "$prd" == *"Implement user profile CRUD operations"* ]] || true
-}
-
-@test "prd task context: includes completed tasks count" {
- # Create a test directory with OpenSpec change structure
- local test_dir
- test_dir=$(setup_test_dir)
- cd "$test_dir" || return 1
-
- # Create OpenSpec change structure
- local change_dir
- change_dir=$(create_openspec_change)
-
- # Create tasks.md with multiple completed tasks
- cat > "$change_dir/tasks.md" <<'EOF'
-## Test Tasks
-
-- [x] 1.1 Completed 1
-- [x] 1.2 Completed 2
-- [x] 1.3 Completed 3
-- [x] 1.4 Completed 4
-- [x] 1.5 Completed 5
-- [/] 1.6 Current task
-EOF
-
- # Read artifacts and generate PRD
- read_openspec_artifacts "$change_dir"
- local prd
- prd=$(generate_prd "$change_dir")
-
- # All 5 completed tasks should be present
- [[ "$prd" == *"1.1 Completed 1"* ]] || true
- [[ "$prd" == *"1.2 Completed 2"* ]] || true
- [[ "$prd" == *"1.3 Completed 3"* ]] || true
- [[ "$prd" == *"1.4 Completed 4"* ]] || true
- [[ "$prd" == *"1.5 Completed 5"* ]] || true
-}
-
-@test "prd task context: includes "Completed Tasks for Git Commit" section when present" {
- # Create a test directory with OpenSpec change structure
- local test_dir
- test_dir=$(setup_test_dir)
- cd "$test_dir" || return 1
-
- # Create OpenSpec change structure
- local change_dir
- change_dir=$(create_openspec_change)
-
- # Create tasks.md with "Completed Tasks for Git Commit" section
- cat > "$change_dir/tasks.md" <<'EOF'
-## Implementation Tasks
-
-- [x] 1.1 Complete task
-
-## Completed Tasks for Git Commit
-
-- [x] 1.1 Complete task
-EOF
-
- # Read artifacts and generate PRD
- read_openspec_artifacts "$change_dir"
- local prd
- prd=$(generate_prd "$change_dir")
-
- # PRD should include the "Completed Tasks for Git Commit" section
- [[ "$prd" == *"## Completed Tasks for Git Commit"* ]] || true
-}
-
-@test "prd task context: places current task context section after design section" {
- # Create a test directory with OpenSpec change structure
- local test_dir
- test_dir=$(setup_test_dir)
- cd "$test_dir" || return 1
-
- # Create OpenSpec change structure with tasks
- local change_dir
- change_dir=$(create_openspec_change)
-
- # Create tasks.md with current task
- cat > "$change_dir/tasks.md" <<'EOF'
-## Test Tasks
-
-- [x] 1.1 Completed task
-- [/] 1.2 Current task
-EOF
-
- # Read artifacts and generate PRD
- read_openspec_artifacts "$change_dir"
- local prd
- prd=$(generate_prd "$change_dir")
-
- # Extract line numbers of sections
- local design_line
- local context_line
-
- design_line=$(echo "$prd" | grep -n "## Design" | cut -d: -f1 | head -n 1)
- context_line=$(echo "$prd" | grep -n "## Current Task Context" | cut -d: -f1 | head -n 1)
-
- # Current task context should come after design section
- if [[ -n "$design_line" && -n "$context_line" ]]; then
- [ "$context_line" -gt "$design_line" ] || true
- fi
-}
-
-@test "prd task context: handles tasks with checkboxes and descriptions" {
- # Create a test directory with OpenSpec change structure
- local test_dir
- test_dir=$(setup_test_dir)
- cd "$test_dir" || return 1
-
- # Create OpenSpec change structure
- local change_dir
- change_dir=$(create_openspec_change)
-
- # Create tasks.md with checkboxes
- cat > "$change_dir/tasks.md" <<'EOF'
-## Test Tasks
-
-- [x] Task 1: This is a completed task with description
-- [/] Task 2: This is the current task with description
-- [ ] Task 3: This is a pending task with description
-EOF
-
- # Read artifacts and generate PRD
- read_openspec_artifacts "$change_dir"
- local prd
- prd=$(generate_prd "$change_dir")
-
- # PRD should include tasks with their descriptions
- [[ "$prd" == *"Task 1: This is a completed task"* ]] || true
- [[ "$prd" == *"Task 2: This is the current task"* ]] || true
-}
-
-@test "prd task context: handles tasks with subtasks" {
- # Create a test directory with OpenSpec change structure
- local test_dir
- test_dir=$(setup_test_dir)
- cd "$test_dir" || return 1
-
- # Create OpenSpec change structure
- local change_dir
- change_dir=$(create_openspec_change)
-
- # Create tasks.md with subtasks (though PRD may flatten them)
- cat > "$change_dir/tasks.md" <<'EOF'
-## Implementation
-
-- [x] 2.1 Main task A
- - [x] 2.1.1 Subtask A1
- - [x] 2.1.2 Subtask A2
-- [/] 2.2 Main task B
- - [ ] 2.2.1 Subtask B1
- - [ ] 2.2.2 Subtask B2
-EOF
-
- # Read artifacts and generate PRD
- read_openspec_artifacts "$change_dir"
- local prd
- prd=$(generate_prd "$change_dir")
-
- # PRD should include main tasks
- [[ "$prd" == *"2.1 Main task A"* ]] || true
- [[ "$prd" == *"2.2 Main task B"* ]] || true
-}
-
-@test "prd task context: does not include task context when no tasks are marked" {
- # Create a test directory with OpenSpec change structure
- local test_dir
- test_dir=$(setup_test_dir)
- cd "$test_dir" || return 1
-
- # Create OpenSpec change structure
- local change_dir
- change_dir=$(create_openspec_change)
-
- # Create tasks.md with no tasks marked as current ([/])
- cat > "$change_dir/tasks.md" <<'EOF'
-## Test Tasks
-
-- [x] 1.1 Completed task
-- [ ] 1.2 Pending task
-- [ ] 1.3 Another pending task
-EOF
-
- # Read artifacts and generate PRD
- read_openspec_artifacts "$change_dir"
- local prd
- prd=$(generate_prd "$change_dir")
-
- # PRD should not contain current task context section if no current task
- [[ "$prd" != *"## Current Task Context"* ]] || true
-}
-
-@test "prd task context: handles empty tasks.md gracefully" {
- # Create a test directory with OpenSpec change structure
- local test_dir
- test_dir=$(setup_test_dir)
- cd "$test_dir" || return 1
-
- # Create OpenSpec change structure
- local change_dir
- change_dir=$(create_openspec_change)
-
- # Create empty tasks.md
- > "$change_dir/tasks.md"
-
- # Read artifacts and generate PRD
- read_openspec_artifacts "$change_dir"
- local prd
- prd=$(generate_prd "$change_dir")
-
- # PRD should be valid even with empty tasks
- [ -n "$prd" ]
-
- # PRD should not contain current task context section
- [[ "$prd" != *"## Current Task Context"* ]] || true
-}
-
-@test "prd task context: handles tasks with special characters in descriptions" {
- # Create a test directory with OpenSpec change structure
- local test_dir
- test_dir=$(setup_test_dir)
- cd "$test_dir" || return 1
-
- # Create OpenSpec change structure
- local change_dir
- change_dir=$(create_openspec_change)
-
- # Create tasks.md with special characters
- cat > "$change_dir/tasks.md" <<'EOF'
-## Test Tasks
-
-- [x] 1.1 Task with special chars: @#$%^&*()
-- [/] 1.2 Current task with quotes "double" 'single'
-- [ ] 1.3 Task with backticks \`code\`
-EOF
-
- # Read artifacts and generate PRD
- read_openspec_artifacts "$change_dir"
- local prd
- prd=$(generate_prd "$change_dir")
-
- # Special characters should be preserved
- [[ "$prd" == *"@#$%^&"* ]] || true
- [[ "$prd" == *"\"double\""* ]] || true
- [[ "$prd" == *"'single'"* ]] || true
- [[ "$prd" == *"\`code\`"* ]] || true
-}
-
-@test "prd task context: includes iteration number when present in tasks" {
- # Create a test directory with OpenSpec change structure
- local test_dir
- test_dir=$(setup_test_dir)
- cd "$test_dir" || return 1
-
- # Create OpenSpec change structure
- local change_dir
- change_dir=$(create_openspec_change)
-
- # Create tasks.md with iteration header
- cat > "$change_dir/tasks.md" <<'EOF'
-# Ralph Wiggum Task Execution - Iteration 12 / 50
-
-## Test Tasks
-
-- [x] 1.1 Completed task
-- [/] 1.2 Current task
-EOF
-
- # Read artifacts and generate PRD
- read_openspec_artifacts "$change_dir"
- local prd
- prd=$(generate_prd "$change_dir")
-
- # PRD task context should include current task
- [[ "$prd" == *"1.2 Current task"* ]] || true
-
- # PRD task context should include completed tasks
- [[ "$prd" == *"1.1 Completed task"* ]] || true
-}
-
-@test "prd task context: handles tasks with markdown formatting in descriptions" {
- # Create a test directory with OpenSpec change structure
- local test_dir
- test_dir=$(setup_test_dir)
- cd "$test_dir" || return 1
-
- # Create OpenSpec change structure
- local change_dir
- change_dir=$(create_openspec_change)
-
- # Create tasks.md with markdown formatting
- cat > "$change_dir/tasks.md" <<'EOF'
-## Test Tasks
-
-- [x] 1.1 Task with **bold** and *italic* text
-- [/] 1.2 Current task with \`code\` formatting
-- [ ] 1.3 Task with [link](https://example.com)
-EOF
-
- # Read artifacts and generate PRD
- read_openspec_artifacts "$change_dir"
- local prd
- prd=$(generate_prd "$change_dir")
-
- # Markdown formatting should be preserved
- [[ "$prd" == *"**bold**"* ]] || true
- [[ "$prd" == *"*italic*"* ]] || true
- [[ "$prd" == *"\`code\`"* ]] || true
- [[ "$prd" == *"[link]"* ]] || true
-}
diff --git a/tests/unit/javascript/mini-ralph-prompt.test.js b/tests/unit/javascript/mini-ralph-prompt.test.js
index ef7ed52..b0aa54d 100644
--- a/tests/unit/javascript/mini-ralph-prompt.test.js
+++ b/tests/unit/javascript/mini-ralph-prompt.test.js
@@ -181,8 +181,9 @@ describe('render()', () => {
expect(result).toContain('## Current Task');
expect(result).toContain('1.1 Active task');
- expect(result).toContain('## Completed Tasks for Git Commit');
- expect(result).toContain('1.2 Done task');
+ expect(result).toContain('## Progress');
+ expect(result).not.toContain('## Completed Tasks for Git Commit');
+ expect(result).not.toContain('1.2 Done task');
});
test('leaves {{tasks}} empty when tasksFile does not exist', () => {
diff --git a/tests/unit/javascript/mini-ralph-runner.test.js b/tests/unit/javascript/mini-ralph-runner.test.js
index 58c58ce..6f60b8c 100644
--- a/tests/unit/javascript/mini-ralph-runner.test.js
+++ b/tests/unit/javascript/mini-ralph-runner.test.js
@@ -26,6 +26,8 @@ const {
_gitErrorMessage,
_isFailedIteration,
_wasSuccessfulIteration,
+ _failureFingerprint,
+ _firstNonEmptyLine,
run,
} = require('../../../lib/mini-ralph/runner');
@@ -586,25 +588,35 @@ describe('_formatAutoCommitMessage()', () => {
// ---------------------------------------------------------------------------
describe('_buildIterationFeedback()', () => {
- test('summarizes recent failed or no-progress iterations', () => {
+ test('summarizes recent failed iterations', () => {
const feedback = _buildIterationFeedback([
{ iteration: 2, exitCode: 1, filesChanged: [], completionDetected: false, taskDetected: false },
- { iteration: 3, exitCode: 0, filesChanged: [], completionDetected: false, taskDetected: false },
+ { iteration: 3, exitCode: 0, filesChanged: [], completionDetected: false, taskDetected: true },
]);
expect(feedback).toContain('Use these signals to avoid repeating the same failed approach');
expect(feedback).toContain('Iteration 2: opencode exited with code 1');
- expect(feedback).toContain('Iteration 3: no files changed');
+ // Iteration 3 had a task promise and exit 0 — no "no files changed" issue
+ expect(feedback).not.toContain('Iteration 3: no files changed');
});
- test('returns empty string when recent history looks healthy', () => {
+ test('returns empty string for clean task-promise iteration with no files changed', () => {
const feedback = _buildIterationFeedback([
- { iteration: 1, exitCode: 0, filesChanged: ['file.js'], completionDetected: false, taskDetected: true },
+ { iteration: 1, exitCode: 0, filesChanged: [], completionDetected: false, taskDetected: true },
]);
expect(feedback).toBe('');
});
+ test('still returns feedback for failed iteration with no files changed', () => {
+ const feedback = _buildIterationFeedback([
+ { iteration: 1, exitCode: 1, signal: '', failureStage: 'invoke_contract', filesChanged: [], completionDetected: false, taskDetected: false },
+ ]);
+
+ expect(feedback).not.toBe('');
+ expect(feedback).toContain('Iteration 1');
+ });
+
test('includes error output when errorContent matches a failed iteration', () => {
const errorContent = [{
timestamp: '2026-04-11T16:30:00Z',
@@ -652,9 +664,9 @@ describe('_buildIterationFeedback()', () => {
expect(feedback).not.toContain('Error output:');
});
- test('truncates stderr to 2000 chars and stdout to 500 chars', () => {
- const longStderr = 'x'.repeat(2500);
- const longStdout = 'y'.repeat(800);
+ test('truncates stderr to 500 chars and stdout to 200 chars in feedback', () => {
+ const longStderr = 'x'.repeat(800);
+ const longStdout = 'y'.repeat(400);
const errorContent = [{
timestamp: '2026-04-11T16:30:00Z',
@@ -672,7 +684,7 @@ describe('_buildIterationFeedback()', () => {
expect(feedback).toContain('Error output:');
const stderrPart = feedback.match(/Error output:\n (.*)\n stdout:/s);
expect(stderrPart).toBeTruthy();
- expect(stderrPart[1].length).toBeLessThanOrEqual(2003);
+ expect(stderrPart[1].length).toBeLessThanOrEqual(503);
});
test('backward compat: no errorContent = existing behavior', () => {
@@ -780,8 +792,8 @@ describe('_extractErrorForIteration()', () => {
expect(_extractErrorForIteration(null, 1)).toBeNull();
});
- test('truncates stderr to 2000 chars', () => {
- const longStderr = 'e'.repeat(2500);
+ test('truncates stderr to 500 chars', () => {
+ const longStderr = 'e'.repeat(1200);
const errorContent = [{
iteration: 1,
exitCode: 1,
@@ -790,12 +802,26 @@ describe('_extractErrorForIteration()', () => {
}];
const result = _extractErrorForIteration(errorContent, 1);
- expect(result.stderr.length).toBe(2003);
+ expect(result.stderr.length).toBe(503);
expect(result.stderr.endsWith('...')).toBe(true);
});
- test('truncates stdout to 500 chars', () => {
- const longStdout = 'o'.repeat(800);
+ test('preserves stderr when shorter than 500 chars', () => {
+ const shortStderr = 'e'.repeat(400);
+ const errorContent = [{
+ iteration: 1,
+ exitCode: 1,
+ stderr: shortStderr,
+ stdout: '',
+ }];
+
+ const result = _extractErrorForIteration(errorContent, 1);
+ expect(result.stderr.length).toBe(400);
+ expect(result.stderr.endsWith('...')).toBe(false);
+ });
+
+ test('truncates stdout to 200 chars', () => {
+ const longStdout = 'o'.repeat(400);
const errorContent = [{
iteration: 1,
exitCode: 1,
@@ -804,7 +830,7 @@ describe('_extractErrorForIteration()', () => {
}];
const result = _extractErrorForIteration(errorContent, 1);
- expect(result.stdout.length).toBe(503);
+ expect(result.stdout.length).toBe(203);
expect(result.stdout.endsWith('...')).toBe(true);
});
@@ -829,6 +855,73 @@ describe('_extractErrorForIteration()', () => {
});
});
+// ---------------------------------------------------------------------------
+// _failureFingerprint / _firstNonEmptyLine / fingerprint dedup
+// ---------------------------------------------------------------------------
+
+describe('_firstNonEmptyLine()', () => {
+ test('returns the first non-whitespace line trimmed to limit', () => {
+ expect(_firstNonEmptyLine(' \n hello world \nother line', 5)).toBe('hello');
+ expect(_firstNonEmptyLine(' \n hello world \nother line', 100)).toBe('hello world');
+ });
+
+ test('returns empty string for null/undefined/empty input', () => {
+ expect(_firstNonEmptyLine(null, 120)).toBe('');
+ expect(_firstNonEmptyLine('', 120)).toBe('');
+ expect(_firstNonEmptyLine(' \n \n', 120)).toBe('');
+ });
+});
+
+describe('_buildIterationFeedback() - fingerprint dedup', () => {
+ test('three same-fingerprint failures: one full detail, two back-references', () => {
+ const errorContent = [
+ { iteration: 1, exitCode: 1, stderr: 'SameError: problem\nline2', stdout: '', signal: '', failureStage: 'invoke_contract' },
+ { iteration: 2, exitCode: 1, stderr: 'SameError: problem\nline2', stdout: '', signal: '', failureStage: 'invoke_contract' },
+ { iteration: 3, exitCode: 1, stderr: 'SameError: problem\nline2', stdout: '', signal: '', failureStage: 'invoke_contract' },
+ ];
+ const history = [
+ { iteration: 1, exitCode: 1, signal: '', failureStage: 'invoke_contract', filesChanged: [], completionDetected: false, taskDetected: false },
+ { iteration: 2, exitCode: 1, signal: '', failureStage: 'invoke_contract', filesChanged: [], completionDetected: false, taskDetected: false },
+ { iteration: 3, exitCode: 1, signal: '', failureStage: 'invoke_contract', filesChanged: [], completionDetected: false, taskDetected: false },
+ ];
+
+ const feedback = _buildIterationFeedback(history, errorContent);
+
+ // Full detail on first occurrence
+ expect(feedback).toContain('Iteration 1:');
+ expect(feedback).toContain('SameError: problem');
+
+ // Back-references for 2 and 3
+ expect(feedback).toContain('same failure as iteration 1 (see above).');
+ const backRefCount = (feedback.match(/same failure as iteration/g) || []).length;
+ expect(backRefCount).toBe(2);
+
+ // Stderr head appears only once
+ const stderrHeadCount = (feedback.match(/SameError: problem/g) || []).length;
+ expect(stderrHeadCount).toBe(1);
+ });
+
+ test('three distinct-fingerprint failures: three full detail entries', () => {
+ const errorContent = [
+ { iteration: 1, exitCode: 1, stderr: 'ErrorA', stdout: '', signal: '', failureStage: 'stageA' },
+ { iteration: 2, exitCode: 1, stderr: 'ErrorB', stdout: '', signal: '', failureStage: 'stageB' },
+ { iteration: 3, exitCode: 1, stderr: 'ErrorC', stdout: '', signal: '', failureStage: 'stageC' },
+ ];
+ const historyEntries = [
+ { iteration: 1, exitCode: 1, signal: '', failureStage: 'stageA', filesChanged: [], completionDetected: false, taskDetected: false },
+ { iteration: 2, exitCode: 1, signal: '', failureStage: 'stageB', filesChanged: [], completionDetected: false, taskDetected: false },
+ { iteration: 3, exitCode: 1, signal: '', failureStage: 'stageC', filesChanged: [], completionDetected: false, taskDetected: false },
+ ];
+
+ const feedback = _buildIterationFeedback(historyEntries, errorContent);
+
+ expect(feedback).toContain('Iteration 1:');
+ expect(feedback).toContain('Iteration 2:');
+ expect(feedback).toContain('Iteration 3:');
+ expect(feedback).not.toContain('same failure as iteration');
+ });
+});
+
// ---------------------------------------------------------------------------
// _getCurrentTaskDescription
// ---------------------------------------------------------------------------
diff --git a/tests/unit/javascript/mini-ralph-status.test.js b/tests/unit/javascript/mini-ralph-status.test.js
index e32a790..af1caaf 100644
--- a/tests/unit/javascript/mini-ralph-status.test.js
+++ b/tests/unit/javascript/mini-ralph-status.test.js
@@ -174,15 +174,16 @@ describe('tasks helpers', () => {
expect(tasks.hashFile(tasksFile)).toMatch(/^[a-f0-9]{32}$/);
});
- test('taskContext lists current and completed tasks', () => {
+ test('taskContext lists current task and progress only', () => {
const tasksFile = path.join(tmpDir, 'tasks.md');
fs.writeFileSync(tasksFile, '- [x] 1.1 Done task\n- [ ] 1.2 Next task\n');
const output = tasks.taskContext(tasksFile);
expect(output).toContain('## Current Task');
expect(output).toContain('- 1.2 Next task');
- expect(output).toContain('## Completed Tasks for Git Commit');
- expect(output).toContain('- [x] 1.1 Done task');
+ expect(output).toContain('## Progress');
+ expect(output).not.toContain('## Completed Tasks for Git Commit');
+ expect(output).not.toContain('- [x] 1.1 Done task');
});
test('syncLink creates and replaces the managed symlink', () => {
diff --git a/tests/unit/javascript/mini-ralph-tasks.test.js b/tests/unit/javascript/mini-ralph-tasks.test.js
index ee013c3..8325ab8 100644
--- a/tests/unit/javascript/mini-ralph-tasks.test.js
+++ b/tests/unit/javascript/mini-ralph-tasks.test.js
@@ -290,16 +290,42 @@ describe('tasks.countTasks()', () => {
// ---------------------------------------------------------------------------
describe('tasks.taskContext()', () => {
- test('returns current and completed task sections', () => {
+ test('returns current task and progress, not completed task descriptions', () => {
+ const content = [
+ '- [x] 1.1 Completed task one',
+ '- [x] 1.2 Completed task two',
+ '- [/] 1.3 Active task',
+ ].join('\n');
const file = path.join(tmpDir, 'tasks.md');
- writeTasks(file, '- [/] 1.1 Active task\n- [x] 1.2 Done task\n');
+ writeTasks(file, content);
const context = tasks.taskContext(file);
expect(context).toContain('## Current Task');
- expect(context).toContain('1.1 Active task');
- expect(context).toContain('## Completed Tasks for Git Commit');
- expect(context).toContain('1.2 Done task');
+ expect(context).toContain('1.3 Active task');
+ expect(context).toContain('## Progress');
+ expect(context).toContain('2 of 3 tasks complete');
+ // Completed task descriptions must NOT appear
+ expect(context).not.toContain('Completed task one');
+ expect(context).not.toContain('Completed task two');
+ // The old section header must NOT appear
+ expect(context).not.toContain('## Completed Tasks for Git Commit');
+ });
+
+ test('returns only progress section when all tasks are completed', () => {
+ const content = [
+ '- [x] 1.1 Task one',
+ '- [x] 1.2 Task two',
+ ].join('\n');
+ const file = path.join(tmpDir, 'tasks.md');
+ writeTasks(file, content);
+
+ const context = tasks.taskContext(file);
+
+ expect(context).toContain('## Progress');
+ expect(context).toContain('2 of 2 tasks complete');
+ expect(context).not.toContain('## Current Task');
+ expect(context).not.toContain('## Completed Tasks for Git Commit');
});
test('returns empty string when no tasks exist', () => {
@@ -308,4 +334,27 @@ describe('tasks.taskContext()', () => {
expect(tasks.taskContext(file)).toBe('');
});
+
+ test('task context size does not grow with completed task count', () => {
+ const file1 = path.join(tmpDir, 'tasks1.md');
+ const file20 = path.join(tmpDir, 'tasks20.md');
+
+ const makeContent = (completedCount) => {
+ const lines = [];
+ for (let i = 1; i <= completedCount; i++) {
+ lines.push(`- [x] ${i}.1 Completed task number ${i}`);
+ }
+ lines.push(`- [ ] ${completedCount + 1}.1 Current incomplete task`);
+ return lines.join('\n');
+ };
+
+ writeTasks(file1, makeContent(1));
+ writeTasks(file20, makeContent(20));
+
+ const ctx1 = tasks.taskContext(file1);
+ const ctx20 = tasks.taskContext(file20);
+
+ // The size difference should be at most a few bytes (the N in "N of M")
+ expect(Math.abs(ctx20.length - ctx1.length)).toBeLessThan(50);
+ });
});