Skip to content

Add interactive publishing#35

Draft
stefanvictora wants to merge 10 commits into
mainfrom
interactive-publish
Draft

Add interactive publishing#35
stefanvictora wants to merge 10 commits into
mainfrom
interactive-publish

Conversation

@stefanvictora

@stefanvictora stefanvictora commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Rename review.md to note.md to better communicate its purpose

Closes #29


Added

  • Adds a guided revpack publish flow for selecting pending review outputs and checkpoint publishing interactively.
  • Adds revpack publish note and note.md as the primary review-note publishing path, while keeping publish review as a hidden command alias.

Changed

  • Updates status guidance, docs, templates, validation messages, and bundle state defaults to use note.md instead of review.md.
  • Stops discovering or publishing legacy review.md drafts from refreshed bundles.

Summary by CodeRabbit

  • New Features

    • Added interactive Guided Publish as the default revpack publish workflow, with keyboard navigation, selective publishing, previews, and confirmation.
    • Added safeguards for stale bundles, malformed drafts, deferred items, and non-interactive script or CI usage.
    • Renamed the review-note output to note.md and added the revpack publish note command.
  • Bug Fixes

    • Preserved pending drafts and default note content after publishing.
    • Improved partial-failure reporting and safe handling of changed publish material.
  • Documentation

    • Updated workflow guidance, command references, templates, and status suggestions for Guided Publish and note.md.

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ee906cc4-2d9f-4cf4-938e-a42515944605

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Bare revpack publish now opens a guided TTY workflow for selecting and previewing publishable material. A shared publish plan executes selections safely, preserves deferred drafts, refreshes eligible bundles, and uses .revpack/outputs/note.md for review notes.

Changes

Guided publish workflow

