Skip to content
Closed
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
93 changes: 68 additions & 25 deletions examples/linear-shipper/agent.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,35 @@
import { handler } from '@agentworkforce/runtime';
import {
draftFile,
encodeSegment,
handler,
readJsonFile,
resolveMountRoot,
writeJsonFile,
type IntegrationClientOptions,
type WorkforceCtx
} from '@agentworkforce/runtime';

type LinearIssueEvent = {
issue?: { id?: string; identifier?: string; title?: string; url?: string };
};

function inputDefault(ctx: Parameters<Parameters<typeof handler>[0]>[0], name: string): string {
interface LinearIssueFile {
id?: string;
identifier?: string;
title?: string;
description?: string;
url?: string;
[key: string]: unknown;
}

function vfsClient(): IntegrationClientOptions {
return { relayfileMountRoot: resolveMountRoot({}) };
}

function inputDefault(ctx: WorkforceCtx, name: string): string {
// Mirror `resolvePersonaInputs` precedence (packages/persona-kit/src/inputs.ts):
// explicit env var (spec.env ?? input name) wins over the runtime-resolved
// value, which in turn wins over the static spec default.
//
// NOTE: the raw spec lives at `ctx.persona.inputSpecs` (Record<string, PersonaInputSpec>);
// `ctx.persona.inputs` is the already-resolved Record<string, string>. The earlier
// version of this helper read .env / .default off `inputs` directly, which only
// worked because the type was looser before the WorkforceCtx readonly tightening.
const spec = ctx.persona.inputSpecs?.[name];
const envName = spec?.env ?? name;
const fromEnv = process.env[envName];
Expand All @@ -37,8 +54,6 @@ function safeRepoDirName(value: string): string {

export default handler(async (ctx, event) => {
if (event.source !== 'linear' || event.type !== 'issue.created') return;
if (!ctx.linear) throw new Error('linear-shipper requires the linear integration');
if (!ctx.github) throw new Error('linear-shipper requires the github integration');

const payload =
typeof event.payload === 'object' && event.payload !== null
Expand All @@ -48,31 +63,59 @@ export default handler(async (ctx, event) => {
const issueId = issueRef?.id ?? issueRef?.identifier;
if (!issueId) throw new Error('Linear event is missing an issue id');

const issue = await ctx.linear.getIssue(issueId);
const client = vfsClient();
const issue = await readJsonFile<LinearIssueFile>(
client,
'linear',
'getIssue',
`/linear/issues/${encodeSegment(issueId)}.json`
);

const repoUrl = inputDefault(ctx, 'REPO_URL');
const owner = inputDefault(ctx, 'GITHUB_OWNER');
const repo = safeRepoDirName(inputDefault(ctx, 'GITHUB_REPO'));
const repoDir = `${ctx.sandbox.cwd}/${repo}`;

await ctx.sandbox.exec(`git clone ${shellQuote(repoUrl)} ${shellQuote(repoDir)}`);
const result = await ctx.harness.run({
prompt: `Implement this Linear issue. Create the smallest reviewable change and include verification notes.\n\nTitle: ${issue.title}\n\n${issue.description ?? ''}`,
prompt: `Implement this Linear issue. Create the smallest reviewable change and include verification notes.\n\nTitle: ${issue.title ?? ''}\n\n${issue.description ?? ''}`,
cwd: repoDir
});

// TODO(human): createPr is not in the published GithubClient contract yet.
const created = await ctx.github.createIssue({
owner,
repo,
title: `Draft PR needed: ${issue.title}`,
body: [
`Linear issue: ${issue.url ?? issueId}`,
'',
'The harness produced an implementation attempt, but GithubClient.createPr is not exposed yet.',
'',
result.output
].join('\n')
});
// No createPullRequest writeback path yet — fall back to a placeholder issue
// so the workflow stays observable end-to-end.
const created = await writeJsonFile(
client,
'github',
'createIssue',
`/github/repos/${encodeSegment(owner)}/${encodeSegment(repo)}/issues/${draftFile('create issue')}`,
{
title: `Draft PR needed: ${issue.title ?? issueId}`,
body: [
`Linear issue: ${issue.url ?? issueId}`,
'',
'Implementation attempt captured below; the github createPullRequest writeback is not exposed yet.',
'',
result.output
].join('\n')
}
);

await ctx.linear.comment(issueId, `Implementation attempt captured in GitHub issue: ${created.url}`);
// Only post a back-link comment when writeback returned a real receipt —
// surfacing the in-mount draft path as if it were a clickable issue URL
// would be misleading.
const issueUrl = created.receipt?.url;
if (!issueUrl) {
ctx.log('warn', 'linear-shipper.github-issue.no-receipt', { draftPath: created.path });
return;
}
await writeJsonFile(
client,
'linear',
'comment',
`/linear/issues/${encodeSegment(issueId)}/comments/${draftFile('comment')}`,
{
body: `Implementation attempt captured in GitHub issue: ${issueUrl}`
}
);
});
51 changes: 39 additions & 12 deletions examples/notion-essay-pr/agent.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import {
draftFile,
encodeSegment,
handler,
resolveMountRoot,
writeJsonFile,
type IntegrationClientOptions,
type WorkforceCtx,
type WorkforceProviderEvent
} from '@agentworkforce/runtime';
Expand All @@ -20,6 +25,10 @@ interface RepoTarget {
repo: string;
}

function vfsClient(): IntegrationClientOptions {
return { relayfileMountRoot: resolveMountRoot({}) };
}
Comment on lines +28 to +30
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Show the default writeback timeout constant and where it's applied
rg -nP 'DEFAULT_WRITEBACK_TIMEOUT_MS' -C2

Repository: AgentWorkforce/workforce

Length of output: 1729


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant regions in the example agent
FILE="examples/notion-essay-pr/agent.ts"
if [ -f "$FILE" ]; then
  echo "=== $FILE (lines 1-120) ==="
  nl -ba "$FILE" | sed -n '1,140p'
else
  echo "Missing $FILE"
  exit 1
fi

echo
echo "=== Search for key strings in $FILE ==="
rg -n "writebackTimeoutMs|weekly-digest|pr-pending|createPullRequest|receipt\.url|writeJsonFile|relayfileMountRoot" "$FILE" -n || true

Repository: AgentWorkforce/workforce

Length of output: 185


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="examples/notion-essay-pr/agent.ts"
[ -f "$FILE" ] || { echo "Missing $FILE"; exit 1; }

echo "=== $FILE (lines 1-140) ==="
cat -n "$FILE" | sed -n '1,140p'

echo
echo "=== Search for key strings in $FILE ==="
rg -n "writebackTimeoutMs|weekly-digest|pr-pending|createPullRequest|receipt\.url|writeJsonFile|relayfileMountRoot" "$FILE" || true

Repository: AgentWorkforce/workforce

Length of output: 5476


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the weekly-digest example and any explicit writeback timeout config
rg -n "weekly-digest" -S . || true
rg -n "writebackTimeoutMs" -S . || true
rg -n "30_000|30000|3_000|DEFAULT_WRITEBACK_TIMEOUT_MS" -S examples packages | head -n 200 || true

Repository: AgentWorkforce/workforce

Length of output: 20168


examples/notion-essay-pr: raise writebackTimeoutMs to avoid dropping the PR URL path

vfsClient() omits writebackTimeoutMs, so the runtime falls back to DEFAULT_WRITEBACK_TIMEOUT_MS = 3_000ms. For the github/createPullRequest writeback that uploads branch+commit+files, this can exceed 3s; when it does, pr.receipt?.url is undefined and the code logs notion-essay-pr.pr-pending and returns before ctx.memory.save(...).

Mirror the approach used in examples/weekly-digest/agent.ts (it sets writebackTimeoutMs: 30_000) by setting an explicit timeout in vfsClient() (e.g. 30_000ms).

🛡️ Proposed fix: set an explicit writeback timeout
 function vfsClient(): IntegrationClientOptions {
-  return { relayfileMountRoot: resolveMountRoot({}) };
+  return { relayfileMountRoot: resolveMountRoot({}), writebackTimeoutMs: 30_000 };
 }
📝 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
function vfsClient(): IntegrationClientOptions {
return { relayfileMountRoot: resolveMountRoot({}) };
}
function vfsClient(): IntegrationClientOptions {
return { relayfileMountRoot: resolveMountRoot({}), writebackTimeoutMs: 30_000 };
}
🤖 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 `@examples/notion-essay-pr/agent.ts` around lines 28 - 30, The vfsClient()
factory returns an IntegrationClientOptions object without writebackTimeoutMs,
causing the runtime to use the 3_000ms default and drop long writebacks (making
pr.receipt?.url undefined); update vfsClient() to include an explicit
writebackTimeoutMs (e.g. 30_000) in the returned object alongside
relayfileMountRoot (resolveMountRoot({})) so the github/createPullRequest
writeback has enough time to complete.


export default handler(async (ctx, event) => {
if (event.source !== 'notion' || event.type !== 'page.created') {
ctx.log('debug', 'notion-essay-pr.ignored', {
Expand All @@ -32,7 +41,6 @@ export default handler(async (ctx, event) => {
});

async function handleNotionPageCreated(ctx: WorkforceCtx, event: WorkforceProviderEvent): Promise<void> {
if (!ctx.github) throw new Error('notion-essay-pr requires the github integration');
const payload = readPayload(event.payload);
const pageId = pageIdFrom(payload);
const pageTitle = pageTitleFrom(payload, event.summary?.title);
Expand All @@ -50,18 +58,37 @@ async function handleNotionPageCreated(ctx: WorkforceCtx, event: WorkforceProvid

await ctx.files.write(outputPath, essay);
const branch = `essay/${safeBranchSegment(pageId)}`;
const pr = await ctx.github.createPullRequest({
...repoTarget,
title: `Essay: ${pageTitle}`,
body: `Drafted from Notion page ${pageId}.\n\nOutput: ${outputPath}`,
head: branch,
base: 'main',
files: {
[`output/${safeFileSegment(pageId)}.md`]: essay
const pr = await writeJsonFile(
vfsClient(),
'github',
'createPullRequest',
`/github/repos/${encodeSegment(repoTarget.owner)}/${encodeSegment(repoTarget.repo)}/pulls/${draftFile('create pr')}`,
{
title: `Essay: ${pageTitle}`,
body: `Drafted from Notion page ${pageId}.\n\nOutput: ${outputPath}`,
head: branch,
base: 'main',
files: {
[`output/${safeFileSegment(pageId)}.md`]: essay
}
}
});
);

// Only treat the PR as "opened" when the writeback worker returned a
// receipt with a real GitHub URL — `pr.path` is the in-mount draft and
// would mislead anyone who saw it in memory or logs.
const prUrl = pr.receipt?.url;
if (!prUrl) {
ctx.log('warn', 'notion-essay-pr.pr-pending', {
pageId,
pageTitle,
outputPath,
draftPath: pr.path
});
return;
}

await ctx.memory.save(`Notion essay PR opened for ${pageTitle}: ${pr.url}`, {
await ctx.memory.save(`Notion essay PR opened for ${pageTitle}: ${prUrl}`, {
scope: 'workspace',
tags: ['notion-essay-pr', `page:${pageId}`]
});
Expand All @@ -70,7 +97,7 @@ async function handleNotionPageCreated(ctx: WorkforceCtx, event: WorkforceProvid
pageId,
pageTitle,
outputPath,
prUrl: pr.url
prUrl
});
}

Expand Down
Loading