Bootstrap first publish directly to source repos - #194
Conversation
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
📝 WalkthroughWalkthroughAdds a bootstrap path to initialize unpublished source repositories, refactors workspace cleanup into Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant RepoSession
participant Workspace
participant Git
participant Manifest
participant Database
Client->>RepoSession: bootstrapSource(sessionId, sourceId, userId, edits)
RepoSession->>Workspace: resetWorkspace()
RepoSession->>Workspace: create/initialize workspace
RepoSession->>Git: init repo & configure remote
RepoSession->>Workspace: applyWorkspaceEdits(edits)
Workspace-->>RepoSession: edits applied / results
RepoSession->>Git: commit & push branch
Git-->>RepoSession: publishedCommit
RepoSession->>Manifest: read & parse manifest file
Manifest-->>RepoSession: parsed manifest data
RepoSession->>Database: persist publishedCommit + manifest/sourceRoot
Database-->>RepoSession: persisted
RepoSession-->>Client: return RepoSourceBootstrapResult(sessionId, publishedCommit, message)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
|
🔎 Preview deployed: https://kody-pr-194.kentcdodds.workers.dev Worker: Mocks:
|
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
packages/worker/src/repo/source-sync.node.test.ts (1)
19-193: Solid branch coverage for the new routing.Both tests pin down the positive and negative RPC calls per branch, and the
editsshape check guards the precomputed array. Consider a follow-up test for thepublishResult.status !== 'ok'rethrow path insource-sync.ts— currently uncovered.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/worker/src/repo/source-sync.node.test.ts` around lines 19 - 193, Add a new unit test that covers the error path when publishResult.status !== 'ok' in syncArtifactSourceSnapshot: mock getEntitySourceById and repoSessionRpc to return a sessionClient where publishSession resolves with status !== 'ok' (e.g., status: 'error') and ensure syncArtifactSourceSnapshot throws/rethrows that error, and verify discardSession is still called with the expected sessionId and userId; locate behavior around the syncArtifactSourceSnapshot function and the sessionClient.publishSession / publishResult.status handling in source-sync.ts to implement this negative-path test.packages/worker/src/repo/repo-session-do.ts (1)
535-555: DRY: duplicate manifest-read + source update block withpublishSession.Lines 535-555 here and lines 956-979 in
publishSessionare near-identical (read manifest →parseRepoManifest→updateEntitySourcewith the samesourceRootnormalizationstartsWith('/') ? ... : '/' + ... : source.source_root). A small helper (e.g.writeSourcePublishedState({ source, publishedCommit })) on the class would remove the drift risk and centralize thesourceRootnormalization rule.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/worker/src/repo/repo-session-do.ts` around lines 535 - 555, The manifest-read + updateEntitySource logic is duplicated between the block in this function and publishSession; extract a private helper on the RepoSessionDo class (e.g. writeSourcePublishedState({ source, publishedCommit })) that performs workspace.readFile(resolveRepoWorkspacePath(...)), parses with parseRepoManifest, computes sourceRoot using the existing normalization (manifest.sourceRoot?.startsWith('/') ? manifest.sourceRoot : manifest.sourceRoot ? `/${manifest.sourceRoot}` : source.source_root), and calls updateEntitySource(this.env.APP_DB, {...}) with the same fields (id, userId, publishedCommit, manifestPath, sourceRoot); replace the two duplicated blocks (the current block and the block in publishSession) to call this new helper so the normalization and update logic are centralized.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/worker/src/repo/repo-session-do.ts`:
- Around line 521-555: The manifest is being read and parsed after git.push,
which can leave a commit on the remote if parsing fails; move the manifest
read/validation to before the push so parsing errors prevent any remote push.
Specifically, after publishCommit is created by commitIfDirty (publishedCommit)
and before calling this.git.push, call
this.workspace.readFile(resolveRepoWorkspacePath(...)) and run
parseRepoManifest(...) (same args currently used), validate manifest.sourceRoot
normalization, and only then call this.git.push and updateEntitySource; keep
updateEntitySource, commitIfDirty, this.git.push, this.workspace.readFile and
parseRepoManifest as the referenced symbols to locate the change.
- Around line 389-393: The new throw in openSession tightens the invariant but
breaks callers that pass unpublished sources (e.g., repo_open_session,
run-saved-skill.ts, jobs/service.ts, app-source.ts) because ensureEntitySource
can create sources without published_commit and bootstrapSource isn't exported;
fix by changing openSession in repo-session-do.ts to detect missing
source.published_commit and either (A) call bootstrapSource internally to create
the published commit before proceeding (ensure bootstrapSource is accessible in
this module and handle errors/logging), or (B) if you prefer explicit flow,
export bootstrapSource and add a public MCP capability so callers (including
repo_open_session) can bootstrap prior to calling openSession; reference
symbols: openSession, bootstrapSource, published_commit, repo_open_session,
ensureEntitySource.
---
Nitpick comments:
In `@packages/worker/src/repo/repo-session-do.ts`:
- Around line 535-555: The manifest-read + updateEntitySource logic is
duplicated between the block in this function and publishSession; extract a
private helper on the RepoSessionDo class (e.g. writeSourcePublishedState({
source, publishedCommit })) that performs
workspace.readFile(resolveRepoWorkspacePath(...)), parses with
parseRepoManifest, computes sourceRoot using the existing normalization
(manifest.sourceRoot?.startsWith('/') ? manifest.sourceRoot :
manifest.sourceRoot ? `/${manifest.sourceRoot}` : source.source_root), and calls
updateEntitySource(this.env.APP_DB, {...}) with the same fields (id, userId,
publishedCommit, manifestPath, sourceRoot); replace the two duplicated blocks
(the current block and the block in publishSession) to call this new helper so
the normalization and update logic are centralized.
In `@packages/worker/src/repo/source-sync.node.test.ts`:
- Around line 19-193: Add a new unit test that covers the error path when
publishResult.status !== 'ok' in syncArtifactSourceSnapshot: mock
getEntitySourceById and repoSessionRpc to return a sessionClient where
publishSession resolves with status !== 'ok' (e.g., status: 'error') and ensure
syncArtifactSourceSnapshot throws/rethrows that error, and verify discardSession
is still called with the expected sessionId and userId; locate behavior around
the syncArtifactSourceSnapshot function and the sessionClient.publishSession /
publishResult.status handling in source-sync.ts to implement this negative-path
test.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: eb67fc48-514c-4d50-b7f2-578a360ae21c
📒 Files selected for processing (5)
packages/worker/src/repo/repo-session-do.tspackages/worker/src/repo/repo-session-rpc.tspackages/worker/src/repo/source-sync.node.test.tspackages/worker/src/repo/source-sync.tspackages/worker/src/repo/types.ts
| if (!baseCommit) { | ||
| throw new Error( | ||
| `Source "${source.id}" has no published commit yet. Bootstrap the source repo before opening a repo session.`, | ||
| ) | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find all call sites of openSession to confirm none rely on the previous lenient behavior.
rg -nP --type=ts -C3 '\bopenSession\s*\(' -g '!**/*.test.ts'Repository: kentcdodds/kody
Length of output: 3850
🏁 Script executed:
#!/bin/bash
# Check the context of openSession calls to see if sources are prepared/bootstrapped first
# Start with run-saved-skill.ts
echo "=== run-saved-skill.ts context (around line 108) ==="
sed -n '95,120p' packages/worker/src/mcp/skills/run-saved-skill.ts
echo -e "\n=== repo-open-session.ts context (around line 60) ==="
sed -n '40,75p' packages/worker/src/mcp/capabilities/repo/repo-open-session.ts
echo -e "\n=== app-source.ts context (around line 80) ==="
sed -n '60,95p' packages/worker/src/repo/app-source.ts
echo -e "\n=== jobs/service.ts context (around lines 464, 478) ==="
sed -n '450,490p' packages/worker/src/jobs/service.tsRepository: kentcdodds/kody
Length of output: 4389
🏁 Script executed:
#!/bin/bash
# Check source-sync.ts to understand bootstrap pattern
echo "=== source-sync.ts full context ==="
sed -n '1,80p' packages/worker/src/repo/source-sync.ts
echo -e "\n=== Check if bootstrapSource is exported/available ==="
rg -n "export.*bootstrapSource" packages/worker/src/repo/
echo -e "\n=== Check source types and validation ==="
echo "Checking if sources are validated as published before these calls..."
rg -n "published_commit|isPublished" packages/worker/src/repo/ -t ts | head -20Repository: kentcdodds/kody
Length of output: 4335
🏁 Script executed:
#!/bin/bash
# Check if sources are validated as published in each context
echo "=== repo-open-session.ts: Check source validation before openSession ==="
sed -n '40,70p' packages/worker/src/mcp/capabilities/repo/repo-open-session.ts
echo -e "\n=== run-saved-skill.ts: Check source validation context ==="
sed -n '85,110p' packages/worker/src/mcp/skills/run-saved-skill.ts
echo -e "\n=== jobs/service.ts: Check job/source setup ==="
sed -n '445,475p' packages/worker/src/jobs/service.ts
echo -e "\n=== app-source.ts: Check artifact source constraints ==="
sed -n '65,95p' packages/worker/src/repo/app-source.ts
echo -e "\n=== Check if these sources are created via published flows ==="
echo "Checking source creation context..."
rg -n "sourceId|source_id" packages/worker/src/mcp/skills/run-saved-skill.ts | head -5Repository: kentcdodds/kody
Length of output: 4159
🏁 Script executed:
#!/bin/bash
# Check repoSessionNeedsRefresh to understand the refresh logic
echo "=== Check repoSessionNeedsRefresh ==="
rg -n "repoSessionNeedsRefresh" packages/worker/src/ -A 5 -B 2
# Check if skills and jobs require published sources
echo -e "\n=== Check skill source constraints ==="
rg -n "getMcpSkillByNameInput|skill.*source" packages/worker/src/mcp/skills/run-saved-skill.ts | head -10
# Check artifact source constraints
echo -e "\n=== Check canResolveRepoBackedSource ==="
rg -n "canResolveRepoBackedSource" packages/worker/src/repo/app-source.ts -B 10 -A 10Repository: kentcdodds/kody
Length of output: 3362
🏁 Script executed:
#!/bin/bash
# Check the actual error that would be thrown and when it happens
echo "=== Check openSession implementation (lines 364-393) ==="
sed -n '364,395p' packages/worker/src/repo/repo-session-do.ts
# Check if job sources are guaranteed to be published
echo -e "\n=== Check JobRecord and job source setup ==="
rg -n "JobRecord|job.*source" packages/worker/src/jobs/ -t ts | grep -E "interface|type|sourceId" | head -15
# Check if skill sources are guaranteed to be published
echo -e "\n=== Check getMcpSkillByNameInput result ==="
rg -n "getMcpSkillByNameInput" packages/worker/src/mcp/skills/ -A 20 | head -40Repository: kentcdodds/kody
Length of output: 5718
🏁 Script executed:
#!/bin/bash
# Critical: Check if unpublished sources can be stored in jobs/skills/artifacts
echo "=== Check job creation - can sourceId point to unpublished source? ==="
rg -n "createJob|insertJob" packages/worker/src/jobs/ -A 10 -B 2 | head -40
echo -e "\n=== Check skill creation - can source_id point to unpublished source? ==="
rg -n "createSkill|meta_save_skill" packages/worker/src/mcp/skills/ -A 10 -B 2 | head -50
echo -e "\n=== Check artifact creation - can sourceId point to unpublished source? ==="
rg -n "createArtifact|insertArtifact" packages/worker/src/ -A 10 -B 2 | head -50
echo -e "\n=== Most critical: Check if repo-open-session allows unpublished sources ==="
echo "Can user request an unpublished source directly in repo-open-session?"
sed -n '40,70p' packages/worker/src/mcp/capabilities/repo/repo-open-session.tsRepository: kentcdodds/kody
Length of output: 8830
🏁 Script executed:
#!/bin/bash
# Check if bootstrapSource is exported and accessible
echo "=== Check bootstrapSource export ==="
rg -n "bootstrapSource" packages/worker/src/repo/repo-session-do.ts | head -20
echo -e "\n=== Check what is exported from repo-session-do ==="
rg -n "export" packages/worker/src/repo/repo-session-do.ts | head -20
echo -e "\n=== Critical: Check if repo-open-session can handle unpublished sources ==="
echo "Does repo-open-session MCP capability have any bootstrap logic?"
rg -n "bootstrap" packages/worker/src/mcp/capabilities/repo/ -t tsRepository: kentcdodds/kody
Length of output: 465
🏁 Script executed:
#!/bin/bash
# Check if bootstrapSource is accessible via RPC and if there's a bootstrap MCP capability
echo "=== Check bootstrapSource in RepoSession class ==="
sed -n '458,480p' packages/worker/src/repo/repo-session-do.ts
echo -e "\n=== Check if bootstrapSource is accessible via repoSessionRpc ==="
rg -n "bootstrapSource" packages/worker/src/ -t ts | grep -v test
echo -e "\n=== Check all MCP capabilities for repo ==="
rg -n "defineCapability" packages/worker/src/mcp/capabilities/repo/ -A 3 -B 1
echo -e "\n=== Check if there's a separate bootstrap capability ==="
find packages/worker/src/mcp/capabilities -name "*bootstrap*" -o -name "*source*"Repository: kentcdodds/kody
Length of output: 1305
🏁 Script executed:
#!/bin/bash
# Check if there's a way to bootstrap sources before repo-open-session
echo "=== Check if there's a bootstrap MCP capability ==="
rg -n "defineCapability.*bootstrap" packages/worker/src/mcp/capabilities/
echo -e "\n=== Check repo-open-session full implementation ==="
sed -n '1,100p' packages/worker/src/mcp/capabilities/repo/repo-open-session.ts
echo -e "\n=== Check if source is guaranteed published by how it's looked up ==="
rg -n "getEntitySourceById" packages/worker/src/mcp/capabilities/repo/repo-open-session.ts -B 5 -A 5Repository: kentcdodds/kody
Length of output: 3628
🏁 Script executed:
#!/bin/bash
# Verify if bootstrapSource is truly accessible via RPC
echo "=== Check repo-session-rpc.ts to see if bootstrapSource is callable ==="
sed -n '70,80p' packages/worker/src/repo/repo-session-rpc.ts
echo -e "\n=== Check the full RPC export ==="
rg -n "export.*repoSessionRpc" packages/worker/src/repo/repo-session-rpc.ts -A 30
echo -e "\n=== Verify: Is there any way to bootstrap before repo-open-session? ==="
rg -n "bootstrap" packages/worker/src/mcp/capabilities/ -t tsRepository: kentcdodds/kody
Length of output: 4358
🏁 Script executed:
#!/bin/bash
# Final check: Can sources be unpublished when reached through these callers?
echo "=== Check if there's a validation that ensures sources are published ==="
rg -n "published" packages/worker/src/repo/entity-sources.ts -B 3 -A 3
echo -e "\n=== Check how sources are created/persisted ==="
rg -n "ensureEntitySource" packages/worker/src/ -t ts | head -10
echo -e "\n=== Most critical: Could an unpublished source reach repo-open-session? ==="
echo "What if a user calls repo_open_session with a just-created source?"
sed -n '1,20p' packages/worker/src/repo/source-service.tsRepository: kentcdodds/kody
Length of output: 3397
🏁 Script executed:
#!/bin/bash
# Final confirmation: check if ensureEntitySource can create unpublished sources
echo "=== Check ensureEntitySource in jobs service ==="
sed -n '150,180p' packages/worker/src/jobs/service.ts
echo -e "\n=== Verify: Can repo-open-session receive an unpublished source? ==="
echo "Scenario: User creates a job with a source, then tries to open repo session"
rg -n "ensureEntitySource" packages/worker/src/jobs/service.ts -B 2 -A 8Repository: kentcdodds/kody
Length of output: 2386
🏁 Script executed:
#!/bin/bash
# Final verification: Can an unpublished source actually be created and reach openSession?
echo "=== Check if ensureEntitySource can leave sources unpublished ==="
sed -n '47,95p' packages/worker/src/repo/source-service.ts
echo -e "\n=== Conclusion: Is bootstrapSource exported/accessible? ==="
echo "Checking all exports from repo-session-do.ts..."
grep "^export" packages/worker/src/repo/repo-session-do.tsRepository: kentcdodds/kody
Length of output: 1727
🏁 Script executed:
#!/bin/bash
# One final check: Can sources reach openSession calls while unpublished?
echo "=== Check buildEntitySourceRow - does it set published_commit? ==="
sed -n '68,100p' packages/worker/src/repo/source-service.ts
echo -e "\n=== Check: Does any of these callers validate published_commit? ==="
echo "Checking each caller for published_commit validation..."
for file in \
"packages/worker/src/mcp/skills/run-saved-skill.ts" \
"packages/worker/src/jobs/service.ts" \
"packages/worker/src/repo/app-source.ts" \
"packages/worker/src/mcp/capabilities/repo/repo-open-session.ts"
do
echo "=== $file ==="
rg -n "published" "$file"
doneRepository: kentcdodds/kody
Length of output: 2630
Tightens the invariant, but creates a breaking change for unpublished sources without a bootstrap path.
While the invariant is sound, the new error in openSession will break callers that pass unpublished sources—specifically the public repo_open_session MCP capability and other callers like run-saved-skill.ts, jobs/service.ts, and app-source.ts. These sources can be created unpublished via ensureEntitySource (which doesn't set published_commit), but there is no public way to bootstrap them:
bootstrapSourceis not exported fromrepo-session-do.ts- No public MCP
bootstrapcapability exists - Only
source-sync.tshas the pattern to checkif (!source.published_commit)before deciding to bootstrap
The suggestion to "route unpublished sources through bootstrapSource first" is not feasible for the public repo_open_session MCP API or other existing callers. Either provide a public bootstrap capability or ensure all source creation paths guarantee published_commit is set before this change takes effect.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/worker/src/repo/repo-session-do.ts` around lines 389 - 393, The new
throw in openSession tightens the invariant but breaks callers that pass
unpublished sources (e.g., repo_open_session, run-saved-skill.ts,
jobs/service.ts, app-source.ts) because ensureEntitySource can create sources
without published_commit and bootstrapSource isn't exported; fix by changing
openSession in repo-session-do.ts to detect missing source.published_commit and
either (A) call bootstrapSource internally to create the published commit before
proceeding (ensure bootstrapSource is accessible in this module and handle
errors/logging), or (B) if you prefer explicit flow, export bootstrapSource and
add a public MCP capability so callers (including repo_open_session) can
bootstrap prior to calling openSession; reference symbols: openSession,
bootstrapSource, published_commit, repo_open_session, ensureEntitySource.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Guard in
openSessionbreaks fallback for unpublished sources- Removed the early error so
openSessionagain tolerates missing published commits and allows the inline fallback path to proceed.
- Removed the early error so
Preview (4e2e7cf39b)
diff --git a/packages/worker/src/repo/repo-session-do.ts b/packages/worker/src/repo/repo-session-do.ts
--- a/packages/worker/src/repo/repo-session-do.ts
+++ b/packages/worker/src/repo/repo-session-do.ts
@@ -33,6 +33,7 @@
type RepoApplyPatchResult,
type RepoSearchMode,
type RepoSearchOutputMode,
+ type RepoSourceBootstrapResult,
type RepoSessionApplyEditsResult,
type RepoSessionCheckRun,
type RepoSessionCheckStatus,
@@ -169,6 +170,18 @@
})
}
+ private async resetWorkspace() {
+ await this.workspace
+ .rm(repoSessionWorkspacePrefix, {
+ force: true,
+ recursive: true,
+ })
+ .catch(() => {
+ // Best effort only; the session row is the source of truth.
+ })
+ this.initializedSessionId = null
+ }
+
private async getCurrentBranch(defaultBranch = defaultSessionBranch) {
const branchResult = await this.git.branch({
dir: repoSessionWorkspacePrefix,
@@ -271,6 +284,83 @@
this.initializedSessionId = input.sessionId
}
+ private async applyWorkspaceEdits(input: {
+ edits: Array<{
+ kind: 'write' | 'replace' | 'writeJson'
+ path: string
+ content?: string
+ search?: string
+ replacement?: string
+ value?: unknown
+ options?: {
+ caseSensitive?: boolean
+ regex?: boolean
+ wholeWord?: boolean
+ contextBefore?: number
+ contextAfter?: number
+ maxMatches?: number
+ spaces?: number
+ }
+ }>
+ dryRun?: boolean
+ rollbackOnError?: boolean
+ }): Promise<RepoSessionApplyEditsResult> {
+ const plan = await this.state.planEdits(
+ input.edits.map((edit) => {
+ const path = resolveRepoWorkspacePath(
+ edit.path,
+ repoSessionWorkspacePrefix,
+ )
+ switch (edit.kind) {
+ case 'write':
+ if (typeof edit.content !== 'string') {
+ throw new Error('repo_apply_patch write edits require content.')
+ }
+ return {
+ kind: 'write' as const,
+ path,
+ content: edit.content,
+ }
+ case 'replace':
+ if (typeof edit.search !== 'string') {
+ throw new Error('repo_apply_patch replace edits require search.')
+ }
+ return {
+ kind: 'replace' as const,
+ path,
+ search: edit.search,
+ replacement: edit.replacement ?? '',
+ options: edit.options,
+ }
+ case 'writeJson':
+ return {
+ kind: 'writeJson' as const,
+ path,
+ value: edit.value,
+ options:
+ typeof edit.options?.spaces === 'number'
+ ? { spaces: edit.options.spaces }
+ : undefined,
+ }
+ }
+ }),
+ )
+ const result = await this.state.applyEditPlan(plan, {
+ dryRun: input.dryRun,
+ rollbackOnError: input.rollbackOnError,
+ })
+ return {
+ dryRun: result.dryRun,
+ totalChanged: result.totalChanged,
+ edits: result.edits.map((edit) => ({
+ path: toExternalRepoPath(edit.path, repoSessionWorkspacePrefix),
+ changed: edit.changed,
+ content: edit.content,
+ diff: edit.diff,
+ })),
+ }
+ }
+
async openSession(input: {
sessionId: string
sourceId: string
@@ -360,6 +450,111 @@
return toRepoSessionInfoResult(sessionRow, source)
}
+ async bootstrapSource(input: {
+ sessionId: string
+ sourceId: string
+ userId: string
+ edits: Array<{
+ kind: 'write' | 'replace' | 'writeJson'
+ path: string
+ content?: string
+ search?: string
+ replacement?: string
+ value?: unknown
+ options?: {
+ caseSensitive?: boolean
+ regex?: boolean
+ wholeWord?: boolean
+ contextBefore?: number
+ contextAfter?: number
+ maxMatches?: number
+ spaces?: number
+ }
+ }>
+ }): Promise<RepoSourceBootstrapResult> {
+ const source = await getEntitySourceById(this.env.APP_DB, input.sourceId)
+ if (!source) {
+ throw new Error(`Source "${input.sourceId}" was not found.`)
+ }
+ if (source.user_id !== input.userId) {
+ throw new Error(
+ `Source "${input.sourceId}" was not found for this user.`,
+ )
+ }
+ if (source.published_commit) {
+ throw new Error(
+ `Source "${source.id}" already has a published commit. Use repo sessions for later edits.`,
+ )
+ }
+ const sourceRepo = await resolveArtifactSourceRepo(this.env, source.repo_id)
+ const sourceInfo = await sourceRepo.info()
+ const sourceAccess = await ensureArtifactRepoRemote({
+ repo: sourceRepo,
+ scope: 'write',
+ })
+ const targetBranch = sourceInfo?.defaultBranch ?? defaultSessionBranch
+ await this.resetWorkspace()
+ await this.workspace.mkdir(repoSessionWorkspacePrefix, {
+ recursive: true,
+ })
+ await this.git.init({
+ dir: repoSessionWorkspacePrefix,
+ defaultBranch: targetBranch,
+ })
+ await this.ensureRemote({
+ name: 'source',
+ url: buildAuthenticatedArtifactsRemote({
+ remote: sourceAccess.remote,
+ token: sourceAccess.token,
+ }),
+ })
+ await this.applyWorkspaceEdits({
+ edits: input.edits,
+ dryRun: false,
+ rollbackOnError: true,
+ })
+ const publishedCommit = await this.commitIfDirty(
+ `Bootstrap source repo ${source.id}`,
+ )
+ if (!publishedCommit) {
+ throw new Error(`Source "${source.id}" bootstrap produced no commit.`)
+ }
+ await this.git.push({
+ dir: repoSessionWorkspacePrefix,
+ remote: 'source',
+ ref: targetBranch,
+ token: sourceAccess.token,
+ username: 'x',
+ password: sourceAccess.token.split('?expires=')[0] ?? sourceAccess.token,
+ })
+ const manifestContent = await this.workspace.readFile(
+ resolveRepoWorkspacePath(source.manifest_path, repoSessionWorkspacePrefix),
+ )
+ if (manifestContent == null) {
+ throw new Error(`Manifest "${source.manifest_path}" was not found.`)
+ }
+ const manifest = parseRepoManifest({
+ content: manifestContent,
+ manifestPath: source.manifest_path,
+ })
+ await updateEntitySource(this.env.APP_DB, {
+ id: source.id,
+ userId: source.user_id,
+ publishedCommit,
+ manifestPath: source.manifest_path,
+ sourceRoot: manifest.sourceRoot?.startsWith('/')
+ ? manifest.sourceRoot
+ : manifest.sourceRoot
+ ? `/${manifest.sourceRoot}`
+ : source.source_root,
+ })
+ return {
+ sessionId: input.sessionId,
+ publishedCommit,
+ message: `Bootstrapped source ${source.id} in ${source.repo_id}.`,
+ }
+ }
+
async getSessionInfo(input: { sessionId: string; userId: string }) {
const { sessionRow, source } = await this.getSessionState(
input.sessionId,
@@ -377,6 +572,7 @@
input.sessionId,
)
if (!sessionRow) {
+ await this.resetWorkspace()
return {
ok: true,
sessionId: input.sessionId,
@@ -389,14 +585,7 @@
)
}
await deleteRepoSession(this.env.APP_DB, input.sessionId)
- try {
- await this.workspace.rm(repoSessionWorkspacePrefix, {
- force: true,
- recursive: true,
- })
- } catch {
- // Best effort only; the session row is the source of truth.
- }
+ await this.resetWorkspace()
return {
ok: true,
sessionId: input.sessionId,
@@ -569,65 +758,13 @@
input.sessionId,
input.userId,
)
- const plan = await this.state.planEdits(
- input.edits.map((edit) => {
- const path = resolveRepoWorkspacePath(
- edit.path,
- repoSessionWorkspacePrefix,
- )
- switch (edit.kind) {
- case 'write':
- if (typeof edit.content !== 'string') {
- throw new Error('repo_apply_patch write edits require content.')
- }
- return {
- kind: 'write' as const,
- path,
- content: edit.content,
- }
- case 'replace':
- if (typeof edit.search !== 'string') {
- throw new Error('repo_apply_patch replace edits require search.')
- }
- return {
- kind: 'replace' as const,
- path,
- search: edit.search,
- replacement: edit.replacement ?? '',
- options: edit.options,
- }
- case 'writeJson':
- return {
- kind: 'writeJson' as const,
- path,
- value: edit.value,
- options:
- typeof edit.options?.spaces === 'number'
- ? { spaces: edit.options.spaces }
- : undefined,
- }
- }
- }),
- )
- const result = await this.state.applyEditPlan(plan, {
- dryRun: input.dryRun,
- rollbackOnError: input.rollbackOnError,
- })
+ const result = await this.applyWorkspaceEdits(input)
await updateRepoSession(this.env.APP_DB, {
id: input.sessionId,
userId: sessionRow.user_id,
lastCheckpointAt: nowIso(),
})
- return {
- dryRun: result.dryRun,
- totalChanged: result.totalChanged,
- edits: result.edits.map((edit) => ({
- path: toExternalRepoPath(edit.path, repoSessionWorkspacePrefix),
- changed: edit.changed,
- content: edit.content,
- diff: edit.diff,
- })),
- }
+ return result
}
async runChecks(input: {
diff --git a/packages/worker/src/repo/repo-session-rpc.ts b/packages/worker/src/repo/repo-session-rpc.ts
--- a/packages/worker/src/repo/repo-session-rpc.ts
+++ b/packages/worker/src/repo/repo-session-rpc.ts
@@ -1,4 +1,5 @@
import {
+ type RepoSourceBootstrapResult,
type RepoSearchMode,
type RepoSearchOutputMode,
type RepoSessionApplyEditsResult,
@@ -68,6 +69,12 @@
dryRun?: boolean
rollbackOnError?: boolean
}) => Promise<RepoSessionApplyEditsResult>
+ bootstrapSource: (payload: {
+ sessionId: string
+ sourceId: string
+ userId: string
+ edits: Array<RepoSessionEdit>
+ }) => Promise<RepoSourceBootstrapResult>
runChecks: (payload: {
sessionId: string
userId: string
diff --git a/packages/worker/src/repo/source-sync.node.test.ts b/packages/worker/src/repo/source-sync.node.test.ts
new file mode 100644
--- /dev/null
+++ b/packages/worker/src/repo/source-sync.node.test.ts
@@ -1,0 +1,193 @@
+import { expect, test, vi } from 'vitest'
+
+const mockModule = vi.hoisted(() => ({
+ getEntitySourceById: vi.fn(),
+ repoSessionRpc: vi.fn(),
+}))
+
+vi.mock('./entity-sources.ts', () => ({
+ getEntitySourceById: (...args: Array<unknown>) =>
+ mockModule.getEntitySourceById(...args),
+}))
+
+vi.mock('./repo-session-do.ts', () => ({
+ repoSessionRpc: (...args: Array<unknown>) => mockModule.repoSessionRpc(...args),
+}))
+
+const { syncArtifactSourceSnapshot } = await import('./source-sync.ts')
+
+test('syncArtifactSourceSnapshot bootstraps unpublished sources directly into the source repo', async () => {
+ mockModule.getEntitySourceById.mockReset()
+ mockModule.repoSessionRpc.mockReset()
+
+ const sessionClient = {
+ bootstrapSource: vi.fn(async () => ({
+ sessionId: 'source-sync-source-1-session',
+ publishedCommit: 'commit-bootstrap-1',
+ message: 'Bootstrapped source source-1 in app-1.',
+ })),
+ openSession: vi.fn(),
+ applyEdits: vi.fn(),
+ publishSession: vi.fn(),
+ discardSession: vi.fn(async () => ({
+ ok: true as const,
+ sessionId: 'source-sync-source-1-session',
+ deleted: false,
+ })),
+ }
+
+ mockModule.getEntitySourceById.mockResolvedValue({
+ id: 'source-1',
+ user_id: 'user-1',
+ entity_kind: 'app',
+ entity_id: 'app-1',
+ repo_id: 'app-1',
+ published_commit: null,
+ indexed_commit: null,
+ manifest_path: 'kody.json',
+ source_root: '/',
+ created_at: '2026-04-18T00:00:00.000Z',
+ updated_at: '2026-04-18T00:00:00.000Z',
+ })
+ mockModule.repoSessionRpc.mockReturnValue(sessionClient as never)
+
+ const publishedCommit = await syncArtifactSourceSnapshot({
+ env: {
+ APP_DB: {
+ prepare() {
+ return {} as D1PreparedStatement
+ },
+ },
+ REPO_SESSION: {},
+ CLOUDFLARE_ACCOUNT_ID: 'account-1',
+ CLOUDFLARE_API_TOKEN: 'token-1',
+ } as unknown as Env,
+ userId: 'user-1',
+ baseUrl: 'https://heykody.dev',
+ sourceId: 'source-1',
+ files: {
+ 'kody.json': '{"version":1,"kind":"app"}',
+ 'client.html': '<main>Hello</main>',
+ },
+ })
+
+ expect(publishedCommit).toBe('commit-bootstrap-1')
+ expect(sessionClient.bootstrapSource).toHaveBeenCalledWith({
+ sessionId: expect.stringMatching(/^source-sync-source-1-/),
+ sourceId: 'source-1',
+ userId: 'user-1',
+ edits: [
+ {
+ kind: 'write',
+ path: 'kody.json',
+ content: '{"version":1,"kind":"app"}',
+ },
+ {
+ kind: 'write',
+ path: 'client.html',
+ content: '<main>Hello</main>',
+ },
+ ],
+ })
+ expect(sessionClient.openSession).not.toHaveBeenCalled()
+ expect(sessionClient.applyEdits).not.toHaveBeenCalled()
+ expect(sessionClient.publishSession).not.toHaveBeenCalled()
+ expect(sessionClient.discardSession).toHaveBeenCalledWith({
+ sessionId: expect.stringMatching(/^source-sync-source-1-/),
+ userId: 'user-1',
+ })
+})
+
+test('syncArtifactSourceSnapshot still uses repo sessions for already-published sources', async () => {
+ mockModule.getEntitySourceById.mockReset()
+ mockModule.repoSessionRpc.mockReset()
+
+ const sessionClient = {
+ bootstrapSource: vi.fn(),
+ openSession: vi.fn(async () => ({
+ id: 'source-sync-source-1-session',
+ })),
+ applyEdits: vi.fn(async () => ({
+ dryRun: false,
+ totalChanged: 1,
+ edits: [],
+ })),
+ publishSession: vi.fn(async () => ({
+ status: 'ok' as const,
+ sessionId: 'source-sync-source-1-session',
+ publishedCommit: 'commit-session-2',
+ message: 'Published session source-sync-source-1-session to app-1.',
+ })),
+ discardSession: vi.fn(async () => ({
+ ok: true as const,
+ sessionId: 'source-sync-source-1-session',
+ deleted: true,
+ })),
+ }
+
+ mockModule.getEntitySourceById.mockResolvedValue({
+ id: 'source-1',
+ user_id: 'user-1',
+ entity_kind: 'app',
+ entity_id: 'app-1',
+ repo_id: 'app-1',
+ published_commit: 'commit-existing-1',
+ indexed_commit: 'commit-existing-1',
+ manifest_path: 'kody.json',
+ source_root: '/',
+ created_at: '2026-04-18T00:00:00.000Z',
+ updated_at: '2026-04-18T00:00:00.000Z',
+ })
+ mockModule.repoSessionRpc.mockReturnValue(sessionClient as never)
+
+ const publishedCommit = await syncArtifactSourceSnapshot({
+ env: {
+ APP_DB: {
+ prepare() {
+ return {} as D1PreparedStatement
+ },
+ },
+ REPO_SESSION: {},
+ CLOUDFLARE_ACCOUNT_ID: 'account-1',
+ CLOUDFLARE_API_TOKEN: 'token-1',
+ } as unknown as Env,
+ userId: 'user-1',
+ baseUrl: 'https://heykody.dev',
+ sourceId: 'source-1',
+ files: {
+ 'kody.json': '{"version":1,"kind":"app"}',
+ },
+ })
+
+ expect(publishedCommit).toBe('commit-session-2')
+ expect(sessionClient.bootstrapSource).not.toHaveBeenCalled()
+ expect(sessionClient.openSession).toHaveBeenCalledWith({
+ sessionId: expect.stringMatching(/^source-sync-source-1-/),
+ sourceId: 'source-1',
+ userId: 'user-1',
+ baseUrl: 'https://heykody.dev',
+ sourceRoot: '/',
+ })
+ expect(sessionClient.applyEdits).toHaveBeenCalledWith({
+ sessionId: expect.stringMatching(/^source-sync-source-1-/),
+ userId: 'user-1',
+ edits: [
+ {
+ kind: 'write',
+ path: 'kody.json',
+ content: '{"version":1,"kind":"app"}',
+ },
+ ],
+ dryRun: false,
+ rollbackOnError: true,
+ })
+ expect(sessionClient.publishSession).toHaveBeenCalledWith({
+ sessionId: expect.stringMatching(/^source-sync-source-1-/),
+ userId: 'user-1',
+ force: true,
+ })
+ expect(sessionClient.discardSession).toHaveBeenCalledWith({
+ sessionId: expect.stringMatching(/^source-sync-source-1-/),
+ userId: 'user-1',
+ })
+})
diff --git a/packages/worker/src/repo/source-sync.ts b/packages/worker/src/repo/source-sync.ts
--- a/packages/worker/src/repo/source-sync.ts
+++ b/packages/worker/src/repo/source-sync.ts
@@ -34,7 +34,21 @@
if (!source) return null
const sessionId = buildSyncSessionId(source.id)
const session = repoSessionRpc(input.env, sessionId)
+ const edits = Object.entries(input.files).map(([path, content]) => ({
+ kind: 'write' as const,
+ path,
+ content,
+ }))
try {
+ if (!source.published_commit) {
+ const bootstrapResult = await session.bootstrapSource({
+ sessionId,
+ sourceId: source.id,
+ userId: input.userId,
+ edits,
+ })
+ return bootstrapResult.publishedCommit
+ }
await session.openSession({
sessionId,
sourceId: source.id,
@@ -45,11 +59,7 @@
await session.applyEdits({
sessionId,
userId: input.userId,
- edits: Object.entries(input.files).map(([path, content]) => ({
- kind: 'write' as const,
- path,
- content,
- })),
+ edits,
dryRun: false,
rollbackOnError: true,
})
diff --git a/packages/worker/src/repo/types.ts b/packages/worker/src/repo/types.ts
--- a/packages/worker/src/repo/types.ts
+++ b/packages/worker/src/repo/types.ts
@@ -326,6 +326,12 @@
publishedCommit: null
}
+export type RepoSourceBootstrapResult = {
+ sessionId: string
+ publishedCommit: string
+ message: string
+}
+
export type RepoSessionRebaseResult = {
ok: true
sessionId: stringYou can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit 9feeb84. Configure here.
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
packages/worker/src/repo/repo-session-do.ts (1)
516-539:⚠️ Potential issue | 🟠 MajorValidate the manifest before pushing to the source remote.
Line 522 pushes before Lines 530-539 read and parse the manifest. If parsing fails, the source remote already contains the commit while
entity_sources.published_commitstays null, making retries fragile.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/worker/src/repo/repo-session-do.ts` around lines 516 - 539, The code pushes the commit (git.push in this block) before reading/parsing the manifest, which can leave the remote updated if parseRepoManifest or workspace.readFile fails and entity_sources.published_commit remains null; move the manifest validation earlier: call workspace.readFile and parseRepoManifest (using resolveRepoWorkspacePath and parseRepoManifest) and ensure manifestContent is non-null and parse succeeds before calling this.git.push, or alternatively delay the push until after you successfully set entity_sources.published_commit (i.e., validate manifest after commitIfDirty but before this.git.push) so failures don’t leave a pushed but unrecorded published_commit.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/worker/src/repo/repo-session-do.ts`:
- Around line 173-181: The current resetWorkspace() swallows fs.rm errors which
allows stale workspace files (including .git created by bootstrapSource()) to
persist and cause initialize() to skip cloning; change resetWorkspace() to treat
workspace removal as a hard precondition by propagating or throwing errors on
failure (do not catch-and-ignore), ensure bootstrapSource() explicitly removes
any direct-source .git under the session if present, and add a guaranteed
cleanup step after bootstrapSource() (or before initialize()) that
retries/validates removal of repoSessionWorkspacePrefix and the .git/config file
so initialize() always sees a clean workspace; refer to resetWorkspace(),
bootstrapSource(), and initialize() when making these changes.
---
Duplicate comments:
In `@packages/worker/src/repo/repo-session-do.ts`:
- Around line 516-539: The code pushes the commit (git.push in this block)
before reading/parsing the manifest, which can leave the remote updated if
parseRepoManifest or workspace.readFile fails and
entity_sources.published_commit remains null; move the manifest validation
earlier: call workspace.readFile and parseRepoManifest (using
resolveRepoWorkspacePath and parseRepoManifest) and ensure manifestContent is
non-null and parse succeeds before calling this.git.push, or alternatively delay
the push until after you successfully set entity_sources.published_commit (i.e.,
validate manifest after commitIfDirty but before this.git.push) so failures
don’t leave a pushed but unrecorded published_commit.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: 751f141c-7107-418d-9939-5b836cf14c58
📒 Files selected for processing (1)
packages/worker/src/repo/repo-session-do.ts
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>

Summary
.gitstate cannot be silently reused by later session clonespublished_commitValidation
npx vitest run --project node-unit "packages/worker/src/repo/source-sync.node.test.ts" "packages/worker/src/repo/source-service.node.test.ts" "packages/worker/src/repo/source-backfill.node.test.ts"npx vitest run --project node-unit "packages/worker/src/repo/source-sync.node.test.ts" "packages/worker/src/repo/source-service.node.test.ts" "packages/worker/src/repo/source-backfill.node.test.ts" "packages/worker/src/repo/app-source.node.test.ts" "packages/worker/src/jobs/service.node.test.ts"npx tsc -b --noEmitNotes
package-lock.jsonworking-tree change untouchedSummary by CodeRabbit
New Features
Improvements
Tests