Merge branch 'dev' into local
Build: Production / build (push) Successful in 5m15s

# Conflicts:
#	packages/db/src/migrations/meta/_journal.json
This commit is contained in:
2026-05-31 08:05:15 -04:00
170 changed files with 55452 additions and 930 deletions
@@ -0,0 +1,183 @@
import { describe, expect, it } from "vitest";
import {
createDocumentAnchorSelector,
projectMarkdownToText,
remapDocumentAnchor,
resolveProjectionRange,
verifyDocumentAnchorSelector,
} from "./document-anchors.js";
function selectorFor(markdown: string, quote: string) {
const projection = projectMarkdownToText(markdown);
const start = projection.text.indexOf(quote);
expect(start).toBeGreaterThanOrEqual(0);
const range = resolveProjectionRange(projection, start, start + quote.length);
expect(range).not.toBeNull();
return createDocumentAnchorSelector(projection, range!);
}
describe("document text projection", () => {
it("projects markdown into normalized rendered text with source ranges", () => {
const markdown = [
"# Heading",
"",
"- Ship **bold** [link text](https://example.com) and `code span`.",
"| Name | Value |",
"| --- | --- |",
"| Alpha | Beta |",
].join("\n");
const projection = projectMarkdownToText(markdown);
expect(projection.text).toContain("Heading");
expect(projection.text).toContain("Ship bold link text and code span.");
expect(projection.text).toContain("Name Value");
expect(projection.text).toContain("Alpha Beta");
expect(projection.text).not.toContain("https://example.com");
expect(projection.positions).toHaveLength(projection.text.length);
const linkStart = projection.text.indexOf("link text");
const range = resolveProjectionRange(projection, linkStart, linkStart + "link text".length);
expect(range?.markdownStart).toBe(markdown.indexOf("link text"));
expect(range?.markdownEnd).toBe(markdown.indexOf("link text") + "link text".length);
});
it("normalizes whitespace while retaining markdown offsets", () => {
const markdown = "First line\n\nSecond\t\tline";
const projection = projectMarkdownToText(markdown);
expect(projection.text).toBe("First line Second line");
const range = resolveProjectionRange(projection, projection.text.indexOf("Second"), projection.text.length);
expect(range?.markdownStart).toBe(markdown.indexOf("Second"));
expect(range?.markdownEnd).toBe(markdown.length);
});
it("preserves non-link punctuation", () => {
const markdown = "Keep (parenthetical) [plain brackets] visible.";
const projection = projectMarkdownToText(markdown);
expect(projection.text).toBe("Keep (parenthetical) [plain brackets] visible.");
});
});
describe("document anchor verification and remapping", () => {
it("verifies a selector against its base revision", () => {
const markdown = "Intro text with **selected text** inside.";
const selector = selectorFor(markdown, "selected text");
const result = verifyDocumentAnchorSelector({ markdown, selector });
expect(result.ok).toBe(true);
expect(result.anchor?.selectedText).toBe("selected text");
expect(result.anchor?.markdownStart).toBe(markdown.indexOf("selected text"));
});
it("remaps exact anchors after surrounding text moves", () => {
const selector = selectorFor("Alpha paragraph.\n\nTarget sentence here.\n\nOmega paragraph.", "Target sentence here.");
const previousAnchor = {
selectedText: selector.quote.exact,
prefixText: selector.quote.prefix,
suffixText: selector.quote.suffix,
normalizedStart: selector.position.normalizedStart,
normalizedEnd: selector.position.normalizedEnd,
markdownStart: selector.position.markdownStart,
markdownEnd: selector.position.markdownEnd,
};
const result = remapDocumentAnchor({
previousAnchor,
nextMarkdown: "Omega paragraph.\n\nAlpha paragraph.\n\nTarget sentence here.",
});
expect(result.anchorState).toBe("active");
expect(result.confidence).toBe("exact");
expect(result.anchor?.selectedText).toBe("Target sentence here.");
});
it("uses context and proximity to disambiguate duplicate quotes", () => {
const selector = selectorFor("One apple near the start.\n\nTwo apple near the end.", "apple");
const previousAnchor = {
selectedText: selector.quote.exact,
prefixText: selector.quote.prefix,
suffixText: selector.quote.suffix,
normalizedStart: selector.position.normalizedStart,
normalizedEnd: selector.position.normalizedEnd,
markdownStart: selector.position.markdownStart,
markdownEnd: selector.position.markdownEnd,
};
const result = remapDocumentAnchor({
previousAnchor,
nextMarkdown: "Zero apple elsewhere.\n\nOne apple near the start.\n\nTwo apple near the end.",
});
expect(result.anchorState).toBe("active");
expect(result.confidence).toBe("duplicate");
expect(result.anchor?.prefixText).toContain("One");
});
it("marks duplicate anchors ambiguous when context cannot distinguish them", () => {
const selector = selectorFor("apple apple", "apple");
const previousAnchor = {
selectedText: selector.quote.exact,
prefixText: "",
suffixText: "",
normalizedStart: selector.position.normalizedStart,
normalizedEnd: selector.position.normalizedEnd,
markdownStart: selector.position.markdownStart,
markdownEnd: selector.position.markdownEnd,
};
const result = remapDocumentAnchor({ previousAnchor, nextMarkdown: "apple apple" });
expect(result.anchorState).toBe("stale");
expect(result.confidence).toBe("ambiguous");
});
it("keeps edited anchors as stale fuzzy matches", () => {
const selector = selectorFor("We rely on an important launch assumption for scope.", "important launch assumption");
const previousAnchor = {
selectedText: selector.quote.exact,
prefixText: selector.quote.prefix,
suffixText: selector.quote.suffix,
normalizedStart: selector.position.normalizedStart,
normalizedEnd: selector.position.normalizedEnd,
markdownStart: selector.position.markdownStart,
markdownEnd: selector.position.markdownEnd,
};
const result = remapDocumentAnchor({
previousAnchor,
nextMarkdown: "We rely on an important product launch assumption for scope.",
});
expect(result.anchorState).toBe("stale");
expect(result.confidence).toBe("fuzzy");
expect(result.anchor?.selectedText).toBe("important product launch assumption");
});
it("marks deleted anchors orphaned and allows future remapping from the latest known anchor", () => {
const selector = selectorFor("Keep this reviewed phrase in mind.", "reviewed phrase");
const previousAnchor = {
selectedText: selector.quote.exact,
prefixText: selector.quote.prefix,
suffixText: selector.quote.suffix,
normalizedStart: selector.position.normalizedStart,
normalizedEnd: selector.position.normalizedEnd,
markdownStart: selector.position.markdownStart,
markdownEnd: selector.position.markdownEnd,
};
const missing = remapDocumentAnchor({ previousAnchor, nextMarkdown: "The target disappeared." });
const recovered = remapDocumentAnchor({
previousAnchor,
nextMarkdown: "The target came back: reviewed phrase.",
});
expect(missing.anchorState).toBe("orphaned");
expect(missing.confidence).toBe("missing");
expect(missing.anchor).toBeNull();
expect(recovered.anchorState).toBe("active");
expect(recovered.anchor?.selectedText).toBe("reviewed phrase");
});
});
+8
View File
@@ -473,6 +473,12 @@ export type {
RequestConfirmationTarget,
RequestConfirmationPayload,
RequestConfirmationResult,
AcceptedPlanDecompositionStatus,
AcceptedPlanDecompositionChild,
AcceptedPlanDecomposition,
AcceptedPlanDecompositionResult,
AcceptedPlanDecompositionChildIssue,
AcceptedPlanDecompositionSummary,
IssueThreadInteractionBase,
SuggestTasksInteraction,
AskUserQuestionsInteraction,
@@ -868,6 +874,7 @@ export {
createIssueSchema,
createIssueInputSchema,
createChildIssueSchema,
createAcceptedPlanDecompositionSchema,
resolveCreateIssueStatusDefault,
createIssueLabelSchema,
issueBlockedInboxAttentionSchema,
@@ -936,6 +943,7 @@ export {
releaseIssueTreeHoldSchema,
type CreateIssue,
type CreateChildIssue,
type CreateAcceptedPlanDecomposition,
type CreateIssueLabel,
type UpdateIssue,
type ResolveIssueRecoveryAction,
+6
View File
@@ -238,6 +238,12 @@ export type {
RequestConfirmationTarget,
RequestConfirmationPayload,
RequestConfirmationResult,
AcceptedPlanDecompositionStatus,
AcceptedPlanDecompositionChild,
AcceptedPlanDecomposition,
AcceptedPlanDecompositionResult,
AcceptedPlanDecompositionChildIssue,
AcceptedPlanDecompositionSummary,
IssueThreadInteractionBase,
SuggestTasksInteraction,
AskUserQuestionsInteraction,
+1
View File
@@ -29,6 +29,7 @@ export interface InstanceGeneralSettings {
export interface InstanceExperimentalSettings {
enableEnvironments: boolean;
enableIsolatedWorkspaces: boolean;
enableIssuePlanDecompositions: boolean;
enableCloudSync: boolean;
autoRestartDevServerWhenIdle: boolean;
enableIssueGraphLivenessAutoRecovery: boolean;
+65
View File
@@ -129,6 +129,71 @@ export interface LegacyPlanDocument {
source: "issue_description";
}
export type AcceptedPlanDecompositionStatus = "in_flight" | "completed";
export interface AcceptedPlanDecompositionChild {
projectId?: string | null;
projectWorkspaceId?: string | null;
goalId?: string | null;
blockedByIssueIds?: string[];
title: string;
description?: string | null;
status: IssueStatus;
workMode: IssueWorkMode;
priority: IssuePriority;
assigneeAgentId?: string | null;
assigneeUserId?: string | null;
requestDepth?: number;
billingCode?: string | null;
assigneeAdapterOverrides?: IssueAssigneeAdapterOverrides | null;
executionPolicy?: IssueExecutionPolicy | null;
executionWorkspaceId?: string | null;
executionWorkspacePreference?: string | null;
executionWorkspaceSettings?: IssueExecutionWorkspaceSettings | null;
labelIds?: string[];
acceptanceCriteria?: string[];
blockParentUntilDone?: boolean;
}
export interface AcceptedPlanDecomposition {
id: string;
companyId: string;
sourceIssueId: string;
acceptedPlanRevisionId: string;
acceptedInteractionId: string | null;
status: AcceptedPlanDecompositionStatus;
requestFingerprint: string;
requestedChildCount: number;
childIssueIds: string[];
ownerAgentId: string | null;
ownerUserId: string | null;
ownerRunId: string | null;
completedAt: Date | string | null;
createdAt: Date | string;
updatedAt: Date | string;
}
export interface AcceptedPlanDecompositionResult {
decomposition: AcceptedPlanDecomposition;
childIssueIds: string[];
newlyCreatedChildIssueIds: string[];
}
export interface AcceptedPlanDecompositionChildIssue {
id: string;
identifier: string | null;
title: string;
status: IssueStatus;
priority: IssuePriority;
assigneeAgentId: string | null;
assigneeUserId: string | null;
}
export interface AcceptedPlanDecompositionSummary extends AcceptedPlanDecomposition {
acceptedPlanRevisionNumber: number | null;
childIssues: AcceptedPlanDecompositionChildIssue[];
}
export interface IssueRelationIssueSummary {
id: string;
identifier: string | null;
+17 -1
View File
@@ -38,8 +38,24 @@ import type { Routine, RoutineTrigger, RoutineVariable } from "./routine.js";
/**
* A JSON Schema object used for plugin config schemas and tool parameter schemas.
* Plugins provide these as plain JSON Schema compatible objects.
*
* The Paperclip extension keywords below are recognised by the Paperclip UI
* but are otherwise ignored by standard JSON Schema validators.
*/
export type JsonSchema = Record<string, unknown>;
export type JsonSchema = {
/**
* When true, the Paperclip config UI hides this property behind an
* "Advanced options" disclosure. Defaults to false (always visible).
*/
"x-paperclip-advanced"?: boolean;
/**
* Optional sub-section heading used to group advanced properties inside
* the disclosure (e.g. "SSH access", "VM resources"). Ignored when
* `x-paperclip-advanced` is not true.
*/
"x-paperclip-group"?: string;
[key: string]: unknown;
};
export type {
PluginDatabaseCoreReadTable,
@@ -2,7 +2,8 @@ export type WorkspaceOperationPhase =
| "worktree_prepare"
| "workspace_provision"
| "workspace_teardown"
| "worktree_cleanup";
| "worktree_cleanup"
| "workspace_finalize";
export type WorkspaceOperationStatus = "running" | "succeeded" | "failed" | "skipped";
@@ -0,0 +1,158 @@
import { describe, expect, it } from "vitest";
import {
catalogSkillFileDetailSchema,
catalogSkillListQuerySchema,
companySkillAuditResultSchema,
companySkillInstallCatalogResultSchema,
companySkillInstallCatalogSchema,
companySkillInstallUpdateSchema,
companySkillResetSchema,
companySkillUpdateStatusSchema,
} from "./company-skill.js";
const catalogSkill = {
id: "paperclipai:bundled:software-development:review",
key: "paperclipai/bundled/software-development/review",
kind: "bundled",
category: "software-development",
slug: "review",
name: "review",
description: "Review code",
path: "catalog/bundled/software-development/review",
entrypoint: "SKILL.md",
trustLevel: "markdown_only",
compatibility: "compatible",
defaultInstall: false,
recommendedForRoles: ["engineer"],
requires: [],
tags: ["review"],
files: [{ path: "SKILL.md", kind: "skill", sizeBytes: 8, sha256: "abc" }],
contentHash: "sha256:abc",
};
const companySkill = {
id: "00000000-0000-4000-8000-000000000001",
companyId: "00000000-0000-4000-8000-000000000002",
key: catalogSkill.key,
slug: catalogSkill.slug,
name: catalogSkill.name,
description: catalogSkill.description,
markdown: "# Review\n",
sourceType: "catalog",
sourceLocator: "/tmp/review",
sourceRef: catalogSkill.contentHash,
trustLevel: "markdown_only",
compatibility: "compatible",
fileInventory: [{ path: "SKILL.md", kind: "skill" }],
metadata: {
sourceKind: "catalog",
catalogId: catalogSkill.id,
originHash: catalogSkill.contentHash,
},
createdAt: "2026-05-26T00:00:00.000Z",
updatedAt: "2026-05-26T00:00:00.000Z",
};
describe("company skill catalog validators", () => {
it("accepts catalog list and install request shapes", () => {
expect(catalogSkillListQuerySchema.parse({
kind: "bundled",
category: "software-development",
q: "review",
})).toEqual({
kind: "bundled",
category: "software-development",
q: "review",
});
expect(companySkillInstallCatalogSchema.parse({
catalogSkillId: catalogSkill.id,
slug: "team-review",
force: true,
})).toEqual({
catalogSkillId: catalogSkill.id,
slug: "team-review",
force: true,
});
});
it("rejects invalid catalog filter and install payloads", () => {
expect(() => catalogSkillListQuerySchema.parse({ kind: "external" })).toThrow();
expect(() => companySkillInstallCatalogSchema.parse({ force: true })).toThrow();
});
it("accepts catalog file and install result responses", () => {
expect(catalogSkillFileDetailSchema.parse({
catalogSkillId: catalogSkill.id,
path: "SKILL.md",
kind: "skill",
content: "# Review\n",
language: "markdown",
markdown: true,
})).toMatchObject({
catalogSkillId: catalogSkill.id,
path: "SKILL.md",
});
expect(companySkillInstallCatalogResultSchema.parse({
action: "created",
skill: companySkill,
catalogSkill,
warnings: [],
})).toMatchObject({
action: "created",
skill: {
key: catalogSkill.key,
sourceType: "catalog",
},
catalogSkill: {
id: catalogSkill.id,
},
});
});
it("accepts update status, audit, update, and reset contract shapes", () => {
expect(companySkillUpdateStatusSchema.parse({
supported: true,
reason: null,
trackingRef: catalogSkill.id,
currentRef: "sha256:old",
latestRef: catalogSkill.contentHash,
hasUpdate: true,
installedHash: "sha256:installed",
originHash: catalogSkill.contentHash,
userModifiedAt: "2026-05-26T00:00:00.000Z",
updateHoldReason: "local_modifications",
auditVerdict: "warning",
auditCodes: ["local_modifications"],
})).toMatchObject({
supported: true,
updateHoldReason: "local_modifications",
auditVerdict: "warning",
});
expect(companySkillAuditResultSchema.parse({
skillId: companySkill.id,
installedHash: "sha256:installed",
originHash: catalogSkill.contentHash,
verdict: "fail",
codes: ["remote_fetch_exec"],
findings: [{
code: "remote_fetch_exec",
severity: "error",
message: "Remote-fetch or dynamic execution pattern is not allowed.",
path: "SKILL.md",
}],
scannedAt: "2026-05-26T00:00:00.000Z",
scanVersion: "skills-audit-v1",
})).toMatchObject({
verdict: "fail",
codes: ["remote_fetch_exec"],
});
expect(companySkillInstallUpdateSchema.parse(undefined)).toEqual({});
expect(companySkillInstallUpdateSchema.parse({ force: true })).toEqual({ force: true });
expect(companySkillResetSchema.parse(undefined)).toEqual({});
expect(companySkillResetSchema.parse({ force: true })).toEqual({ force: true });
});
});
+2
View File
@@ -186,6 +186,7 @@ export {
createIssueSchema,
createIssueInputSchema,
createChildIssueSchema,
createAcceptedPlanDecompositionSchema,
resolveCreateIssueStatusDefault,
createIssueLabelSchema,
issueBlockedInboxAttentionSchema,
@@ -237,6 +238,7 @@ export {
restoreIssueDocumentRevisionSchema,
type CreateIssue,
type CreateChildIssue,
type CreateAcceptedPlanDecomposition,
type CreateIssueLabel,
type UpdateIssue,
type IssueExecutionWorkspaceSettings,
@@ -38,6 +38,7 @@ export const patchInstanceGeneralSettingsSchema = instanceGeneralSettingsSchema.
export const instanceExperimentalSettingsSchema = z.object({
enableEnvironments: z.boolean().default(false),
enableIsolatedWorkspaces: z.boolean().default(false),
enableIssuePlanDecompositions: z.boolean().default(false),
enableCloudSync: z.boolean().default(false),
autoRestartDevServerWhenIdle: z.boolean().default(false),
enableIssueGraphLivenessAutoRecovery: z.boolean().default(false),
+7
View File
@@ -412,6 +412,13 @@ export const createChildIssueSchema = withCreateIssueStatusDefault(createIssueBa
export type CreateChildIssue = z.infer<typeof createChildIssueSchema>;
export const createAcceptedPlanDecompositionSchema = z.object({
acceptedPlanRevisionId: z.string().uuid(),
children: z.array(createChildIssueSchema).min(1).max(25),
});
export type CreateAcceptedPlanDecomposition = z.infer<typeof createAcceptedPlanDecompositionSchema>;
export const createIssueLabelSchema = z.object({
name: z.string().trim().min(1).max(48),
color: z.string().regex(/^#(?:[0-9a-fA-F]{6})$/, "Color must be a 6-digit hex value"),