mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-13 23:39:21 +02:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Native agents receive task constraints and completion tools from Paperclip. > - Completion tools already define the procedure for reporting a result. > - Repeated procedure text adds instructions to each full task turn. > - The final reply must still explain a blocker and link a saved document. > - This pull request removes repeated procedure text and keeps these visible outcome requirements explicit. > - A document receipt supplies the exact link, and stricter evals check the persisted reply and browser navigation. ## Linked Issues or Issue Description Refs: #14961. Related: #14948 and #15007. **What happened?** Native task envelopes repeat completion procedure text. A reduced envelope needs explicit final-reply requirements. The `write_document` receipt also lacks a canonical document link. **Expected behavior** Keep the completion tools as the source of procedure details. Require one accepted completion result before the final reply. A blocked reply must explain the reason, owner and unblock action. A document reply must contain a working link to the saved document. **Steps to reproduce** 1. Run the native assigned-skill document case and native blocker case. 2. Inspect the run-attributed provider final and its persisted comment. 3. Check the blocker explanation or open the final reply's document link. ## What Changed - Remove repeated completion procedure text from the native task constraints and backend instructions. - Keep explicit blocker and document-link requirements in full task turns. - Return a company/task-scoped `documentHref` from `write_document`. Preserve the link in the idempotent mutation receipt. - Repeat canonical links for this run's current saved revisions in accepted completion feedback. Give blocked providers final-response guidance for the cause, owner and unblock action. - Keep internal document/comment anchors when Markdown issue links load cached issue details. - Add a manual six-cell comparison suite with strict source, build, default-instruction and budget admission. - Capture eighteen shared runnerd RPC projections and six direct OpenCode HTTP projections across start, resume and continuation phases, using scripted local transports and no provider execution. - Apply v3 checks only to the manual instruction comparison; preserve v2 checks for the existing native completion suite. Check the actual persisted blocker reason and exact saved-document link. Click the rendered document link and check the original content marker in the classic document card or the new document tab. - Forward exact OpenCode finishing calls through the controller. Wait for acceptance, keep accepted feedback and concrete rejection text, and reject malformed responses. Preserve ordinary dynamic-tool response handling. - Settle the completion decision and tool response before mapping a racing idle/error/abort event or handling explicit close/interruption. Reject a concurrent finishing call before controller admission. - Add a provider-free regression through real runnerd, the OpenCode proxy and a fake provider. Reject the first completion, accept the corrected report in the same turn, and propose one result. - Keep all original verdicts unchanged. Treat replay under new checks as separate diagnostics. ## Verification - `pnpm -r typecheck` and `pnpm build` pass locally. - Native document-authority tests pass, including company/run authorization and idempotent replay. - Native runtime-context, backend and measurement tests pass. - Final-answer calibration, protocol scoring, source-admission and catalog tests pass. Wrong reasons, absent links and wrong link targets fail. - `pnpm test:e2e:runner:typecheck` passes. Discovery lists exactly six single-attempt local cells with the declared models. - Exported `prepareNativeInstructionPreflight` then `verifyNativeInstructionPreflight` pass on this clean committed source. They build locally and make zero provider calls. - Corrective live confirmation is incomplete. Source3a7349dpassed both Claude and both Codex cases. OpenCode saved the correct document but omitted its final link; its blocker case was canceled before paid execution. Preserve this failure. Thee171282confirmation was stopped during build after fresh review found a completion-settlement race; it executed zero providers. Sourcec3e0cb303fixes that race. Two affected OpenCode cases await fresh review and one bounded confirmation; earlier results remain attributed to their original source. - OpenCode proxy parsing, driver, factory and input tests: 81 pass across retained focused runs, including six settlement races. Evaluator/scoring/admission checks: 122 pass. The real proxy regression passes. Fresh local prepare then verify passes with 18 shared and 6 direct scripted captures, fresh SDK/Rust builds and zero providers. - The full local suite recorded two failures: a webhook timeout and a Git-scan load count mismatch. Both files pass in isolation with unchanged assertions/time budgets; preserve the original failure log. Freshc3e0cb303CI and review are pending. This PR remains draft. ## Risks - Final-answer wording can vary by provider. The checks cover the declared release-access blocker and saved document fixture, not general answer quality. - A single trial does not establish general equivalence, cause, speed, cost or live resume behavior. - `documentHref` is an additive receipt field. It points to the current saved document, not an immutable historical revision. Replaying an older receipt does not fabricate a new link. - The correction adds four production paths for document receipts, accepted completion feedback and UI navigation, plus four OpenCode controller/proxy paths, beyond the original three instruction paths. Completion rejection must remain repairable; the production-boundary regression covers it. - Preserve the frozen comparison context for live measurement. A merge-tree check against current master is clean. Do not relabel earlier live results as results from a later source tree. ## Model Used - OpenAI Codex, GPT-6 family. The exact serving model ID and context window are unavailable in this session. Capabilities used: reasoning, code editing, shell execution, test authoring and evidence review. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [ ] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
181 lines
6.2 KiB
TypeScript
181 lines
6.2 KiB
TypeScript
type MarkdownNode = {
|
|
type: string;
|
|
value?: string;
|
|
url?: string;
|
|
children?: MarkdownNode[];
|
|
};
|
|
|
|
const BARE_ISSUE_IDENTIFIER_RE = /^[A-Z][A-Z0-9]*-\d+$/i;
|
|
const ISSUE_SCHEME_RE = /^issue:\/\/:?([^?#\s]+)([?#].*)?$/i;
|
|
const ISSUE_REFERENCE_TOKEN_RE = /issue:\/\/:?[^\s<>()]+|https?:\/\/[^\s<>()]+|\/(?:[^\s<>()/]+\/)*issues\/[A-Z][A-Z0-9]*-\d+(?=$|[\s<>)\],.;!?:])|\b[A-Z][A-Z0-9]*-\d+\b/gi;
|
|
|
|
export function parseIssuePathIdFromPath(pathOrUrl: string | null | undefined): string | null {
|
|
if (!pathOrUrl) return null;
|
|
const value = pathOrUrl.trim();
|
|
if (!value) return null;
|
|
if (/^https?:\/\//i.test(value)) return null;
|
|
const pathname = value.split(/[?#]/, 1)[0]!;
|
|
|
|
const segments = pathname.split("/").filter(Boolean);
|
|
const issueIndex = segments.findIndex((segment) => segment === "issues");
|
|
if (issueIndex === -1 || issueIndex === segments.length - 1) return null;
|
|
const issuePathId = decodeURIComponent(segments[issueIndex + 1] ?? "");
|
|
if (!issuePathId || issuePathId.startsWith(":")) return null;
|
|
return BARE_ISSUE_IDENTIFIER_RE.test(issuePathId) ? issuePathId.toUpperCase() : issuePathId;
|
|
}
|
|
|
|
export function parseIssueReferenceFromHref(
|
|
href: string | null | undefined,
|
|
knownPrefixes?: Set<string>,
|
|
) {
|
|
if (!href) return null;
|
|
const trimmed = href.trim();
|
|
const issueSchemeMatch = trimmed.match(ISSUE_SCHEME_RE);
|
|
if (issueSchemeMatch?.[1]) {
|
|
const issuePathId = decodeURIComponent(issueSchemeMatch[1]);
|
|
return {
|
|
issuePathId,
|
|
href: `/issues/${encodeURIComponent(issuePathId)}${issueSchemeMatch[2] ?? ""}`,
|
|
};
|
|
}
|
|
|
|
const pathId = parseIssuePathIdFromPath(href);
|
|
if (pathId) {
|
|
return {
|
|
issuePathId: pathId,
|
|
href: `/issues/${encodeURIComponent(pathId)}${trimmed.match(/[?#].*$/)?.[0] ?? ""}`,
|
|
};
|
|
}
|
|
|
|
if (!BARE_ISSUE_IDENTIFIER_RE.test(trimmed)) return null;
|
|
const normalized = trimmed.toUpperCase();
|
|
// Only auto-link a bare IDENT-123 token when its prefix belongs to a known
|
|
// company. An empty or omitted set means "prefixes unknown" -> stay permissive
|
|
// (preserves behavior during initial load and in provider-less render
|
|
// surfaces). The issue:// scheme and /issues/ path forms handled above are
|
|
// never gated -- they are deliberate references, not accidental foreign keys.
|
|
if (knownPrefixes && knownPrefixes.size > 0) {
|
|
const prefix = normalized.split("-")[0];
|
|
if (!prefix || !knownPrefixes.has(prefix)) return null;
|
|
}
|
|
return {
|
|
issuePathId: normalized,
|
|
href: `/issues/${encodeURIComponent(normalized)}`,
|
|
};
|
|
}
|
|
|
|
function splitTrailingPunctuation(token: string) {
|
|
let core = token;
|
|
let trailing = "";
|
|
|
|
while (core.length > 0) {
|
|
const lastChar = core.at(-1);
|
|
if (!lastChar || !/[),.;!?:\]]/.test(lastChar)) break;
|
|
if (lastChar === ")") {
|
|
const openCount = (core.match(/\(/g) ?? []).length;
|
|
const closeCount = (core.match(/\)/g) ?? []).length;
|
|
if (closeCount <= openCount) break;
|
|
}
|
|
if (lastChar === "]") {
|
|
const openCount = (core.match(/\[/g) ?? []).length;
|
|
const closeCount = (core.match(/\]/g) ?? []).length;
|
|
if (closeCount <= openCount) break;
|
|
}
|
|
trailing = `${lastChar}${trailing}`;
|
|
core = core.slice(0, -1);
|
|
}
|
|
|
|
return { core, trailing };
|
|
}
|
|
|
|
function createIssueLinkNode(value: string, href: string, childType: "text" | "inlineCode" = "text"): MarkdownNode {
|
|
return {
|
|
type: "link",
|
|
url: href,
|
|
children: [{ type: childType, value }],
|
|
};
|
|
}
|
|
|
|
function linkifyIssueReferencesInText(value: string, knownPrefixes?: Set<string>): MarkdownNode[] | null {
|
|
const nodes: MarkdownNode[] = [];
|
|
let cursor = 0;
|
|
let matched = false;
|
|
|
|
for (const match of value.matchAll(ISSUE_REFERENCE_TOKEN_RE)) {
|
|
const raw = match[0];
|
|
if (!raw) continue;
|
|
|
|
const start = match.index ?? 0;
|
|
const end = start + raw.length;
|
|
const { core, trailing } = splitTrailingPunctuation(raw);
|
|
const issueRef = parseIssueReferenceFromHref(core, knownPrefixes);
|
|
if (!issueRef) continue;
|
|
|
|
matched = true;
|
|
if (start > cursor) {
|
|
nodes.push({ type: "text", value: value.slice(cursor, start) });
|
|
}
|
|
nodes.push(createIssueLinkNode(core, issueRef.href));
|
|
if (trailing) {
|
|
nodes.push({ type: "text", value: trailing });
|
|
}
|
|
cursor = end;
|
|
}
|
|
|
|
if (!matched) return null;
|
|
if (cursor < value.length) {
|
|
nodes.push({ type: "text", value: value.slice(cursor) });
|
|
}
|
|
return nodes;
|
|
}
|
|
|
|
function rewriteMarkdownTree(node: MarkdownNode, knownPrefixes?: Set<string>) {
|
|
if (!Array.isArray(node.children) || node.children.length === 0) return;
|
|
if (node.type === "link" || node.type === "linkReference" || node.type === "code" || node.type === "definition" || node.type === "html") {
|
|
return;
|
|
}
|
|
|
|
const nextChildren: MarkdownNode[] = [];
|
|
for (const child of node.children) {
|
|
if (child.type === "inlineCode" && typeof child.value === "string") {
|
|
const issueRef = parseIssueReferenceFromHref(child.value, knownPrefixes);
|
|
if (issueRef) {
|
|
nextChildren.push(createIssueLinkNode(child.value, issueRef.href, "inlineCode"));
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (child.type === "text" && typeof child.value === "string") {
|
|
const linked = linkifyIssueReferencesInText(child.value, knownPrefixes);
|
|
if (linked) {
|
|
nextChildren.push(...linked);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
rewriteMarkdownTree(child, knownPrefixes);
|
|
nextChildren.push(child);
|
|
}
|
|
node.children = nextChildren;
|
|
}
|
|
|
|
export interface RemarkLinkIssueReferencesOptions {
|
|
/**
|
|
* Company issue prefixes that are eligible for auto-linking. When provided
|
|
* and non-empty, a bare IDENT-123 token only becomes an issue link if its
|
|
* prefix is in this set -- this keeps foreign tracker keys (e.g. a Jira
|
|
* "TREE-604") from linking to non-existent Paperclip issues. Omit or pass an
|
|
* empty list to keep the legacy permissive behavior (link any IDENT-123).
|
|
*/
|
|
knownPrefixes?: string[];
|
|
}
|
|
|
|
export function remarkLinkIssueReferences(options?: RemarkLinkIssueReferencesOptions) {
|
|
const knownPrefixes = options?.knownPrefixes && options.knownPrefixes.length > 0
|
|
? new Set(options.knownPrefixes.map((prefix) => prefix.toUpperCase()))
|
|
: undefined;
|
|
return (tree: MarkdownNode) => {
|
|
rewriteMarkdownTree(tree, knownPrefixes);
|
|
};
|
|
}
|