Layer / File(s) Summary
Workspace material and output contracts
src/workspace/*, src/core/types.ts, src/workspace/workspace-manager.ts
Loads and validates publishable drafts, protects against unsafe paths and concurrent edits, preserves deferred queue entries, and standardizes the review-note output as note.md.
Selective publish execution
src/orchestration/publish-plan.ts, src/orchestration/orchestrator.ts, src/providers/provider.ts, src/orchestration/*.test.ts
Publishes selected replies, findings, summaries, notes, and checkpoints with progress results, cleanup, refresh handling, and optional provider thread IDs.
Interactive terminal selection
src/cli/commands/publish-tui.ts, src/cli/commands/publish-tui.test.ts
Adds keyboard navigation, previews, selection toggles, stale-bundle prompts, responsive rendering, cancellation, terminal cleanup, and control-sequence sanitization.
Publish command orchestration
src/cli/commands/publish.ts, src/cli/commands/publish.test.ts
Makes bare revpack publish guided and TTY-only, retains explicit non-interactive commands, handles freshness and refresh behavior, and exposes publish note with hidden publish review compatibility.
User-facing workflow documentation
README.md, docs/*, templates/*, CHANGELOG.md, src/cli/commands/status.ts
Updates workflow examples, status suggestions, generated instructions, command references, and changelog entries for guided publishing and note.md.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Operator
  participant PublishCommand
  participant GuidedPublish
  participant PublishPlan
  participant Workspace
  participant Provider
  Operator->>PublishCommand: run revpack publish
  PublishCommand->>Workspace: load and validate bundle material
  Workspace-->>GuidedPublish: findings, replies, summary, note, checkpoint state
  GuidedPublish-->>PublishCommand: selected PublishSelection
  PublishCommand->>PublishPlan: execute selection
  PublishPlan->>Provider: publish selected review material
  Provider-->>PublishPlan: publication results
  PublishPlan->>Workspace: remove published drafts and refresh state
  PublishPlan-->>Operator: progress and final result
Loading

Poem

I’m a rabbit with keys in my paws,
Guiding drafts through terminal halls.
Notes hop to note.md, clean and bright,
Deferred crumbs stay tucked out of sight.
Refresh, select, publish with cheer—
A tidy review trail grows here!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR updates guided publish and note.md, but it removes legacy review.md fallback instead of reading it when note.md is absent or empty. Add a fallback that reads legacy .revpack/outputs/review.md when note.md is missing or empty, while keeping note.md as the primary path.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding interactive publishing.
Out of Scope Changes check ✅ Passed The changes stay focused on guided publish, note.md migration, docs, tests, and related workspace/orchestrator plumbing.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread src/cli/commands/publish.ts Outdated
Comment thread src/workspace/workspace-manager.ts Outdated
@stefanvictora
stefanvictora marked this pull request as draft July 6, 2026 22:07
Rename review.md to note.md to better communicate its purpose
Comment thread src/cli/commands/publish.ts Outdated
@stefanvictora

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (2)
src/orchestration/orchestrator.ts (1)

589-600: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Passes through threadIds without validating length/order against comments.

This is the same ordering-contract gap flagged in provider.tspublishReviewBatch forwards whatever threadIds the provider returns straight through to callers that map it positionally to findings. Consider validating threadIds.length === comments.length here (or at least logging a warning on mismatch) so a provider that returns partial/misordered IDs fails loudly instead of causing silent misattribution downstream in publish-plan.ts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/orchestration/orchestrator.ts` around lines 589 - 600, Validate the
provider result in the publishReviewBatch flow before assigning threadIds,
ensuring the returned IDs match comments.length and their positional ordering
contract. On mismatch, fail loudly or emit the established warning and avoid
passing invalid IDs to downstream callers; preserve the existing return shape
for valid results.
src/orchestration/publish-plan.ts (1)

214-226: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Positional threadIds mapping assumes provider preserves submission order.

threadIds[index] is matched to findings[index] purely by array position. If a provider ever returns thread IDs in a different order than submitted (e.g., sorted by creation/ID rather than payload order), a finding's tracked providerThreadId would silently point to the wrong thread. The typeof providerThreadId === 'string' guard only protects against missing entries, not misordering.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/orchestration/publish-plan.ts` around lines 214 - 226, Replace the
positional threadIds[index] association in the findings cleanup loop with an
order-independent mapping using the provider’s stable finding/thread correlation
data. Ensure each finding receives only its own providerThreadId when calling
orchestrator.workspace.appendPublishedAction, while preserving the existing
behavior for missing IDs.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/cli/commands/publish-tui.ts`:
- Around line 668-674: Update the finding label mapping to use each finding’s
`newPath` with a fallback to `oldPath` before rendering the path and position.
Preserve the existing line selection, selection markers, focus handling, and
label formatting.
- Around line 379-415: Update wrapText, fitColumn, and truncateColumn to measure
and slice text by grapheme clusters and terminal display width rather than
JavaScript code units, preserving ANSI handling and column limits for emoji and
CJK characters. Reuse or add a shared display-width helper near visibleText, and
add regression coverage for wrapping and truncating emoji/CJK content without
splitting graphemes or exceeding the requested width.

In `@src/cli/commands/publish.ts`:
- Line 693: Update the repository selection in the publish flow and
`publishAllPending` to use the active bundle’s repository first, falling back to
`getRepository()` only when the bundle target is unavailable; then pass that
result to `requirePublishRepository`. Ensure Git-derived repository selection
cannot override the loaded bundle’s authoritative publishing target.
- Around line 310-315: Update readOptionalTextFile to return an absent result
only when fs.readFile fails with an ENOENT error; rethrow permission, directory,
and other I/O errors while preserving their original cause. Keep the successful
read behavior and returned object shape unchanged.

In `@src/workspace/publish-material.ts`:
- Around line 83-108: The compare-and-mutate flows in the queue cleanup function
and clearPublishedDocument must hold the exclusive workspace/output lock across
the expected-content re-read and subsequent write, rename, or fs.rm operation.
Acquire the existing lock before validation, keep it held through mutation, and
release it reliably afterward while preserving the current mismatch errors and
return behavior.
- Around line 359-364: Update loadPublishMaterial to resolve the note path using
DEFAULT_PUBLISH_PATHS.note instead of bundleState.outputs.review.path, while
retaining the summary resolution. Add coverage for an older bundle whose review
output references review.md and verify that legacy note content is not
published.

In `@src/workspace/workspace-manager.ts`:
- Around line 42-43: Update the discard-only filename list used by
discardOutputs() to include legacy review.md, without restoring its status or
publishing compatibility. In src/workspace/workspace-manager.ts lines 42-43, add
review.md to OUTPUT_FILENAMES; update src/workspace/workspace-manager.test.ts
lines 2168-2175 to assert that both note.md and review.md are removed.

---

Nitpick comments:
In `@src/orchestration/orchestrator.ts`:
- Around line 589-600: Validate the provider result in the publishReviewBatch
flow before assigning threadIds, ensuring the returned IDs match comments.length
and their positional ordering contract. On mismatch, fail loudly or emit the
established warning and avoid passing invalid IDs to downstream callers;
preserve the existing return shape for valid results.

In `@src/orchestration/publish-plan.ts`:
- Around line 214-226: Replace the positional threadIds[index] association in
the findings cleanup loop with an order-independent mapping using the provider’s
stable finding/thread correlation data. Ensure each finding receives only its
own providerThreadId when calling orchestrator.workspace.appendPublishedAction,
while preserving the existing behavior for missing IDs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d2f3c476-56d5-40f8-9c73-254f3f100e1d

📥 Commits

Reviewing files that changed from the base of the PR and between 912c73a and a5ccbe1.

📒 Files selected for processing (29)
  • CHANGELOG.md
  • README.md
  • docs/adr/0002-agent-output-files-are-lazy-drafts.md
  • docs/commands.md
  • src/cli/commands/publish-tui.test.ts
  • src/cli/commands/publish-tui.ts
  • src/cli/commands/publish.test.ts
  • src/cli/commands/publish.ts
  • src/cli/commands/status.test.ts
  • src/cli/commands/status.ts
  • src/cli/index.ts
  • src/core/types.ts
  • src/orchestration/orchestrator.test.ts
  • src/orchestration/orchestrator.ts
  • src/orchestration/publish-plan.test.ts
  • src/orchestration/publish-plan.ts
  • src/providers/local/local-git-provider.test.ts
  • src/providers/provider.ts
  • src/workspace/finding-validator.test.ts
  • src/workspace/finding-validator.ts
  • src/workspace/publish-material.test.ts
  • src/workspace/publish-material.ts
  • src/workspace/workspace-manager.test.ts
  • src/workspace/workspace-manager.ts
  • templates/CONTEXT.md.hbs
  • templates/INSTRUCTIONS.md
  • templates/instructions/01-review-workflow-and-outputs.md
  • templates/instructions/05-review-note.md
  • templates/instructions/07-final-checks.md

Comment on lines +379 to +415
function wrapText(text: string, width: number): string[] {
const safeWidth = Math.max(10, width);
const result: string[] = [];
for (const sourceLine of text.replace(/\r\n/g, '\n').split('\n')) {
if (sourceLine.length <= safeWidth) {
result.push(sourceLine);
continue;
}
let remaining = sourceLine;
while (remaining.length > safeWidth) {
const candidate = remaining.slice(0, safeWidth + 1);
const breakAt = candidate.lastIndexOf(' ');
const end = breakAt > 0 ? breakAt : safeWidth;
result.push(remaining.slice(0, end));
remaining = remaining.slice(end).replace(/^ /, '');
}
result.push(remaining);
}
return result;
}

const ANSI_PATTERN = new RegExp(`${String.fromCharCode(27)}\\[[0-?]*[ -/]*[@-~]`, 'g');

function visibleText(value: string): string {
return value.replace(ANSI_PATTERN, '');
}

function fitColumn(value: string, width: number): string {
const visible = visibleText(value);
if (visible.length > width) return visible.slice(0, Math.max(0, width - 1)) + (width > 0 ? '…' : '');
return value + ' '.repeat(width - visible.length);
}

function truncateColumn(value: string, width: number): string {
const visible = visibleText(value);
if (visible.length <= width) return value;
return visible.slice(0, Math.max(0, width - 1)) + (width > 0 ? '…' : '');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the file first, then inspect the relevant slice and search for any existing
# display-width / grapheme helpers in the repo.
ast-grep outline src/cli/commands/publish-tui.ts --view expanded

printf '\n--- targeted slice ---\n'
sed -n '350,440p' src/cli/commands/publish-tui.ts

printf '\n--- related usages of wrapText / fitColumn / truncateColumn ---\n'
rg -n "wrapText|fitColumn|truncateColumn|visibleText|ANSI_PATTERN" src/cli/commands/publish-tui.ts

printf '\n--- existing width/grapheme helpers in src ---\n'
rg -n "string-width|strip-ansi|grapheme|display width|wcwidth|width" src

Repository: stefanvictora/revpack

Length of output: 15808


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n--- package manifests mentioning width/ansi/grapheme helpers ---\n'
rg -n '"(string-width|strip-ansi|grapheme-splitter|east-asian-width|wcwidth|emoji-regex)"' package.json package-lock.json pnpm-lock.yaml yarn.lock npm-shrinkwrap.json 2>/dev/null || true

printf '\n--- package.json ---\n'
cat -n package.json

Repository: stefanvictora/revpack

Length of output: 4096


🏁 Script executed:

#!/bin/bash
set -euo pipefail

git ls-files 'src/cli/commands/*test.ts' 'src/cli/commands/*spec.ts' 'src/cli/commands/*.ts' | sed -n '1,120p'

Repository: stefanvictora/revpack

Length of output: 658


🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline src/cli/commands/publish-tui.test.ts --view expanded

printf '\n--- focused test slices ---\n'
sed -n '1,260p' src/cli/commands/publish-tui.test.ts

printf '\n--- width-related assertions in publish-tui tests ---\n'
rg -n "wrapText|truncateColumn|fitColumn|unicode|emoji|CJK|column|width|ellipsis" src/cli/commands/publish-tui.test.ts

Repository: stefanvictora/revpack

Length of output: 9390


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n--- publish-tui.test.ts around rendering/layout tests ---\n'
sed -n '440,860p' src/cli/commands/publish-tui.test.ts

printf '\n--- direct references to unicode/ansi handling in tests ---\n'
rg -n "stripVTControlCharacters|emoji|CJK|Unicode|ansi|width|wrapText|fitColumn|truncateColumn" src/cli/commands/publish-tui.test.ts

Repository: stefanvictora/revpack

Length of output: 15104


Make wrapping and truncation terminal-width aware. wrapText, fitColumn, and truncateColumn still operate on code units, so emoji/CJK can be split or spill past the allotted columns; use a grapheme/display-width helper here and add an emoji/CJK regression test.

🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 399-399: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(${String.fromCharCode(27)}\\[[0-?]*[ -/]*[@-~], 'g')
Note: [CWE-1333] Inefficient Regular Expression Complexity

(regexp-from-variable)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/commands/publish-tui.ts` around lines 379 - 415, Update wrapText,
fitColumn, and truncateColumn to measure and slice text by grapheme clusters and
terminal display width rather than JavaScript code units, preserving ANSI
handling and column limits for emoji and CJK characters. Reuse or add a shared
display-width helper near visibleText, and add regression coverage for wrapping
and truncating emoji/CJK content without splitting graphemes or exceeding the
requested width.

Source: Coding guidelines

Comment on lines +668 to +674
...model.findings.map(({ index, value }) => {
const position = value.newLine ?? value.oldLine ?? '?';
const label = value.body.split('\n')[0] || '(untitled finding)';
return ` ${focus?.kind === 'finding' && focus.index === index ? '> ' : ' '}${
selection.findingIndexes.has(index) ? '[x]' : '[ ]'
} ${value.newPath}:${position} ${label}`;
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fall back to oldPath for deleted-file findings.

The downstream publish plan uses newPath || oldPath, but this list always renders newPath. Findings for deleted files can therefore appear as :line, making them difficult to identify.

Proposed fix
     ...model.findings.map(({ index, value }) => {
       const position = value.newLine ?? value.oldLine ?? '?';
+      const displayPath = value.newPath || value.oldPath;
       const label = value.body.split('\n')[0] || '(untitled finding)';
       return `  ${focus?.kind === 'finding' && focus.index === index ? '> ' : '  '}${
         selection.findingIndexes.has(index) ? '[x]' : '[ ]'
-      } ${value.newPath}:${position} ${label}`;
+      } ${displayPath}:${position} ${label}`;
     }),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
...model.findings.map(({ index, value }) => {
const position = value.newLine ?? value.oldLine ?? '?';
const label = value.body.split('\n')[0] || '(untitled finding)';
return ` ${focus?.kind === 'finding' && focus.index === index ? '> ' : ' '}${
selection.findingIndexes.has(index) ? '[x]' : '[ ]'
} ${value.newPath}:${position} ${label}`;
}),
...model.findings.map(({ index, value }) => {
const position = value.newLine ?? value.oldLine ?? '?';
const displayPath = value.newPath || value.oldPath;
const label = value.body.split('\n')[0] || '(untitled finding)';
return ` ${focus?.kind === 'finding' && focus.index === index ? '> ' : ' '}${
selection.findingIndexes.has(index) ? '[x]' : '[ ]'
} ${displayPath}:${position} ${label}`;
}),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/commands/publish-tui.ts` around lines 668 - 674, Update the finding
label mapping to use each finding’s `newPath` with a fallback to `oldPath`
before rendering the path and position. Preserve the existing line selection,
selection markers, focus handling, and label formatting.

Comment on lines +310 to 315
async function readOptionalTextFile(filePath: string): Promise<{ exists: boolean; content: string }> {
try {
return { exists: true, content: await fs.readFile(workspacePath(filePath), 'utf-8') };
} catch {
return { exists: false, content: '' };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Do not treat every note read failure as a missing file.

Permission, directory, and I/O errors currently become an empty note, hiding the real failure. Only ENOENT should return { exists: false }; rethrow other errors with their cause.

Proposed fix
 async function readOptionalTextFile(filePath: string): Promise<{ exists: boolean; content: string }> {
   try {
     return { exists: true, content: await fs.readFile(workspacePath(filePath), 'utf-8') };
-  } catch {
-    return { exists: false, content: '' };
+  } catch (error) {
+    if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
+      return { exists: false, content: '' };
+    }
+    throw new Error(`Could not read ${filePath}.`, { cause: error });
   }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function readOptionalTextFile(filePath: string): Promise<{ exists: boolean; content: string }> {
try {
return { exists: true, content: await fs.readFile(workspacePath(filePath), 'utf-8') };
} catch {
return { exists: false, content: '' };
}
async function readOptionalTextFile(filePath: string): Promise<{ exists: boolean; content: string }> {
try {
return { exists: true, content: await fs.readFile(workspacePath(filePath), 'utf-8') };
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
return { exists: false, content: '' };
}
throw new Error(`Could not read ${filePath}.`, { cause: error });
}
}
🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 311-311: Filesystem path is not a string literal; a request-/variable-derived path can enable path traversal. Validate and normalize the path before use.
Context: fs.readFile(workspacePath(filePath), 'utf-8')
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(detect-non-literal-fs-filename-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/commands/publish.ts` around lines 310 - 315, Update
readOptionalTextFile to return an absent result only when fs.readFile fails with
an ENOENT error; rethrow permission, directory, and other I/O errors while
preserving their original cause. Keep the successful read behavior and returned
object shape unchanged.


let material = await loadMaterial(process.cwd());
const orchestrator = await createPublishOrchestrator();
const repository = requirePublishRepository((await getRepository()) ?? material.bundleState.target.repository);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Use the active bundle’s repository before inspecting Git remotes.

getRepoFromGit() selects the first remote, so repositories with multiple remotes can publish or perform freshness checks against a target different from the loaded bundle. The active bundle is the authoritative publishing target; use Git only as a fallback.

Proposed fix
-  const repository = requirePublishRepository((await getRepository()) ?? material.bundleState.target.repository);
+  const repository = requirePublishRepository(
+    material.bundleState.target.repository || (await getRepository()),
+  );

Apply the equivalent ordering in publishAllPending.

Also applies to: 754-754

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/commands/publish.ts` at line 693, Update the repository selection in
the publish flow and `publishAllPending` to use the active bundle’s repository
first, falling back to `getRepository()` only when the bundle target is
unavailable; then pass that result to `requirePublishRepository`. Ensure
Git-derived repository selection cannot override the loaded bundle’s
authoritative publishing target.

Comment on lines +83 to +108
if (options?.expectedEntries !== undefined) {
const currentEntries = await readOptionalJsonArray(filePath, 'draft objects');
if (!isDeepStrictEqual(currentEntries, options.expectedEntries)) {
throw new Error(`${filePath} changed after publish material was loaded; the newer queue was left unchanged.`);
}
}
if (retained.length === 0 && options?.deleteWhenEmpty) {
await fs.rm(filePath, { force: true });
return true;
}
await writeJsonArrayAtomically(
filePath,
retained.map((draft) => draft.raw),
);
return true;
}

export async function clearPublishedDocument(filePath: string, expectedContent?: string): Promise<void> {
if (expectedContent !== undefined) {
const currentContent = await readOptionalFile(filePath);
if (currentContent !== expectedContent) {
throw new Error(`${filePath} changed after publish material was loaded; the newer document was left unchanged.`);
}
}
await fs.rm(filePath, { force: true });
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Make the compare-and-mutate operations atomic.

Another process can update a queue or note after the equality check but before rename/rm; this then overwrites or deletes the newer draft despite the preservation guard. Hold an exclusive workspace/output lock across the re-read and mutation for both queue and document cleanup.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/workspace/publish-material.ts` around lines 83 - 108, The
compare-and-mutate flows in the queue cleanup function and
clearPublishedDocument must hold the exclusive workspace/output lock across the
expected-content re-read and subsequent write, rename, or fs.rm operation.
Acquire the existing lock before validation, keep it held through mutation, and
release it reliably afterward while preserving the current mismatch errors and
return behavior.

Comment on lines +359 to +364
export async function loadPublishMaterial(workingDir: string): Promise<PublishMaterial> {
const bundleState = await loadBundleState(workingDir);
const [summaryPath, notePath] = await Promise.all([
resolveBundleOutputPath(workingDir, bundleState.outputs?.summary?.path, 'Summary'),
resolveBundleOutputPath(workingDir, bundleState.outputs?.review?.path, 'Review note'),
]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Do not load legacy review.md from an older bundle contract.

Guided publishing and publish all still resolve bundleState.outputs.review.path. An existing bundle whose path is .revpack/outputs/review.md will therefore publish that legacy draft, while the explicit publish note path correctly ignores it. Resolve the note from DEFAULT_PUBLISH_PATHS.note, and add coverage for an old bundle referencing review.md.

Proposed fix
   const [summaryPath, notePath] = await Promise.all([
     resolveBundleOutputPath(workingDir, bundleState.outputs?.summary?.path, 'Summary'),
-    resolveBundleOutputPath(workingDir, bundleState.outputs?.review?.path, 'Review note'),
+    resolveBundleOutputPath(workingDir, DEFAULT_PUBLISH_PATHS.note, 'Review note'),
   ]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function loadPublishMaterial(workingDir: string): Promise<PublishMaterial> {
const bundleState = await loadBundleState(workingDir);
const [summaryPath, notePath] = await Promise.all([
resolveBundleOutputPath(workingDir, bundleState.outputs?.summary?.path, 'Summary'),
resolveBundleOutputPath(workingDir, bundleState.outputs?.review?.path, 'Review note'),
]);
export async function loadPublishMaterial(workingDir: string): Promise<PublishMaterial> {
const bundleState = await loadBundleState(workingDir);
const [summaryPath, notePath] = await Promise.all([
resolveBundleOutputPath(workingDir, bundleState.outputs?.summary?.path, 'Summary'),
resolveBundleOutputPath(workingDir, DEFAULT_PUBLISH_PATHS.note, 'Review note'),
]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/workspace/publish-material.ts` around lines 359 - 364, Update
loadPublishMaterial to resolve the note path using DEFAULT_PUBLISH_PATHS.note
instead of bundleState.outputs.review.path, while retaining the summary
resolution. Add coverage for an older bundle whose review output references
review.md and verify that legacy note content is not published.

Comment on lines +42 to +43
const OUTPUT_FILENAMES = ['replies.json', 'new-findings.json', 'summary.md', 'note.md'] as const;
const DEFAULT_REVIEW_NOTE_PATH = '.revpack/outputs/note.md';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Discard legacy review drafts instead of preserving them.

prepare --discard-outputs calls discardOutputs(), but this list now leaves an existing review.md behind. An explicit discard can therefore retain stale or sensitive review content after refresh. Delete legacy drafts without restoring their status or publishing compatibility.

  • src/workspace/workspace-manager.ts#L42-L43: include review.md in the discard-only filename list.
  • src/workspace/workspace-manager.test.ts#L2168-L2175: expect both note.md and legacy review.md to be removed.
📍 Affects 2 files
  • src/workspace/workspace-manager.ts#L42-L43 (this comment)
  • src/workspace/workspace-manager.test.ts#L2168-L2175
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/workspace/workspace-manager.ts` around lines 42 - 43, Update the
discard-only filename list used by discardOutputs() to include legacy review.md,
without restoring its status or publishing compatibility. In
src/workspace/workspace-manager.ts lines 42-43, add review.md to
OUTPUT_FILENAMES; update src/workspace/workspace-manager.test.ts lines 2168-2175
to assert that both note.md and review.md are removed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add guided publish flow and make review notes use note.md

1 participant