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 5291fda..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,20 +744,9 @@ 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}} +## OpenSpec Artifacts -## Task List - -{{tasks}} +{{_openspec_manifest}} ## Fresh Task Context @@ -771,37 +754,9 @@ Include full context from openspec artifacts in {{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: - - 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 - -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 -- 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] -- 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 +Before implementing, read the OpenSpec artifacts listed above that are relevant to the current task. + +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 @@ -810,14 +765,65 @@ Include full context from openspec artifacts in {{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 98608f5..b5f4807 100644 --- a/tests/helpers/test-functions.sh +++ b/tests/helpers/test-functions.sh @@ -718,20 +718,9 @@ create_prompt_template() { Change directory: {{change_dir}} -## OpenSpec Artifacts Context +## OpenSpec Artifacts -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}} +{{_openspec_manifest}} ## Fresh Task Context @@ -739,37 +728,9 @@ Include full context from openspec artifacts in {{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: - - 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 - -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 -- 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] -- 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 +Before implementing, read the OpenSpec artifacts listed above that are relevant to the current task. + +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 @@ -777,15 +738,65 @@ Include full context from openspec artifacts in {{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 - # Use a portable inplace replace: write to temp file then move into place + 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 + 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 34a4248..d665af5 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 } @@ -160,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 7b46e27..c1fbf10 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 } @@ -160,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 7f70a91..7cf022b 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,10 @@ teardown() { [ "$status" -eq 0 ] - grep -q "## OpenSpec Artifacts Context" "$template_file" + # 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" @@ -432,6 +441,35 @@ 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 legacy inline/duplication markers + ! grep -q "Read the full tasks file every iteration" "$template_file" + ! grep -q "{{tasks}}" "$template_file" + ! grep -q "## OpenSpec Artifacts Context" "$template_file" + ! 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 + rm -rf "$test_dir" +} + @test "create_prompt_template: template file is readable" { local test_dir test_dir=$(setup_test_dir) @@ -453,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); + }); });