Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ export const getGitRemoteCapability = defineDomainCapability(
`git -c http.extraHeader=${shellQuote(gitExtraHeader)} clone ${shellQuote(info.remote)} ${shellQuote(cloneDirectory)}`,
`cd ${shellQuote(cloneDirectory)}`,
`git remote add kody ${shellQuote(info.remote)}`,
`git config remote.kody.fetch '+refs/heads/*:refs/remotes/kody/*'`,
`git config --add remote.kody.fetch '+refs/notes/*:refs/notes/*'`,
`git -c http.extraHeader=${shellQuote(gitExtraHeader)} fetch kody 'refs/notes/*:refs/notes/*'`,
`git -c http.extraHeader=${shellQuote(gitExtraHeader)} push kody HEAD:${shellQuote(info.defaultBranch ?? 'main')}`,
],
}
Expand Down
2 changes: 2 additions & 0 deletions packages/worker/src/mcp/capabilities/repo/domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { repoReadFileCapability } from './repo-read-file.ts'
import { repoRebaseSessionCapability } from './repo-rebase-session.ts'
import { repoRunChecksCapability } from './repo-run-checks.ts'
import { repoRunCommandsCapability } from './repo-run-commands.ts'
import { repoShowPublishNoteCapability } from './repo-show-publish-note.ts'
import { repoSearchCapability } from './repo-search.ts'
import { repoTreeCapability } from './repo-tree.ts'
import { repoWriteFileCapability } from './repo-write-file.ts'
Expand All @@ -28,6 +29,7 @@ export const repoDomain = defineDomain({
repoSearchCapability,
repoRunChecksCapability,
repoGetCheckStatusCapability,
repoShowPublishNoteCapability,
repoPublishSessionCapability,
repoRebaseSessionCapability,
repoDiscardSessionCapability,
Expand Down
1 change: 1 addition & 0 deletions packages/worker/src/mcp/capabilities/repo/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export { repoReadFileCapability } from './repo-read-file.ts'
export { repoRebaseSessionCapability } from './repo-rebase-session.ts'
export { repoRunCommandsCapability } from './repo-run-commands.ts'
export { repoRunChecksCapability } from './repo-run-checks.ts'
export { repoShowPublishNoteCapability } from './repo-show-publish-note.ts'
export { repoSearchCapability } from './repo-search.ts'
export { repoTreeCapability } from './repo-tree.ts'
export { repoWriteFileCapability } from './repo-write-file.ts'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { beforeEach, expect, test, vi } from 'vitest'

const mockModule = vi.hoisted(() => ({
getEntitySourceById: vi.fn(),
resolveOwnedPackageSource: vi.fn(),
readPublishGitNoteFromArtifactsRepo: vi.fn(),
}))

vi.mock('#worker/repo/entity-sources.ts', () => ({
getEntitySourceById: (...args: Array<unknown>) =>
mockModule.getEntitySourceById(...args),
}))

vi.mock('#mcp/capabilities/packages/resolve-package-source.ts', () => ({
resolveOwnedPackageSource: (...args: Array<unknown>) =>
mockModule.resolveOwnedPackageSource(...args),
}))

vi.mock('#worker/repo/publish-git-notes.ts', async () => {
const actual = await vi.importActual('#worker/repo/publish-git-notes.ts')
return {
...(actual as object),
readPublishGitNoteFromArtifactsRepo: (...args: Array<unknown>) =>
mockModule.readPublishGitNoteFromArtifactsRepo(...args),
}
})

const { repoShowPublishNoteCapability } =
await import('./repo-show-publish-note.ts')

function createContext(userId = 'user-1') {
return {
env: { APP_DB: {} } as Env,
callerContext: {
baseUrl: 'https://heykody.dev',
user: {
userId,
email: `${userId}@example.com`,
displayName: userId,
},
remoteConnectors: null,
storageContext: null,
repoContext: null,
},
}
}

const sampleSource = {
id: 'source-1',
user_id: 'user-1',
entity_kind: 'package' as const,
entity_id: 'package-1',
repo_id: 'package-package-1',
published_commit: 'commit-published',
indexed_commit: null,
manifest_path: 'package.json',
source_root: '/',
last_external_check_at: null,
created_at: '2026-05-28T00:00:00.000Z',
updated_at: '2026-05-28T00:00:00.000Z',
}

const sampleNote = {
v: 1 as const,
publishedAt: '2026-05-28T12:00:00.000Z',
publishedBy: 'repo_session' as const,
sourceId: 'source-1',
entityKind: 'package' as const,
entityId: 'package-1',
repoId: 'package-package-1',
commit: 'commit-published',
sessionId: 'session-1',
conversationId: null,
previousPublishedCommit: null,
baseCommit: 'commit-base',
checks: null,
}

// eslint-disable-next-line epic-web/prefer-dispose-in-tests -- shared hoisted mocks reset between tests.
beforeEach(() => {
for (const fn of Object.values(mockModule)) {
fn.mockReset()
}
})

test('repo_show_publish_note reads the note for a source id', async () => {
mockModule.getEntitySourceById.mockResolvedValue(sampleSource)
mockModule.readPublishGitNoteFromArtifactsRepo.mockResolvedValue({
found: true,
commit: 'commit-published',
rawNote: JSON.stringify(sampleNote),
note: sampleNote,
})

const result = await repoShowPublishNoteCapability.handler(
{ source_id: 'source-1' },
createContext(),
)

expect(mockModule.readPublishGitNoteFromArtifactsRepo).toHaveBeenCalledWith({
env: expect.objectContaining({ APP_DB: {} }),
repoId: 'package-package-1',
commitOid: 'commit-published',
})
expect(result.found).toBe(true)
expect(result.note?.publishedBy).toBe('repo_session')
})

test('repo_show_publish_note resolves package identity and explicit commit', async () => {
mockModule.resolveOwnedPackageSource.mockResolvedValue({
packageId: 'package-1',
kodyId: 'demo',
name: '@kent/demo',
source: sampleSource,
})
mockModule.readPublishGitNoteFromArtifactsRepo.mockResolvedValue({
found: false,
commit: 'commit-old',
rawNote: null,
note: null,
})

const result = await repoShowPublishNoteCapability.handler(
{ kody_id: 'demo', commit: 'commit-old' },
createContext(),
)

expect(result.found).toBe(false)
expect(result.commit).toBe('commit-old')
})

test('repo_show_publish_note rejects cross-user sources', async () => {
mockModule.getEntitySourceById.mockResolvedValue({
...sampleSource,
user_id: 'other-user',
})
await expect(
repoShowPublishNoteCapability.handler(
{ source_id: 'source-1' },
createContext(),
),
).rejects.toThrow('Repo source was not found for this user.')
})
112 changes: 112 additions & 0 deletions packages/worker/src/mcp/capabilities/repo/repo-show-publish-note.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { z } from 'zod'
import { defineDomainCapability } from '#mcp/capabilities/define-domain-capability.ts'
import { capabilityDomainNames } from '#mcp/capabilities/domain-metadata.ts'
import { requireMcpUser } from '#mcp/capabilities/meta/require-user.ts'
import { resolveOwnedPackageSource } from '#mcp/capabilities/packages/resolve-package-source.ts'
import { getEntitySourceById } from '#worker/repo/entity-sources.ts'
import {
kodyPublishGitNoteSchema,
readPublishGitNoteFromArtifactsRepo,
} from '#worker/repo/publish-git-notes.ts'

const inputSchema = z
.object({
source_id: z
.string()
.min(1)
.optional()
.describe('Shared entity source id for any repo-backed entity.'),
package_id: z.string().min(1).optional(),
kody_id: z.string().min(1).optional(),
commit: z
.string()
.min(1)
.optional()
.describe(
'Commit oid to inspect. Defaults to the source published_commit.',
),
})
.superRefine((value, ctx) => {
const count =
(value.source_id !== undefined ? 1 : 0) +
(value.package_id !== undefined ? 1 : 0) +
(value.kody_id !== undefined ? 1 : 0)
if (count !== 1) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['source_id'],
message:
'Provide exactly one of `source_id`, `package_id`, or `kody_id`.',
})
}
})

const outputSchema = z.object({
found: z.boolean(),
commit: z.string(),
source_id: z.string(),
repo_id: z.string(),
raw_note: z.string().nullable(),
note: kodyPublishGitNoteSchema.nullable(),
})

export const repoShowPublishNoteCapability = defineDomainCapability(
capabilityDomainNames.repo,
{
name: 'repo_show_publish_note',
description:
'Read the Kody publish harness git note attached to a published Artifacts commit. Use this to inspect publish provenance, session/conversation ids, and check summaries without opening D1.',
keywords: [
'repo',
'git',
'notes',
'publish',
'audit',
'provenance',
'artifacts',
],
readOnly: true,
idempotent: true,
destructive: false,
inputSchema,
outputSchema,
async handler(args, ctx) {
const user = requireMcpUser(ctx.callerContext)
const source =
args.source_id !== undefined
? await getEntitySourceById(ctx.env.APP_DB, args.source_id)
: (
await resolveOwnedPackageSource({
db: ctx.env.APP_DB,
userId: user.userId,
args: {
package_id: args.package_id,
kody_id: args.kody_id,
},
})
).source
if (!source || source.user_id !== user.userId) {
throw new Error('Repo source was not found for this user.')
}
const commit = args.commit ?? source.published_commit
if (!commit) {
throw new Error(
`Source "${source.id}" has no published commit yet. Pass an explicit \`commit\` or publish source first.`,
)
}
const result = await readPublishGitNoteFromArtifactsRepo({
env: ctx.env,
repoId: source.repo_id,
commitOid: commit,
})
return {
found: result.found,
commit: result.commit,
source_id: source.id,
repo_id: source.repo_id,
raw_note: result.rawNote,
note: result.note,
}
},
},
)
Loading
Loading