mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-04 11:01:16 +01:00
merge: preserve current canonical source and Claude fixture review
Keep the qualified four-target selection and both Linux provider-pack assembly paths while carrying the current shared source. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
commit
f29dad6c94
449 files changed
+22989
-15619
No files matched your search
@@ -163,7 +163,7 @@ Touch:
|
||||
resolved, stale, disabled/submitting, and validation-error states.
|
||||
- `ui/src/fixtures/issueThreadInteractionFixtures.ts` — seed at least one
|
||||
pending and one resolved fixture for the new kind.
|
||||
- `ui/src/stories/issue-thread-interactions.stories.tsx` — Storybook entries
|
||||
- `ui/storybook/stories/questions-and-approvals/issue-thread-interactions.stories.tsx` — Storybook entries
|
||||
for the key states.
|
||||
- `ui/src/pages/IssueDetail.tsx` — extend the per-kind branches the card is
|
||||
rendered from (callback wiring, response submission).
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
name: maintain-storybook
|
||||
description: Keep Paperclip stories and their downstream catalog aligned with the feature map and production UI. Use to reconcile story drift, classify prototypes, update fixtures, or maintain feature coverage.
|
||||
---
|
||||
|
||||
# Maintain Storybook
|
||||
|
||||
Read the [Storybook catalog contract](../../../maps/storybook-map/README.md),
|
||||
the relevant [feature recipes](../../../maps/feature-map/README.md), and
|
||||
[DESIGN.md](../../../DESIGN.md). The feature map is an upstream input. This skill
|
||||
owns stories, their fixtures/support, and `maps/storybook-map/`; it does not edit
|
||||
the feature or agent-access maps. Report upstream drift to
|
||||
`maintain-verification-skill` with source evidence.
|
||||
|
||||
## Reconcile the requested scope
|
||||
|
||||
For a feature/change, follow its capability IDs to catalog entries and shared
|
||||
dependencies. For a full inventory, account for every configured story file,
|
||||
including historical examples. Start with `node scripts/storybook-catalog.mjs`.
|
||||
Record the source revision and whether the pass covers source, rendering,
|
||||
interactions, or visual comparisons. Inventory completeness is not behavioral
|
||||
coverage. Do not expand a targeted maintenance request into an all-product audit.
|
||||
|
||||
Compare each affected story's rendered subject, props, fixture responses, actions,
|
||||
and assertions with the production implementation and recipe. Follow imported
|
||||
wrappers: a production Button inside a locally recreated page does not make that
|
||||
page a production-component story. A real production page using mocked APIs is
|
||||
valid isolation; record component provenance and data simulation separately.
|
||||
Inspect mock methods/paths, payload shapes, errors, identity, gates, and reset
|
||||
behavior. An API method returning success without the relevant state transition
|
||||
can hide drift. Do not replace errors with generic successful mocks to pass.
|
||||
|
||||
Prefer mounting the shipped subject and mocking its external boundary. Retain
|
||||
local layout wrappers and fixture data when they serve that isolation. Replace a
|
||||
duplicate product implementation only when the current component expresses the
|
||||
same scenario without losing the original purpose. Production bugs and larger
|
||||
redesigns are separate findings, not reasons to rewrite expected behavior.
|
||||
|
||||
Classify current examples, proposals, historical explorations, and unresolved
|
||||
cases explicitly. Use `unreviewed` when evidence does not establish parity.
|
||||
Retain useful superseded work with its rationale and replacement reference;
|
||||
do not delete a story merely because it is old, mocked, or duplicated by name.
|
||||
Record mixed suites honestly and split them only when that improves review.
|
||||
|
||||
## Preserve identity and keep the index useful
|
||||
|
||||
Assign existing stable `area/capability` references, with one primary reference
|
||||
and optional related references. Foundations and unmapped proposals need explicit
|
||||
reasons rather than invented capabilities. Cross-cutting stories have one source
|
||||
and several references. Do not count historical/proposed stories as current
|
||||
feature coverage.
|
||||
|
||||
Preserve story exports and IDs. Before moves/title changes, capture the existing
|
||||
built `index.json`; use explicit meta IDs when needed and compare IDs afterward.
|
||||
Place stories under `ui/storybook/stories/<primary-feature-area>/`. Use the feature
|
||||
map's section and feature names as their sidebar prefix; keep useful scenario
|
||||
groupings beneath that prefix. Put shared fixture modules in `ui/storybook/support/`.
|
||||
Keep the catalog's `stableId` equal to the explicit CSF meta ID even when the title
|
||||
changes. Unmapped component examples belong under Foundations; the catalog guide
|
||||
belongs under Overview. The checker verifies these conventions.
|
||||
Keep review links and snapshot names working. Prefer classification/index fixes
|
||||
before bulk directory moves. Record every directly rendered production subject in
|
||||
the catalog's review sources, including relative imports and primitives when they
|
||||
are the demonstrated subject. Do not cap the number of source references.
|
||||
Update review fingerprints only after inspecting the changed source; do not refresh hashes merely
|
||||
to silence drift checks.
|
||||
|
||||
Update the catalog and run `node scripts/storybook-catalog.mjs --write` to refresh
|
||||
its generated index, then run the checker without flags. The script validates
|
||||
inventory/references/review fingerprints, not semantic parity or runtime health.
|
||||
Use `review-storybook` for affected rendering and interactions. Reuse the existing
|
||||
Storybook/Playwright tools; add no separate launcher, runtime, or baseline system.
|
||||
Do not accept new screenshot baselines as part of a catalog cleanup.
|
||||
|
||||
Report what was corrected, retained, and still needs review, naming exact files
|
||||
and capabilities. Keep source-only results separate from executed results and
|
||||
name blocked checks. A full pass must not hide unreviewed stories or claim that
|
||||
fixture interactions establish production API, permission, or provider behavior.
|
||||
@@ -0,0 +1,3 @@
|
||||
interface:
|
||||
display_name: "Maintain Storybook"
|
||||
short_description: "Align stories with features and production UI"
|
||||
@@ -0,0 +1,47 @@
|
||||
---
|
||||
name: review-storybook
|
||||
description: Review Paperclip stories for a feature or change using the Storybook catalog, production source, and existing browser tests. Report component, mock, interaction, and visual evidence separately.
|
||||
---
|
||||
|
||||
# Review Storybook
|
||||
|
||||
Read the [catalog contract](../../../maps/storybook-map/README.md) and find the
|
||||
requested capability in its generated index. Check the relevant feature recipe,
|
||||
production subject, and story before driving it. Catalog status is prior review
|
||||
context, not proof of the current revision. This skill consumes the maps/catalog;
|
||||
corrections belong to `maintain-storybook` or `maintain-verification-skill`.
|
||||
|
||||
Choose discriminating states for the requested change: the normal action and
|
||||
result, relevant failure/retry, gate/permission presentation, and mobile/theme
|
||||
variants where affected. Record chosen files/exports and omitted scope. For a
|
||||
full source review, account for every file; do not describe sampling as full
|
||||
render or interaction coverage.
|
||||
|
||||
Use [the existing Storybook commands](../../../doc/DEVELOPING.md#storybook):
|
||||
`pnpm build-storybook`, or an owned Storybook dev server on an available local
|
||||
port. Use the built `ui/storybook-static/index.json` for actual story IDs and
|
||||
links. Do not trust an unrelated server on the default port. Keep the process
|
||||
handle and stop only the server this review started, retaining useful evidence.
|
||||
|
||||
Inspect the real rendered subject, including errors and browser console output.
|
||||
For interactive stories, wait for their `play` assertions to complete and inspect
|
||||
the observable result. A screenshot alone does not prove `play` passed. Verify
|
||||
fixture isolation by revisiting/resetting relevant stories when they mutate
|
||||
fetch handlers, storage, query caches, time, or module state. Avoid real provider
|
||||
actions; explicitly document any live dependency required by a story.
|
||||
|
||||
The existing visual suite is
|
||||
`tests/storybook-visual/storybook-visual.spec.ts`. It uses external baselines and
|
||||
a pinned Linux environment. Inspect `baseline-manifest.json` before claiming
|
||||
visual regression proof. An unpublished manifest blocks comparisons; a macOS
|
||||
capture is useful inspection, not the Linux baseline. Do not update/publish
|
||||
baselines just to make a review pass. Use existing focused browser suites where
|
||||
they cover the requested scenario, and the production verification skill when
|
||||
the question requires backend persistence, real authorization, or provider proof.
|
||||
|
||||
Report each selected case as passed, failed, blocked, or not run, naming revision,
|
||||
story ID, environment, production versus prototype subject, simulated boundaries,
|
||||
action/assertion and observed result. Separate render, interaction, visual
|
||||
comparison, and production acceptance evidence. Report proven drift with exact
|
||||
source references and a proposed correction; do not lower expectations, remove
|
||||
failing cases, or silently bless historical behavior as current.
|
||||
@@ -0,0 +1,3 @@
|
||||
interface:
|
||||
display_name: "Review Storybook"
|
||||
short_description: "Review story rendering, interactions, and drift"
|
||||
@@ -56,6 +56,7 @@ packages/paperclip-runner/test-fixtures/pi-acp/**
|
||||
!packages/paperclip-runner/test-fixtures/pi-acp/profile-v22-identity.json
|
||||
!packages/paperclip-runner/test-fixtures/pi-acp/profile-v23-identity.json
|
||||
!packages/paperclip-runner/test-fixtures/pi-acp/profile-v24-identity.json
|
||||
!packages/paperclip-runner/test-fixtures/pi-acp/profile-v25-identity.json
|
||||
!packages/paperclip-runner/docs/capability-contract.md
|
||||
# check:runner-workflow-traceability access()es every regression test the
|
||||
# stress-traceability spec names — those are src/**/*.test.ts files, so
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
import vm from "node:vm";
|
||||
|
||||
const workflow = readFileSync(new URL("../../workflows/ci-routing.yml", import.meta.url), "utf8");
|
||||
const jobs = workflow.split(/^jobs:\n/m)[1];
|
||||
|
||||
test("routing uses reserved capacity and runs no PR code or actions", () => {
|
||||
assert.deepEqual([...jobs.matchAll(/^ {2}([a-z_]+):$/gm)].map((match) => match[1]), ["gate"]);
|
||||
const placement = jobs.match(/^ {4}runs-on: (.+)$/m)?.[1];
|
||||
assert.ok(placement, "routing placement must be defined");
|
||||
assert.doesNotMatch(placement, /fromJSON/);
|
||||
assert.ok(placement.includes("vars.AWS_CI_TRUSTED_USER_IDS == vars.AWS_CI_ROUTING_USER_IDS_SOURCE"));
|
||||
assert.ok(placement.includes("github.event.pull_request.user.id"));
|
||||
assert.ok(placement.includes("github.event.sender.id"));
|
||||
assert.ok(placement.endsWith("&& 'runs-on/fleet=paperclip-ci-routing-x64/env=public-ci' || 'ubuntu-latest' }}"));
|
||||
assert.doesNotMatch(jobs, /uses:|actions\/checkout|secrets\./);
|
||||
assert.doesNotMatch(workflow, /^ {2}(?:actions|contents|pull-requests): write$/m);
|
||||
});
|
||||
|
||||
test("routing exports both placement and stack scope to callers", () => {
|
||||
assert.match(workflow, /^ {6}runner:\n {8}value: \$\{\{ jobs\.gate\.outputs\.runner \}\}$/m);
|
||||
assert.match(workflow, /^ {6}full_ci:\n {8}value: \$\{\{ jobs\.gate\.outputs\.full_ci \}\}$/m);
|
||||
assert.match(jobs, /^ {6}runner: \$\{\{ steps\.route\.outputs\.runner \}\}$/m);
|
||||
assert.match(jobs, /^ {6}full_ci: \$\{\{ steps\.scope\.outputs\.full_ci \}\}$/m);
|
||||
});
|
||||
|
||||
const githubStub = "#!/usr/bin/env bash\nset -Eeuo pipefail\n\nendpoint=\"${*: -1}\"\ncase \"$endpoint\" in\n */pulls/*)\n jq -cn \\\n --argjson author_id \"${TEST_PR_AUTHOR_ID:-34892728}\" \\\n --argjson repository_id \"${TEST_REPOSITORY_ID:-1170821064}\" \\\n --arg base_ref \"${TEST_BASE_REF:-master}\" \\\n --arg base_sha \"${TEST_PR_BASE_SHA:-$EVENT_BASE_SHA}\" \\\n --arg head_sha \"${TEST_PR_HEAD_SHA:-1111111111111111111111111111111111111111}\" \\\n --arg merge_sha \"${TEST_PR_MERGE_SHA:-4444444444444444444444444444444444444444}\" '{\n state: \"open\",\n user: {id: $author_id},\n base: {repo: {id: $repository_id}, ref: $base_ref, sha: $base_sha},\n head: {sha: $head_sha},\n merge_commit_sha: $merge_sha\n }'\n ;;\n */git/ref/heads/*)\n jq -cn \\\n --arg sha \"${TEST_LIVE_BASE_REF_SHA:-$EVENT_BASE_SHA}\" '{object: {sha: $sha}}'\n ;;\n */compare/*)\n jq -cn \\\n --arg merge_base_sha \"${TEST_BASE_COMPARISON_MERGE_BASE_SHA:-$EVENT_BASE_SHA}\" \\\n --arg status \"${TEST_BASE_COMPARISON_STATUS:-ahead}\" '{\n merge_base_commit: {sha: $merge_base_sha},\n status: $status\n }'\n ;;\n */git/commits/*)\n requested_sha=\"${endpoint##*/}\"\n live_merge_sha=\"${TEST_PR_MERGE_SHA:-4444444444444444444444444444444444444444}\"\n synthetic_base_merge_sha=\"${TEST_SYNTHETIC_BASE_MERGE_SHA:-}\"\n if [[ \"$requested_sha\" == \"$EVENT_MERGE_SHA\" ]]; then\n tree_sha=\"${TEST_EVENT_MERGE_TREE_SHA:-5555555555555555555555555555555555555555}\"\n base_sha=\"${TEST_EVENT_MERGE_BASE_SHA:-$EVENT_BASE_SHA}\"\n head_sha=\"${TEST_EVENT_MERGE_HEAD_SHA:-$EVENT_HEAD_SHA}\"\n elif [[ \"$requested_sha\" == \"$live_merge_sha\" ]]; then\n tree_sha=\"${TEST_LIVE_MERGE_TREE_SHA:-5555555555555555555555555555555555555555}\"\n base_sha=\"${TEST_LIVE_MERGE_BASE_SHA:-$EVENT_BASE_SHA}\"\n head_sha=\"${TEST_LIVE_MERGE_HEAD_SHA:-$EVENT_HEAD_SHA}\"\n elif [[ -n \"$synthetic_base_merge_sha\" && \"$requested_sha\" == \"$synthetic_base_merge_sha\" ]]; then\n tree_sha=\"${TEST_SYNTHETIC_BASE_MERGE_TREE_SHA:-8888888888888888888888888888888888888888}\"\n base_sha=\"${TEST_SYNTHETIC_MASTER_PARENT_SHA:-9999999999999999999999999999999999999999}\"\n head_sha=\"${TEST_SYNTHETIC_BASE_REF_PARENT_SHA:-${TEST_LIVE_BASE_REF_SHA:-$EVENT_BASE_SHA}}\"\n else\n echo \"unexpected merge commit: $requested_sha\" >&2\n exit 1\n fi\n jq -cn \\\n --arg sha \"$requested_sha\" \\\n --arg tree_sha \"$tree_sha\" \\\n --arg base_sha \"$base_sha\" \\\n --arg head_sha \"$head_sha\" '{\n sha: $sha,\n tree: {sha: $tree_sha},\n parents: [{sha: $base_sha}, {sha: $head_sha}]\n }'\n ;;\n */actions/runs/*)\n jq -cn \\\n --argjson actor_id \"${TEST_TRIGGERING_ACTOR_ID:-34892728}\" \\\n --argjson repository_id \"${TEST_REPOSITORY_ID:-1170821064}\" \\\n --arg event_name \"${TEST_EVENT_NAME:-pull_request}\" '{\n triggering_actor: {id: $actor_id},\n repository: {id: $repository_id},\n event: $event_name\n }'\n ;;\n *)\n echo \"unexpected gh endpoint: $endpoint\" >&2\n exit 1\n ;;\nesac";
|
||||
|
||||
function stepScript(name) {
|
||||
const start = workflow.indexOf(` - name: ${name}\n`);
|
||||
assert.ok(start >= 0, `missing step: ${name}`);
|
||||
const body = workflow.slice(start);
|
||||
const runStart = body.indexOf(" run: |\n");
|
||||
assert.ok(runStart >= 0, `missing script: ${name}`);
|
||||
const script = body.slice(runStart + " run: |\n".length).split("\n - name:")[0];
|
||||
return script.split("\n").map((line) => line.replace(/^ {10}/, "")).join("\n");
|
||||
}
|
||||
|
||||
function executeStep(name, overrides = {}) {
|
||||
const directory = mkdtempSync(path.join(tmpdir(), "paperclip-routing-test-"));
|
||||
const output = path.join(directory, "output");
|
||||
try {
|
||||
const gh = path.join(directory, "gh");
|
||||
writeFileSync(gh, githubStub);
|
||||
chmodSync(gh, 0o700);
|
||||
const result = spawnSync("bash", ["-c", stepScript(name)], {
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...process.env,
|
||||
PATH: `${directory}:${process.env.PATH}`,
|
||||
GITHUB_OUTPUT: output,
|
||||
AWS_CI_ENABLED: "true",
|
||||
TRUSTED_USER_IDS: "[34892728]",
|
||||
EVENT_NAME: "pull_request",
|
||||
EVENT_REPOSITORY: "paperclipai/paperclip",
|
||||
EVENT_REPOSITORY_ID: "1170821064",
|
||||
EVENT_ACTION: "synchronize",
|
||||
EVENT_PR_NUMBER: "12350",
|
||||
EVENT_PR_AUTHOR_ID: "34892728",
|
||||
EVENT_SENDER_ID: "34892728",
|
||||
EVENT_BASE_REPOSITORY_ID: "1170821064",
|
||||
EVENT_BASE_REF: "master",
|
||||
EVENT_BASE_SHA: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
EVENT_HEAD_SHA: "1111111111111111111111111111111111111111",
|
||||
EVENT_MERGE_SHA: "2222222222222222222222222222222222222222",
|
||||
RUN_ID: "123456789",
|
||||
...overrides,
|
||||
},
|
||||
});
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
const lines = readFileSync(output, "utf8").trim().split("\n");
|
||||
assert.equal(lines.length, 1, "each step must emit exactly one decision");
|
||||
return Object.fromEntries(lines.map((line) => {
|
||||
const separator = line.indexOf("=");
|
||||
return [line.slice(0, separator), line.slice(separator + 1)];
|
||||
}));
|
||||
} finally {
|
||||
rmSync(directory, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
const awsRunner = "runs-on/fleet=paperclip-public-pr-x64/env=public-ci";
|
||||
const stackedBase = {
|
||||
EVENT_BASE_REF: "trusted-stack-base",
|
||||
TEST_BASE_REF: "trusted-stack-base",
|
||||
TEST_PR_BASE_SHA: "7777777777777777777777777777777777777777",
|
||||
TEST_LIVE_BASE_REF_SHA: "7777777777777777777777777777777777777777",
|
||||
TEST_SYNTHETIC_BASE_MERGE_SHA: "8888888888888888888888888888888888888888",
|
||||
TEST_EVENT_MERGE_BASE_SHA: "8888888888888888888888888888888888888888",
|
||||
TEST_LIVE_MERGE_BASE_SHA: "8888888888888888888888888888888888888888",
|
||||
};
|
||||
|
||||
const routeCases = [
|
||||
["allowlisted PR", {}, awsRunner],
|
||||
["validated stacked base", stackedBase, awsRunner],
|
||||
["lagging PR API base snapshot", { TEST_PR_BASE_SHA: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" }, awsRunner],
|
||||
["disabled AWS", { AWS_CI_ENABLED: "false" }],
|
||||
["missing AWS variables", { AWS_CI_ENABLED: "", TRUSTED_USER_IDS: "" }],
|
||||
["unlisted author", { EVENT_PR_AUTHOR_ID: "99999999" }],
|
||||
["unlisted sender", { EVENT_SENDER_ID: "99999999" }],
|
||||
["unlisted rerun actor", { TEST_TRIGGERING_ACTOR_ID: "99999999" }],
|
||||
["missing base", { EVENT_BASE_REF: "" }],
|
||||
["wrong base repository", { EVENT_BASE_REPOSITORY_ID: "999999999" }],
|
||||
["replaced base ancestry", { TEST_BASE_COMPARISON_MERGE_BASE_SHA: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" }],
|
||||
["stale head", { TEST_PR_HEAD_SHA: "3333333333333333333333333333333333333333" }],
|
||||
["changed merge head", { TEST_LIVE_MERGE_HEAD_SHA: "3333333333333333333333333333333333333333" }],
|
||||
["stacked base with wrong parent", { ...stackedBase, TEST_SYNTHETIC_BASE_REF_PARENT_SHA: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" }],
|
||||
["changed merge tree", { TEST_LIVE_MERGE_TREE_SHA: "6666666666666666666666666666666666666666" }],
|
||||
["wrong repository", { EVENT_REPOSITORY_ID: "999999999" }],
|
||||
["malformed allowlist", { TRUSTED_USER_IDS: '["34892728"]' }],
|
||||
["manual dispatch", { EVENT_NAME: "workflow_dispatch" }],
|
||||
["unsupported PR action", { EVENT_ACTION: "closed" }],
|
||||
["malformed commit SHA", { EVENT_HEAD_SHA: "invalid" }],
|
||||
];
|
||||
|
||||
for (const [name, env, expected = "ubuntu-latest"] of routeCases) {
|
||||
test(`runner decision: ${name}`, () => {
|
||||
assert.equal(executeStep("Validate PR identity and select runner", env).runner, expected);
|
||||
});
|
||||
}
|
||||
|
||||
const scopeCases = [
|
||||
["ordinary PR", null, "master", "true"],
|
||||
["top stack PR", { position: 11, size: 11, base: { ref: "master" } }, "stack-10", "true"],
|
||||
["lowest unmerged PR", { position: 1, size: 11, base: { ref: "master" } }, "master", "true"],
|
||||
["middle stack PR", { position: 6, size: 11, base: { ref: "master" } }, "stack-5", "false"],
|
||||
["malformed position", { position: "invalid", size: 11, base: { ref: "master" } }, "stack-5", "true"],
|
||||
["out-of-range position", { position: 12, size: 11, base: { ref: "master" } }, "stack-5", "true"],
|
||||
["missing stack base", { position: 6, size: 11 }, "stack-5", "true"],
|
||||
["empty metadata", {}, "master", "true"],
|
||||
];
|
||||
|
||||
for (const [name, stack, base, expected] of scopeCases) {
|
||||
test(`scope decision: ${name}`, () => {
|
||||
assert.equal(executeStep("Select stacked PR CI scope", {
|
||||
STACK_JSON: JSON.stringify(stack), PR_BASE_REF: base,
|
||||
}).full_ci, expected);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function routingPlacement(vars = {}, github = {}) {
|
||||
const source = "[34892728]";
|
||||
const expression = jobs.match(/^ {4}runs-on: \$\{\{ (.+) \}\}$/m)?.[1];
|
||||
assert.ok(expression, "routing expression is missing");
|
||||
return vm.runInNewContext(expression, {
|
||||
vars: {
|
||||
AWS_CI_ENABLED: "true",
|
||||
AWS_CI_TRUSTED_USER_IDS: source,
|
||||
AWS_CI_ROUTING_USER_IDS_SOURCE: source,
|
||||
AWS_CI_ROUTING_USER_IDS: "|34892728|",
|
||||
...vars,
|
||||
},
|
||||
github: {
|
||||
repository: "paperclipai/paperclip",
|
||||
repository_id: "1170821064",
|
||||
event_name: "pull_request",
|
||||
ref: "refs/heads/master",
|
||||
actor_id: "34892728",
|
||||
event: { pull_request: { user: { id: 34892728 } }, sender: { id: 34892728 } },
|
||||
...github,
|
||||
},
|
||||
contains: (search, item) => String(search).toLowerCase().includes(String(item).toLowerCase()),
|
||||
format: (template, ...args) => template.replace(/\{(\d+)\}/g, (_, index) => String(args[Number(index)])),
|
||||
}, { timeout: 1000 });
|
||||
}
|
||||
|
||||
const placementCases = [
|
||||
["approved PR", {}, {}, "runs-on/fleet=paperclip-ci-routing-x64/env=public-ci"],
|
||||
["unapproved author", {}, { event: { pull_request: { user: { id: 999 } }, sender: { id: 34892728 } } }],
|
||||
["unapproved sender", {}, { event: { pull_request: { user: { id: 34892728 } }, sender: { id: 999 } } }],
|
||||
["partial ID cannot match", {}, { event: { pull_request: { user: { id: 4892728 } }, sender: { id: 34892728 } } }],
|
||||
["invalid JSON keeps hosted fallback", { AWS_CI_TRUSTED_USER_IDS: "[34892728,]" }, {}],
|
||||
["stale snapshot", { AWS_CI_TRUSTED_USER_IDS: "[34892728,999]" }, {}],
|
||||
["missing snapshot", { AWS_CI_ROUTING_USER_IDS_SOURCE: "" }, {}],
|
||||
["missing admission members", { AWS_CI_ROUTING_USER_IDS: "" }, {}],
|
||||
["disabled routing", { AWS_CI_ENABLED: "false" }, {}],
|
||||
["different repository", {}, { repository: "other/repo" }],
|
||||
["different repository ID", {}, { repository_id: "999" }],
|
||||
["approved manual master canary", {}, { event_name: "workflow_dispatch" }, "runs-on/fleet=paperclip-ci-routing-x64/env=public-ci"],
|
||||
["unapproved manual actor", {}, { event_name: "workflow_dispatch", actor_id: "999" }],
|
||||
["manual branch canary", {}, { event_name: "workflow_dispatch", ref: "refs/heads/other" }],
|
||||
];
|
||||
|
||||
for (const [name, vars, github, expected = "ubuntu-latest"] of placementCases) {
|
||||
test(`placement before allocation: ${name}`, () => {
|
||||
assert.equal(routingPlacement(vars, github), expected);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
name: CI routing
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
outputs:
|
||||
runner:
|
||||
value: ${{ jobs.gate.outputs.runner }}
|
||||
full_ci:
|
||||
value: ${{ jobs.gate.outputs.full_ci }}
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
gate:
|
||||
name: Select trusted runner
|
||||
runs-on: ${{ vars.AWS_CI_ENABLED == 'true' && vars.AWS_CI_ROUTING_USER_IDS_SOURCE != '' && vars.AWS_CI_TRUSTED_USER_IDS == vars.AWS_CI_ROUTING_USER_IDS_SOURCE && github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && ((github.event_name == 'pull_request' && contains(vars.AWS_CI_ROUTING_USER_IDS, format('|{0}|', github.event.pull_request.user.id)) && contains(vars.AWS_CI_ROUTING_USER_IDS, format('|{0}|', github.event.sender.id))) || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/master' && contains(vars.AWS_CI_ROUTING_USER_IDS, format('|{0}|', github.actor_id)))) && 'runs-on/fleet=paperclip-ci-routing-x64/env=public-ci' || 'ubuntu-latest' }}
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
runner: ${{ steps.route.outputs.runner }}
|
||||
full_ci: ${{ steps.scope.outputs.full_ci }}
|
||||
|
||||
steps:
|
||||
- name: Validate PR identity and select runner
|
||||
id: route
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
AWS_CI_ENABLED: ${{ vars.AWS_CI_ENABLED }}
|
||||
TRUSTED_USER_IDS: ${{ vars.AWS_CI_TRUSTED_USER_IDS }}
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
EVENT_REPOSITORY: ${{ github.repository }}
|
||||
EVENT_REPOSITORY_ID: ${{ github.repository_id }}
|
||||
EVENT_ACTION: ${{ github.event.action }}
|
||||
EVENT_PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
EVENT_PR_AUTHOR_ID: ${{ github.event.pull_request.user.id }}
|
||||
EVENT_SENDER_ID: ${{ github.event.sender.id }}
|
||||
EVENT_BASE_REPOSITORY_ID: ${{ github.event.pull_request.base.repo.id }}
|
||||
EVENT_BASE_REF: ${{ github.event.pull_request.base.ref }}
|
||||
EVENT_BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
EVENT_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
EVENT_MERGE_SHA: ${{ github.sha }}
|
||||
RUN_ID: ${{ github.run_id }}
|
||||
run: |
|
||||
set -u
|
||||
|
||||
github_runner='ubuntu-latest'
|
||||
aws_runner='runs-on/fleet=paperclip-public-pr-x64/env=public-ci'
|
||||
|
||||
fail_closed() {
|
||||
echo "runner=$github_runner" >> "$GITHUB_OUTPUT"
|
||||
echo "::notice title=AWS CI routing::Using GitHub-hosted runner: $1"
|
||||
exit 0
|
||||
}
|
||||
|
||||
is_positive_integer() {
|
||||
[[ "$1" =~ ^[1-9][0-9]*$ ]]
|
||||
}
|
||||
|
||||
is_commit_sha() {
|
||||
[[ "$1" =~ ^[0-9a-f]{40}$ ]]
|
||||
}
|
||||
|
||||
is_allowed() {
|
||||
local user_id="$1"
|
||||
jq -e --argjson user_id "$user_id" 'index($user_id) != null' \
|
||||
<<< "$TRUSTED_USER_IDS" >/dev/null
|
||||
}
|
||||
|
||||
[[ "$AWS_CI_ENABLED" == 'true' ]] || fail_closed 'AWS_CI_ENABLED is not true'
|
||||
[[ "$EVENT_NAME" == 'pull_request' ]] || fail_closed 'event is not pull_request'
|
||||
[[ "$EVENT_REPOSITORY" == 'paperclipai/paperclip' ]] || fail_closed 'unexpected repository'
|
||||
[[ "$EVENT_REPOSITORY_ID" == '1170821064' ]] || fail_closed 'unexpected repository ID'
|
||||
[[ "$EVENT_BASE_REPOSITORY_ID" == '1170821064' ]] || fail_closed 'unexpected base repository ID'
|
||||
[[ -n "$EVENT_BASE_REF" ]] || fail_closed 'missing base branch'
|
||||
[[ "$EVENT_ACTION" =~ ^(opened|reopened|synchronize)$ ]] || fail_closed 'unsupported pull_request action'
|
||||
|
||||
jq -e '
|
||||
type == "array" and
|
||||
length > 0 and
|
||||
all(.[]; type == "number" and . > 0 and floor == .)
|
||||
' <<< "$TRUSTED_USER_IDS" >/dev/null 2>&1 || fail_closed 'trusted user ID list is malformed'
|
||||
|
||||
for user_id in "$EVENT_PR_AUTHOR_ID" "$EVENT_SENDER_ID"; do
|
||||
is_positive_integer "$user_id" || fail_closed 'event contains a malformed user ID'
|
||||
is_allowed "$user_id" || fail_closed "GitHub user ID $user_id is not allowlisted"
|
||||
done
|
||||
|
||||
for commit_sha in "$EVENT_BASE_SHA" "$EVENT_HEAD_SHA" "$EVENT_MERGE_SHA"; do
|
||||
is_commit_sha "$commit_sha" || fail_closed 'event contains a malformed commit SHA'
|
||||
done
|
||||
|
||||
pr_json="$(gh api \
|
||||
-H 'Accept: application/vnd.github+json' \
|
||||
-H 'X-GitHub-Api-Version: 2022-11-28' \
|
||||
"/repos/paperclipai/paperclip/pulls/$EVENT_PR_NUMBER" 2>/dev/null)" \
|
||||
|| fail_closed 'could not refresh pull request state'
|
||||
|
||||
jq -e \
|
||||
--argjson repository_id 1170821064 \
|
||||
--argjson author_id "$EVENT_PR_AUTHOR_ID" \
|
||||
--arg base_ref "$EVENT_BASE_REF" \
|
||||
--arg head_sha "$EVENT_HEAD_SHA" \
|
||||
'
|
||||
.state == "open" and
|
||||
.user.id == $author_id and
|
||||
.base.repo.id == $repository_id and
|
||||
.base.ref == $base_ref and
|
||||
.head.sha == $head_sha
|
||||
' <<< "$pr_json" >/dev/null 2>&1 \
|
||||
|| fail_closed 'current pull request state does not match the triggering event'
|
||||
|
||||
live_merge_sha="$(jq -r '.merge_commit_sha // empty' <<< "$pr_json")"
|
||||
is_commit_sha "$live_merge_sha" || fail_closed 'current pull request has no valid merge SHA'
|
||||
|
||||
base_ref_json="$(gh api \
|
||||
-H 'Accept: application/vnd.github+json' \
|
||||
-H 'X-GitHub-Api-Version: 2022-11-28' \
|
||||
"/repos/paperclipai/paperclip/git/ref/heads/$EVENT_BASE_REF" 2>/dev/null)" \
|
||||
|| fail_closed 'could not inspect the current base branch'
|
||||
live_base_ref_sha="$(jq -r '.object.sha // empty' <<< "$base_ref_json")"
|
||||
is_commit_sha "$live_base_ref_sha" || fail_closed 'current base branch has no valid commit SHA'
|
||||
|
||||
base_comparison="$(gh api \
|
||||
-H 'Accept: application/vnd.github+json' \
|
||||
-H 'X-GitHub-Api-Version: 2022-11-28' \
|
||||
"/repos/paperclipai/paperclip/compare/$EVENT_BASE_SHA...$live_base_ref_sha" 2>/dev/null)" \
|
||||
|| fail_closed 'could not compare the triggering and current base branches'
|
||||
jq -e \
|
||||
--arg event_base_sha "$EVENT_BASE_SHA" '
|
||||
.merge_base_commit.sha == $event_base_sha and
|
||||
(.status == "ahead" or .status == "identical")
|
||||
' <<< "$base_comparison" >/dev/null 2>&1 \
|
||||
|| fail_closed 'current base branch does not descend from the triggering base snapshot'
|
||||
|
||||
event_merge_json="$(gh api \
|
||||
-H 'Accept: application/vnd.github+json' \
|
||||
-H 'X-GitHub-Api-Version: 2022-11-28' \
|
||||
"/repos/paperclipai/paperclip/git/commits/$EVENT_MERGE_SHA" 2>/dev/null)" \
|
||||
|| fail_closed 'could not inspect the event merge commit'
|
||||
|
||||
live_merge_json="$(gh api \
|
||||
-H 'Accept: application/vnd.github+json' \
|
||||
-H 'X-GitHub-Api-Version: 2022-11-28' \
|
||||
"/repos/paperclipai/paperclip/git/commits/$live_merge_sha" 2>/dev/null)" \
|
||||
|| fail_closed 'could not inspect the current merge commit'
|
||||
|
||||
validate_merge_commit() {
|
||||
local merge_json="$1"
|
||||
local expected_sha="$2"
|
||||
jq -e \
|
||||
--arg expected_sha "$expected_sha" \
|
||||
--arg head_sha "$EVENT_HEAD_SHA" '
|
||||
.sha == $expected_sha and
|
||||
(.parents | length) == 2 and
|
||||
.parents[1].sha == $head_sha and
|
||||
(.parents[0].sha | test("^[0-9a-f]{40}$")) and
|
||||
(.tree.sha | test("^[0-9a-f]{40}$"))
|
||||
' <<< "$merge_json" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
validate_merge_commit "$event_merge_json" "$EVENT_MERGE_SHA" \
|
||||
|| fail_closed 'event merge commit does not match the current base and head'
|
||||
validate_merge_commit "$live_merge_json" "$live_merge_sha" \
|
||||
|| fail_closed 'current merge commit does not match the triggering base and head'
|
||||
|
||||
event_merge_parent="$(jq -r '.parents[0].sha' <<< "$event_merge_json")"
|
||||
live_merge_parent="$(jq -r '.parents[0].sha' <<< "$live_merge_json")"
|
||||
[[ "$event_merge_parent" == "$live_merge_parent" ]] \
|
||||
|| fail_closed 'current merge commit uses a different base merge parent'
|
||||
|
||||
if [[ "$event_merge_parent" != "$live_base_ref_sha" ]]; then
|
||||
base_merge_json="$(gh api \
|
||||
-H 'Accept: application/vnd.github+json' \
|
||||
-H 'X-GitHub-Api-Version: 2022-11-28' \
|
||||
"/repos/paperclipai/paperclip/git/commits/$event_merge_parent" 2>/dev/null)" \
|
||||
|| fail_closed 'could not inspect the stacked base merge commit'
|
||||
jq -e \
|
||||
--arg expected_sha "$event_merge_parent" \
|
||||
--arg live_base_ref_sha "$live_base_ref_sha" '
|
||||
.sha == $expected_sha and
|
||||
(.parents | length) == 2 and
|
||||
any(.parents[]; .sha == $live_base_ref_sha) and
|
||||
(.tree.sha | test("^[0-9a-f]{40}$"))
|
||||
' <<< "$base_merge_json" >/dev/null 2>&1 \
|
||||
|| fail_closed 'stacked base merge commit does not contain the current base branch'
|
||||
fi
|
||||
|
||||
event_merge_tree="$(jq -r '.tree.sha' <<< "$event_merge_json")"
|
||||
live_merge_tree="$(jq -r '.tree.sha' <<< "$live_merge_json")"
|
||||
[[ "$event_merge_tree" == "$live_merge_tree" ]] \
|
||||
|| fail_closed 'current merge tree differs from the triggering merge tree'
|
||||
|
||||
run_json="$(gh api \
|
||||
-H 'Accept: application/vnd.github+json' \
|
||||
-H 'X-GitHub-Api-Version: 2022-11-28' \
|
||||
"/repos/paperclipai/paperclip/actions/runs/$RUN_ID" 2>/dev/null)" \
|
||||
|| fail_closed 'could not refresh workflow run state'
|
||||
|
||||
triggering_actor_id="$(jq -r '.triggering_actor.id // empty' <<< "$run_json")"
|
||||
is_positive_integer "$triggering_actor_id" || fail_closed 'workflow run has no valid triggering actor ID'
|
||||
is_allowed "$triggering_actor_id" || fail_closed "triggering GitHub user ID $triggering_actor_id is not allowlisted"
|
||||
|
||||
jq -e \
|
||||
--argjson repository_id 1170821064 \
|
||||
--arg event_name "$EVENT_NAME" '
|
||||
.repository.id == $repository_id and
|
||||
.event == $event_name
|
||||
' <<< "$run_json" >/dev/null 2>&1 \
|
||||
|| fail_closed 'current workflow run does not match the expected repository event'
|
||||
|
||||
echo "runner=$aws_runner" >> "$GITHUB_OUTPUT"
|
||||
echo '::notice title=AWS CI routing::Using an ephemeral RunsOn Fleet runner'
|
||||
|
||||
- name: Select stacked PR CI scope
|
||||
id: scope
|
||||
shell: bash
|
||||
env:
|
||||
STACK_JSON: ${{ toJSON(github.event.pull_request.stack) }}
|
||||
PR_BASE_REF: ${{ github.event.pull_request.base.ref }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
full_ci='true'
|
||||
reason='ordinary pull request'
|
||||
|
||||
if jq -e 'type == "object"' <<< "$STACK_JSON" >/dev/null 2>&1; then
|
||||
stack_position="$(jq -r '.position // empty' <<< "$STACK_JSON")"
|
||||
stack_size="$(jq -r '.size // empty' <<< "$STACK_JSON")"
|
||||
stack_base_ref="$(jq -r '.base.ref // empty' <<< "$STACK_JSON")"
|
||||
|
||||
if [[ ! "$stack_position" =~ ^[1-9][0-9]*$ ]] ||
|
||||
[[ ! "$stack_size" =~ ^[1-9][0-9]*$ ]] ||
|
||||
(( stack_position > stack_size )) ||
|
||||
[[ -z "$stack_base_ref" ]]; then
|
||||
reason='malformed stack metadata; defaulting to full CI'
|
||||
elif (( stack_position == stack_size )); then
|
||||
reason='top pull request in stack'
|
||||
elif [[ "$stack_base_ref" == "$PR_BASE_REF" ]]; then
|
||||
reason='lowest unmerged pull request in stack'
|
||||
else
|
||||
full_ci='false'
|
||||
reason='middle pull request in stack'
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "full_ci=$full_ci" >> "$GITHUB_OUTPUT"
|
||||
echo "::notice title=Stacked PR CI scope::$reason; full_ci=$full_ci"
|
||||
|
||||
@@ -177,8 +177,6 @@ jobs:
|
||||
run: |
|
||||
set -uo pipefail
|
||||
pnpm install --frozen-lockfile --ignore-scripts
|
||||
# Historical evaluation fixtures read these exact public trees.
|
||||
# Fetch only their objects; keep the selected checkout unchanged.
|
||||
git fetch --no-tags --depth=1 origin 2a8a99e4a5f69aa803b3f10b982f583e75a87042 16ef1a5744b23bb043a8b286e6288921da1de98f
|
||||
status=0
|
||||
for check in 'pnpm -r typecheck' 'pnpm test:run' 'pnpm check:token-gates' 'pnpm test:e2e:runner:typecheck' 'pnpm test:e2e:runner:unit' 'pnpm build' 'if [ -f scripts/verify-grok-npm-install.mjs ]; then node scripts/verify-grok-npm-install.mjs; fi'; do
|
||||
|
||||
@@ -17,241 +17,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
gate:
|
||||
name: Select trusted runner
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
runner: ${{ steps.route.outputs.runner }}
|
||||
full_ci: ${{ steps.scope.outputs.full_ci }}
|
||||
|
||||
steps:
|
||||
- name: Validate PR identity and select runner
|
||||
id: route
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
AWS_CI_ENABLED: ${{ vars.AWS_CI_ENABLED }}
|
||||
TRUSTED_USER_IDS: ${{ vars.AWS_CI_TRUSTED_USER_IDS }}
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
EVENT_REPOSITORY: ${{ github.repository }}
|
||||
EVENT_REPOSITORY_ID: ${{ github.repository_id }}
|
||||
EVENT_ACTION: ${{ github.event.action }}
|
||||
EVENT_PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
EVENT_PR_AUTHOR_ID: ${{ github.event.pull_request.user.id }}
|
||||
EVENT_SENDER_ID: ${{ github.event.sender.id }}
|
||||
EVENT_BASE_REPOSITORY_ID: ${{ github.event.pull_request.base.repo.id }}
|
||||
EVENT_BASE_REF: ${{ github.event.pull_request.base.ref }}
|
||||
EVENT_BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
EVENT_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
EVENT_MERGE_SHA: ${{ github.sha }}
|
||||
RUN_ID: ${{ github.run_id }}
|
||||
run: |
|
||||
set -u
|
||||
|
||||
github_runner='ubuntu-latest'
|
||||
aws_runner='runs-on/fleet=paperclip-public-pr-x64/env=public-ci'
|
||||
|
||||
fail_closed() {
|
||||
echo "runner=$github_runner" >> "$GITHUB_OUTPUT"
|
||||
echo "::notice title=AWS CI routing::Using GitHub-hosted runner: $1"
|
||||
exit 0
|
||||
}
|
||||
|
||||
is_positive_integer() {
|
||||
[[ "$1" =~ ^[1-9][0-9]*$ ]]
|
||||
}
|
||||
|
||||
is_commit_sha() {
|
||||
[[ "$1" =~ ^[0-9a-f]{40}$ ]]
|
||||
}
|
||||
|
||||
is_allowed() {
|
||||
local user_id="$1"
|
||||
jq -e --argjson user_id "$user_id" 'index($user_id) != null' \
|
||||
<<< "$TRUSTED_USER_IDS" >/dev/null
|
||||
}
|
||||
|
||||
[[ "$AWS_CI_ENABLED" == 'true' ]] || fail_closed 'AWS_CI_ENABLED is not true'
|
||||
# Reusable workflows retain the caller's github context and event
|
||||
# payload, so a pull_request caller must still report pull_request.
|
||||
[[ "$EVENT_NAME" == 'pull_request' ]] || fail_closed 'event is not pull_request'
|
||||
[[ "$EVENT_REPOSITORY" == 'paperclipai/paperclip' ]] || fail_closed 'unexpected repository'
|
||||
[[ "$EVENT_REPOSITORY_ID" == '1170821064' ]] || fail_closed 'unexpected repository ID'
|
||||
[[ "$EVENT_BASE_REPOSITORY_ID" == '1170821064' ]] || fail_closed 'unexpected base repository ID'
|
||||
[[ -n "$EVENT_BASE_REF" ]] || fail_closed 'missing base branch'
|
||||
[[ "$EVENT_ACTION" =~ ^(opened|reopened|synchronize)$ ]] || fail_closed 'unsupported pull_request action'
|
||||
|
||||
jq -e '
|
||||
type == "array" and
|
||||
length > 0 and
|
||||
all(.[]; type == "number" and . > 0 and floor == .)
|
||||
' <<< "$TRUSTED_USER_IDS" >/dev/null 2>&1 || fail_closed 'trusted user ID list is malformed'
|
||||
|
||||
for user_id in "$EVENT_PR_AUTHOR_ID" "$EVENT_SENDER_ID"; do
|
||||
is_positive_integer "$user_id" || fail_closed 'event contains a malformed user ID'
|
||||
is_allowed "$user_id" || fail_closed "GitHub user ID $user_id is not allowlisted"
|
||||
done
|
||||
|
||||
for commit_sha in "$EVENT_BASE_SHA" "$EVENT_HEAD_SHA" "$EVENT_MERGE_SHA"; do
|
||||
is_commit_sha "$commit_sha" || fail_closed 'event contains a malformed commit SHA'
|
||||
done
|
||||
|
||||
pr_json="$(gh api \
|
||||
-H 'Accept: application/vnd.github+json' \
|
||||
-H 'X-GitHub-Api-Version: 2022-11-28' \
|
||||
"/repos/paperclipai/paperclip/pulls/$EVENT_PR_NUMBER" 2>/dev/null)" \
|
||||
|| fail_closed 'could not refresh pull request state'
|
||||
|
||||
jq -e \
|
||||
--argjson repository_id 1170821064 \
|
||||
--argjson author_id "$EVENT_PR_AUTHOR_ID" \
|
||||
--arg base_ref "$EVENT_BASE_REF" \
|
||||
--arg head_sha "$EVENT_HEAD_SHA" \
|
||||
'
|
||||
.state == "open" and
|
||||
.user.id == $author_id and
|
||||
.base.repo.id == $repository_id and
|
||||
.base.ref == $base_ref and
|
||||
.head.sha == $head_sha
|
||||
' <<< "$pr_json" >/dev/null 2>&1 \
|
||||
|| fail_closed 'current pull request state does not match the triggering event'
|
||||
|
||||
live_merge_sha="$(jq -r '.merge_commit_sha // empty' <<< "$pr_json")"
|
||||
is_commit_sha "$live_merge_sha" || fail_closed 'current pull request has no valid merge SHA'
|
||||
|
||||
base_ref_json="$(gh api \
|
||||
-H 'Accept: application/vnd.github+json' \
|
||||
-H 'X-GitHub-Api-Version: 2022-11-28' \
|
||||
"/repos/paperclipai/paperclip/git/ref/heads/$EVENT_BASE_REF" 2>/dev/null)" \
|
||||
|| fail_closed 'could not inspect the current base branch'
|
||||
live_base_ref_sha="$(jq -r '.object.sha // empty' <<< "$base_ref_json")"
|
||||
is_commit_sha "$live_base_ref_sha" || fail_closed 'current base branch has no valid commit SHA'
|
||||
|
||||
base_comparison="$(gh api \
|
||||
-H 'Accept: application/vnd.github+json' \
|
||||
-H 'X-GitHub-Api-Version: 2022-11-28' \
|
||||
"/repos/paperclipai/paperclip/compare/$EVENT_BASE_SHA...$live_base_ref_sha" 2>/dev/null)" \
|
||||
|| fail_closed 'could not compare the triggering and current base branches'
|
||||
jq -e \
|
||||
--arg event_base_sha "$EVENT_BASE_SHA" '
|
||||
.merge_base_commit.sha == $event_base_sha and
|
||||
(.status == "ahead" or .status == "identical")
|
||||
' <<< "$base_comparison" >/dev/null 2>&1 \
|
||||
|| fail_closed 'current base branch does not descend from the triggering base snapshot'
|
||||
|
||||
event_merge_json="$(gh api \
|
||||
-H 'Accept: application/vnd.github+json' \
|
||||
-H 'X-GitHub-Api-Version: 2022-11-28' \
|
||||
"/repos/paperclipai/paperclip/git/commits/$EVENT_MERGE_SHA" 2>/dev/null)" \
|
||||
|| fail_closed 'could not inspect the event merge commit'
|
||||
|
||||
live_merge_json="$(gh api \
|
||||
-H 'Accept: application/vnd.github+json' \
|
||||
-H 'X-GitHub-Api-Version: 2022-11-28' \
|
||||
"/repos/paperclipai/paperclip/git/commits/$live_merge_sha" 2>/dev/null)" \
|
||||
|| fail_closed 'could not inspect the current merge commit'
|
||||
|
||||
validate_merge_commit() {
|
||||
local merge_json="$1"
|
||||
local expected_sha="$2"
|
||||
jq -e \
|
||||
--arg expected_sha "$expected_sha" \
|
||||
--arg head_sha "$EVENT_HEAD_SHA" '
|
||||
.sha == $expected_sha and
|
||||
(.parents | length) == 2 and
|
||||
.parents[1].sha == $head_sha and
|
||||
(.parents[0].sha | test("^[0-9a-f]{40}$")) and
|
||||
(.tree.sha | test("^[0-9a-f]{40}$"))
|
||||
' <<< "$merge_json" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
validate_merge_commit "$event_merge_json" "$EVENT_MERGE_SHA" \
|
||||
|| fail_closed 'event merge commit does not match the current base and head'
|
||||
validate_merge_commit "$live_merge_json" "$live_merge_sha" \
|
||||
|| fail_closed 'current merge commit does not match the triggering base and head'
|
||||
|
||||
event_merge_parent="$(jq -r '.parents[0].sha' <<< "$event_merge_json")"
|
||||
live_merge_parent="$(jq -r '.parents[0].sha' <<< "$live_merge_json")"
|
||||
[[ "$event_merge_parent" == "$live_merge_parent" ]] \
|
||||
|| fail_closed 'current merge commit uses a different base merge parent'
|
||||
|
||||
if [[ "$event_merge_parent" != "$live_base_ref_sha" ]]; then
|
||||
base_merge_json="$(gh api \
|
||||
-H 'Accept: application/vnd.github+json' \
|
||||
-H 'X-GitHub-Api-Version: 2022-11-28' \
|
||||
"/repos/paperclipai/paperclip/git/commits/$event_merge_parent" 2>/dev/null)" \
|
||||
|| fail_closed 'could not inspect the stacked base merge commit'
|
||||
jq -e \
|
||||
--arg expected_sha "$event_merge_parent" \
|
||||
--arg live_base_ref_sha "$live_base_ref_sha" '
|
||||
.sha == $expected_sha and
|
||||
(.parents | length) == 2 and
|
||||
any(.parents[]; .sha == $live_base_ref_sha) and
|
||||
(.tree.sha | test("^[0-9a-f]{40}$"))
|
||||
' <<< "$base_merge_json" >/dev/null 2>&1 \
|
||||
|| fail_closed 'stacked base merge commit does not contain the current base branch'
|
||||
fi
|
||||
|
||||
event_merge_tree="$(jq -r '.tree.sha' <<< "$event_merge_json")"
|
||||
live_merge_tree="$(jq -r '.tree.sha' <<< "$live_merge_json")"
|
||||
[[ "$event_merge_tree" == "$live_merge_tree" ]] \
|
||||
|| fail_closed 'current merge tree differs from the triggering merge tree'
|
||||
|
||||
run_json="$(gh api \
|
||||
-H 'Accept: application/vnd.github+json' \
|
||||
-H 'X-GitHub-Api-Version: 2022-11-28' \
|
||||
"/repos/paperclipai/paperclip/actions/runs/$RUN_ID" 2>/dev/null)" \
|
||||
|| fail_closed 'could not refresh workflow run state'
|
||||
|
||||
triggering_actor_id="$(jq -r '.triggering_actor.id // empty' <<< "$run_json")"
|
||||
is_positive_integer "$triggering_actor_id" || fail_closed 'workflow run has no valid triggering actor ID'
|
||||
is_allowed "$triggering_actor_id" || fail_closed "triggering GitHub user ID $triggering_actor_id is not allowlisted"
|
||||
|
||||
jq -e \
|
||||
--argjson repository_id 1170821064 \
|
||||
--arg event_name "$EVENT_NAME" '
|
||||
.repository.id == $repository_id and
|
||||
.event == $event_name
|
||||
' <<< "$run_json" >/dev/null 2>&1 \
|
||||
|| fail_closed 'current workflow run does not match the expected repository event'
|
||||
|
||||
echo "runner=$aws_runner" >> "$GITHUB_OUTPUT"
|
||||
echo '::notice title=AWS CI routing::Using an ephemeral RunsOn Fleet runner'
|
||||
|
||||
- name: Select stacked PR CI scope
|
||||
id: scope
|
||||
shell: bash
|
||||
env:
|
||||
STACK_JSON: ${{ toJSON(github.event.pull_request.stack) }}
|
||||
PR_BASE_REF: ${{ github.event.pull_request.base.ref }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
full_ci='true'
|
||||
reason='ordinary pull request'
|
||||
|
||||
if jq -e 'type == "object"' <<< "$STACK_JSON" >/dev/null 2>&1; then
|
||||
stack_position="$(jq -r '.position // empty' <<< "$STACK_JSON")"
|
||||
stack_size="$(jq -r '.size // empty' <<< "$STACK_JSON")"
|
||||
stack_base_ref="$(jq -r '.base.ref // empty' <<< "$STACK_JSON")"
|
||||
|
||||
if [[ ! "$stack_position" =~ ^[1-9][0-9]*$ ]] ||
|
||||
[[ ! "$stack_size" =~ ^[1-9][0-9]*$ ]] ||
|
||||
(( stack_position > stack_size )) ||
|
||||
[[ -z "$stack_base_ref" ]]; then
|
||||
reason='malformed stack metadata; defaulting to full CI'
|
||||
elif (( stack_position == stack_size )); then
|
||||
reason='top pull request in stack'
|
||||
elif [[ "$stack_base_ref" == "$PR_BASE_REF" ]]; then
|
||||
reason='lowest unmerged pull request in stack'
|
||||
else
|
||||
full_ci='false'
|
||||
reason='middle pull request in stack'
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "full_ci=$full_ci" >> "$GITHUB_OUTPUT"
|
||||
echo "::notice title=Stacked PR CI scope::$reason; full_ci=$full_ci"
|
||||
uses: paperclipai/paperclip/.github/workflows/ci-routing.yml@master
|
||||
|
||||
policy:
|
||||
needs: [gate]
|
||||
|
||||
@@ -227,17 +227,17 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
with:
|
||||
ref: ${{ inputs.qualification_source_sha }}
|
||||
persist-credentials: false
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86
|
||||
with:
|
||||
version: 9.15.4
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
|
||||
with:
|
||||
node-version: 24
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
|
||||
with:
|
||||
pattern: ${{ needs.qualification_daemons.outputs.runner_artifact_prefix }}-*
|
||||
path: ${{ runner.temp }}/release-runner-inputs
|
||||
@@ -287,7 +287,7 @@ jobs:
|
||||
fi
|
||||
- name: Retain completed package assembly even after a later Linux failure
|
||||
if: ${{ !cancelled() && steps.pack_identity.outputs.available == 'true' }}
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||
with:
|
||||
name: ${{ inputs.artifact_name }}-public-packages
|
||||
path: ${{ runner.temp }}/public-runner-packs/
|
||||
@@ -304,17 +304,17 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJSON(needs.qualification_daemons.outputs.native_consumer_matrix) }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
with:
|
||||
ref: ${{ inputs.qualification_source_sha }}
|
||||
persist-credentials: false
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86
|
||||
with:
|
||||
version: 9.15.4
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
|
||||
with:
|
||||
node-version: 24
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
|
||||
with:
|
||||
name: ${{ inputs.artifact_name }}-public-packages
|
||||
path: ${{ runner.temp }}/public-runner-packs
|
||||
@@ -322,9 +322,8 @@ jobs:
|
||||
run: pnpm install --ignore-scripts --no-frozen-lockfile
|
||||
- name: Checkout immutable ARM qualification tooling
|
||||
if: ${{ matrix.target == 'linux-arm64' }}
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
with:
|
||||
# job.workflow_* identifies this reusable workflow, not its caller.
|
||||
repository: ${{ job.workflow_repository }}
|
||||
ref: ${{ job.workflow_sha }}
|
||||
path: .qualification-tooling
|
||||
@@ -375,7 +374,7 @@ jobs:
|
||||
if [[ "$TARGET" == linux-arm64 ]]; then mode=--linux-arm64-consumer; fi
|
||||
node scripts/verify-grok-npm-install.mjs "$mode" \
|
||||
"$RUNNER_TEMP/public-runner-packs" "$SOURCE_SHA" "$RUNNER_TEMP/native-public-install.json"
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||
if: always()
|
||||
with:
|
||||
name: ${{ inputs.artifact_name }}-${{ matrix.target }}
|
||||
|
||||
@@ -529,11 +529,11 @@ jobs:
|
||||
matrix: ${{ steps.targets.outputs.matrix }}
|
||||
native_consumers: ${{ steps.targets.outputs.native_consumers }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
|
||||
with:
|
||||
node-version: 24
|
||||
- name: Preserve the immutable source's platform contract
|
||||
@@ -566,8 +566,6 @@ jobs:
|
||||
&& Object.keys(contract).sort().join() === 'schema,targets', 'Unknown exact-source daemon platform contract');
|
||||
declared = contract.targets;
|
||||
} else {
|
||||
// Frozen pre-contract sources used a literal export. Read that one
|
||||
// declaration as JSON data; never import or evaluate preview code.
|
||||
const legacy = readBlob('packages/paperclip-runner/scripts/stage-release-runner-binaries.mjs');
|
||||
const declarations = [...(legacy ?? '').matchAll(/^[ \t]*export const RELEASE_RUNNER_TARGETS = (?:Object\.freeze\()?(\[[^\r\n]*\])\)?;[ \t]*$/gm)];
|
||||
assert.equal(declarations.length, 1, 'Unknown exact-source daemon platform contract');
|
||||
@@ -599,14 +597,14 @@ jobs:
|
||||
artifact_prefix: ${{ steps.identity.outputs.artifact_prefix }}
|
||||
provider_pack_artifact_prefix: ${{ steps.identity.outputs.provider_pack_artifact_prefix }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
persist-credentials: false
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86
|
||||
with:
|
||||
version: 9.15.4
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
|
||||
with:
|
||||
node-version: 24.21.0
|
||||
- name: Bind immutable producer source and target
|
||||
@@ -646,18 +644,13 @@ jobs:
|
||||
fi
|
||||
rustup toolchain list
|
||||
- name: Cache native Rust dependencies
|
||||
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6
|
||||
with:
|
||||
workspaces: ${{ steps.native_rust_workspace.outputs.path }} -> target
|
||||
# Compiler/environment and Cargo lock hashes are added by the action.
|
||||
# Also bind generated contracts outside the Rust workspace, while
|
||||
# always recompiling workspace code and recording actual new bytes.
|
||||
shared-key: release-native-v2-${{ matrix.host }}-${{ matrix.target }}-${{ hashFiles('packages/paperclip-runner/protocol/**', 'packages/paperclip-runner/src/**', 'packages/paperclip-runner/*.json') }}
|
||||
cache-workspace-crates: false
|
||||
cache-bin: false
|
||||
cache-on-failure: false
|
||||
# A master workflow can verify an unmerged preview ref. Those jobs
|
||||
# restore only; they never write master's trusted dependency cache.
|
||||
save-if: ${{ github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.event_name == 'push' && github.ref == 'refs/heads/master' && inputs.ref == github.sha }}
|
||||
- name: Install dependencies
|
||||
run: pnpm install --no-frozen-lockfile
|
||||
@@ -700,7 +693,7 @@ jobs:
|
||||
run: |
|
||||
node packages/paperclip-runner/scripts/stage-release-runner-binaries.mjs record "$SOURCE_SHA" "$TARGET" \
|
||||
packages/paperclip-runner/dist/bin/paperclip-runnerd "$RUNNER_TEMP/runner-release/$TARGET"
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||
with:
|
||||
name: ${{ steps.identity.outputs.artifact_prefix }}-${{ matrix.target }}
|
||||
path: ${{ runner.temp }}/runner-release/
|
||||
|
||||
@@ -341,8 +341,6 @@ jobs:
|
||||
run: node scripts/cloud-source-verification.mjs "$SOURCE_SHA"
|
||||
|
||||
release_runner_source:
|
||||
# Old promotions retain their already-shipped packaging contract. New
|
||||
# sources opt into source-declared native target assembly in their release.sh.
|
||||
needs: [verify_canary, select_nightly, select_beta, preflight_stable]
|
||||
if: >-
|
||||
!cancelled() && (
|
||||
@@ -357,7 +355,7 @@ jobs:
|
||||
sha: ${{ steps.source.outputs.sha }}
|
||||
required: ${{ steps.source.outputs.required }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
with:
|
||||
ref: ${{ needs.preflight_stable.outputs.sha || needs.select_beta.outputs.sha || needs.select_nightly.outputs.sha || github.sha }}
|
||||
persist-credentials: false
|
||||
@@ -427,7 +425,7 @@ jobs:
|
||||
|
||||
- name: Download exact-source release daemon receipts
|
||||
if: needs.release_runner_source.outputs.required == 'true'
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
|
||||
with:
|
||||
pattern: ${{ needs.release_runner_binaries.outputs.runner_artifact_prefix }}-*
|
||||
path: ${{ runner.temp }}/release-runner-inputs
|
||||
@@ -734,7 +732,7 @@ jobs:
|
||||
|
||||
- name: Download exact-source release daemon receipts
|
||||
if: needs.release_runner_source.outputs.required == 'true'
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
|
||||
with:
|
||||
pattern: ${{ needs.release_runner_binaries.outputs.runner_artifact_prefix }}-*
|
||||
path: ${{ runner.temp }}/release-runner-inputs
|
||||
@@ -1023,7 +1021,7 @@ jobs:
|
||||
|
||||
- name: Download exact-source release daemon receipts
|
||||
if: needs.release_runner_source.outputs.required == 'true'
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
|
||||
with:
|
||||
pattern: ${{ needs.release_runner_binaries.outputs.runner_artifact_prefix }}-*
|
||||
path: ${{ runner.temp }}/release-runner-inputs
|
||||
@@ -1414,7 +1412,7 @@ jobs:
|
||||
|
||||
- name: Download exact-source release daemon receipts
|
||||
if: needs.release_runner_source.outputs.required == 'true'
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
|
||||
with:
|
||||
pattern: ${{ needs.release_runner_binaries.outputs.runner_artifact_prefix }}-*
|
||||
path: ${{ runner.temp }}/release-runner-inputs
|
||||
@@ -1482,7 +1480,7 @@ jobs:
|
||||
|
||||
- name: Download exact-source release daemon receipts
|
||||
if: needs.release_runner_source.outputs.required == 'true'
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
|
||||
with:
|
||||
pattern: ${{ needs.release_runner_binaries.outputs.runner_artifact_prefix }}-*
|
||||
path: ${{ runner.temp }}/release-runner-inputs
|
||||
|
||||
@@ -162,6 +162,12 @@ Follow [the map contract](maps/README.md): update affected entries when mapped
|
||||
behavior or access changes, and run `node scripts/check-maps.mjs` after map edits.
|
||||
Its structural checks do not prove live behavior or authorization.
|
||||
|
||||
Storybook has a downstream [feature catalog](maps/storybook-map/README.md).
|
||||
Use `maintain-storybook` for story/fixture drift and `review-storybook` for browser
|
||||
evidence. Keep the feature-map skills independent of this catalog. After story
|
||||
or declared review-source changes, reconcile affected entries and run
|
||||
`node scripts/storybook-catalog.mjs`; its checks are not production acceptance.
|
||||
|
||||
Default local/agent test path:
|
||||
|
||||
```sh
|
||||
|
||||
@@ -46,6 +46,9 @@ PRs that follow this path are **much** more likely to be accepted, even when the
|
||||
|
||||
## PR Requirements (all PRs)
|
||||
|
||||
- Do not submit a PR that moves a code file from less than 1,000 lines of code to more than 1,000 lines of code.
|
||||
- Do not submit a PR that includes code comments.
|
||||
|
||||
### Use the PR Template
|
||||
|
||||
Every pull request **must** follow the PR template at [`.github/PULL_REQUEST_TEMPLATE.md`](.github/PULL_REQUEST_TEMPLATE.md). If you create a PR via the GitHub API or other tooling that bypasses the template, copy its contents into your PR description manually. The template includes required sections: Thinking Path, What Changed, Verification, Risks, Model Used, and a Checklist.
|
||||
|
||||
+119
-198
@@ -62,6 +62,109 @@ function company(overrides: Record<string, unknown> = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
function allImportEntities(): CompanyPortabilityPreviewResult["include"] {
|
||||
return {
|
||||
company: true,
|
||||
agents: true,
|
||||
projects: true,
|
||||
issues: true,
|
||||
skills: true,
|
||||
};
|
||||
}
|
||||
|
||||
function sourceCompanyManifest(): NonNullable<CompanyPortabilityPreviewResult["manifest"]["company"]> {
|
||||
return {
|
||||
path: "COMPANY.md",
|
||||
name: "Source Co",
|
||||
description: null,
|
||||
logoPath: null,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
feedbackDataSharingEnabled: false,
|
||||
feedbackDataSharingConsentAt: null,
|
||||
feedbackDataSharingConsentByUserId: null,
|
||||
feedbackDataSharingTermsVersion: null,
|
||||
};
|
||||
}
|
||||
|
||||
function ceoManifest(): CompanyPortabilityPreviewResult["manifest"]["agents"][number] {
|
||||
return {
|
||||
slug: "ceo",
|
||||
name: "CEO",
|
||||
path: "agents/ceo/AGENT.md",
|
||||
skills: [],
|
||||
role: "ceo",
|
||||
title: null,
|
||||
icon: null,
|
||||
capabilities: null,
|
||||
reportsToSlug: null,
|
||||
reportsToExistingAgentId: null,
|
||||
reportsToExistingAgentSlug: null,
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
permissionGrants: [],
|
||||
budgetMonthlyCents: 0,
|
||||
metadata: null,
|
||||
};
|
||||
}
|
||||
|
||||
function alphaProjectManifest(): CompanyPortabilityPreviewResult["manifest"]["projects"][number] {
|
||||
return {
|
||||
slug: "alpha",
|
||||
name: "Alpha",
|
||||
path: "projects/alpha/PROJECT.md",
|
||||
description: null,
|
||||
ownerAgentSlug: null,
|
||||
leadAgentSlug: null,
|
||||
targetDate: null,
|
||||
color: null,
|
||||
icon: null,
|
||||
status: null,
|
||||
executionWorkspacePolicy: null,
|
||||
workspaces: [],
|
||||
env: null,
|
||||
metadata: null,
|
||||
};
|
||||
}
|
||||
|
||||
function kickoffIssueManifest(): CompanyPortabilityPreviewResult["manifest"]["issues"][number] {
|
||||
return {
|
||||
slug: "kickoff",
|
||||
identifier: null,
|
||||
title: "Kickoff",
|
||||
path: "projects/alpha/issues/kickoff/TASK.md",
|
||||
projectSlug: "alpha",
|
||||
projectWorkspaceKey: null,
|
||||
assigneeAgentSlug: "ceo",
|
||||
description: null,
|
||||
recurring: false,
|
||||
routine: null,
|
||||
legacyRecurrence: null,
|
||||
status: null,
|
||||
priority: null,
|
||||
labelIds: [],
|
||||
billingCode: null,
|
||||
executionWorkspaceSettings: null,
|
||||
assigneeAdapterOverrides: null,
|
||||
comments: [],
|
||||
metadata: null,
|
||||
};
|
||||
}
|
||||
|
||||
function openAiEnvInput(): CompanyPortabilityPreviewResult["envInputs"][number] {
|
||||
return {
|
||||
key: "OPENAI_API_KEY",
|
||||
description: null,
|
||||
agentSlug: "ceo",
|
||||
projectSlug: null,
|
||||
kind: "secret",
|
||||
requirement: "required",
|
||||
defaultValue: null,
|
||||
portability: "portable",
|
||||
};
|
||||
}
|
||||
|
||||
describe("company CLI commands", () => {
|
||||
let fetchMock: ReturnType<typeof vi.fn>;
|
||||
let logSpy: ReturnType<typeof vi.spyOn>;
|
||||
@@ -298,13 +401,7 @@ describe("buildCompanyDashboardUrl", () => {
|
||||
describe("renderCompanyImportPreview", () => {
|
||||
it("summarizes the preview with counts, selection info, and truncated examples", () => {
|
||||
const preview: CompanyPortabilityPreviewResult = {
|
||||
include: {
|
||||
company: true,
|
||||
agents: true,
|
||||
projects: true,
|
||||
issues: true,
|
||||
skills: true,
|
||||
},
|
||||
include: allImportEntities(),
|
||||
targetCompanyId: "company-123",
|
||||
targetCompanyName: "Imported Co",
|
||||
collisionStrategy: "rename",
|
||||
@@ -334,49 +431,14 @@ describe("renderCompanyImportPreview", () => {
|
||||
companyId: "company-src",
|
||||
companyName: "Source Co",
|
||||
},
|
||||
includes: {
|
||||
company: true,
|
||||
agents: true,
|
||||
projects: true,
|
||||
issues: true,
|
||||
skills: true,
|
||||
},
|
||||
company: {
|
||||
path: "COMPANY.md",
|
||||
name: "Source Co",
|
||||
description: null,
|
||||
logoPath: null,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
feedbackDataSharingEnabled: false,
|
||||
feedbackDataSharingConsentAt: null,
|
||||
feedbackDataSharingConsentByUserId: null,
|
||||
feedbackDataSharingTermsVersion: null,
|
||||
},
|
||||
includes: allImportEntities(),
|
||||
company: sourceCompanyManifest(),
|
||||
sidebar: {
|
||||
agents: ["ceo"],
|
||||
projects: ["alpha"],
|
||||
},
|
||||
agents: [
|
||||
{
|
||||
slug: "ceo",
|
||||
name: "CEO",
|
||||
path: "agents/ceo/AGENT.md",
|
||||
skills: [],
|
||||
role: "ceo",
|
||||
title: null,
|
||||
icon: null,
|
||||
capabilities: null,
|
||||
reportsToSlug: null,
|
||||
reportsToExistingAgentId: null,
|
||||
reportsToExistingAgentSlug: null,
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
permissionGrants: [],
|
||||
budgetMonthlyCents: 0,
|
||||
metadata: null,
|
||||
},
|
||||
ceoManifest(),
|
||||
],
|
||||
skills: [
|
||||
{
|
||||
@@ -395,73 +457,20 @@ describe("renderCompanyImportPreview", () => {
|
||||
},
|
||||
],
|
||||
projects: [
|
||||
{
|
||||
slug: "alpha",
|
||||
name: "Alpha",
|
||||
path: "projects/alpha/PROJECT.md",
|
||||
description: null,
|
||||
ownerAgentSlug: null,
|
||||
leadAgentSlug: null,
|
||||
targetDate: null,
|
||||
color: null,
|
||||
icon: null,
|
||||
status: null,
|
||||
executionWorkspacePolicy: null,
|
||||
workspaces: [],
|
||||
env: null,
|
||||
metadata: null,
|
||||
},
|
||||
alphaProjectManifest(),
|
||||
],
|
||||
issues: [
|
||||
{
|
||||
slug: "kickoff",
|
||||
identifier: null,
|
||||
title: "Kickoff",
|
||||
path: "projects/alpha/issues/kickoff/TASK.md",
|
||||
projectSlug: "alpha",
|
||||
projectWorkspaceKey: null,
|
||||
assigneeAgentSlug: "ceo",
|
||||
description: null,
|
||||
recurring: false,
|
||||
routine: null,
|
||||
legacyRecurrence: null,
|
||||
status: null,
|
||||
priority: null,
|
||||
labelIds: [],
|
||||
billingCode: null,
|
||||
executionWorkspaceSettings: null,
|
||||
assigneeAdapterOverrides: null,
|
||||
comments: [],
|
||||
metadata: null,
|
||||
},
|
||||
kickoffIssueManifest(),
|
||||
],
|
||||
envInputs: [
|
||||
{
|
||||
key: "OPENAI_API_KEY",
|
||||
description: null,
|
||||
agentSlug: "ceo",
|
||||
projectSlug: null,
|
||||
kind: "secret",
|
||||
requirement: "required",
|
||||
defaultValue: null,
|
||||
portability: "portable",
|
||||
},
|
||||
openAiEnvInput(),
|
||||
],
|
||||
},
|
||||
files: {
|
||||
"COMPANY.md": "# Source Co",
|
||||
},
|
||||
envInputs: [
|
||||
{
|
||||
key: "OPENAI_API_KEY",
|
||||
description: null,
|
||||
agentSlug: "ceo",
|
||||
projectSlug: null,
|
||||
kind: "secret",
|
||||
requirement: "required",
|
||||
defaultValue: null,
|
||||
portability: "portable",
|
||||
},
|
||||
openAiEnvInput(),
|
||||
],
|
||||
warnings: ["One warning"],
|
||||
errors: ["One error"],
|
||||
@@ -545,13 +554,7 @@ describe("renderCompanyImportResult", () => {
|
||||
describe("import selection catalog", () => {
|
||||
it("defaults to everything and keeps project selection separate from task selection", () => {
|
||||
const preview: CompanyPortabilityPreviewResult = {
|
||||
include: {
|
||||
company: true,
|
||||
agents: true,
|
||||
projects: true,
|
||||
issues: true,
|
||||
skills: true,
|
||||
},
|
||||
include: allImportEntities(),
|
||||
targetCompanyId: "company-123",
|
||||
targetCompanyName: "Imported Co",
|
||||
collisionStrategy: "rename",
|
||||
@@ -569,13 +572,7 @@ describe("import selection catalog", () => {
|
||||
companyId: "company-src",
|
||||
companyName: "Source Co",
|
||||
},
|
||||
includes: {
|
||||
company: true,
|
||||
agents: true,
|
||||
projects: true,
|
||||
issues: true,
|
||||
skills: true,
|
||||
},
|
||||
includes: allImportEntities(),
|
||||
company: {
|
||||
path: "COMPANY.md",
|
||||
name: "Source Co",
|
||||
@@ -592,26 +589,7 @@ describe("import selection catalog", () => {
|
||||
projects: ["alpha"],
|
||||
},
|
||||
agents: [
|
||||
{
|
||||
slug: "ceo",
|
||||
name: "CEO",
|
||||
path: "agents/ceo/AGENT.md",
|
||||
skills: [],
|
||||
role: "ceo",
|
||||
title: null,
|
||||
icon: null,
|
||||
capabilities: null,
|
||||
reportsToSlug: null,
|
||||
reportsToExistingAgentId: null,
|
||||
reportsToExistingAgentSlug: null,
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
permissionGrants: [],
|
||||
budgetMonthlyCents: 0,
|
||||
metadata: null,
|
||||
},
|
||||
ceoManifest(),
|
||||
],
|
||||
skills: [
|
||||
{
|
||||
@@ -630,45 +608,10 @@ describe("import selection catalog", () => {
|
||||
},
|
||||
],
|
||||
projects: [
|
||||
{
|
||||
slug: "alpha",
|
||||
name: "Alpha",
|
||||
path: "projects/alpha/PROJECT.md",
|
||||
description: null,
|
||||
ownerAgentSlug: null,
|
||||
leadAgentSlug: null,
|
||||
targetDate: null,
|
||||
color: null,
|
||||
icon: null,
|
||||
status: null,
|
||||
executionWorkspacePolicy: null,
|
||||
workspaces: [],
|
||||
env: null,
|
||||
metadata: null,
|
||||
},
|
||||
alphaProjectManifest(),
|
||||
],
|
||||
issues: [
|
||||
{
|
||||
slug: "kickoff",
|
||||
identifier: null,
|
||||
title: "Kickoff",
|
||||
path: "projects/alpha/issues/kickoff/TASK.md",
|
||||
projectSlug: "alpha",
|
||||
projectWorkspaceKey: null,
|
||||
assigneeAgentSlug: "ceo",
|
||||
description: null,
|
||||
recurring: false,
|
||||
routine: null,
|
||||
legacyRecurrence: null,
|
||||
status: null,
|
||||
priority: null,
|
||||
labelIds: [],
|
||||
billingCode: null,
|
||||
executionWorkspaceSettings: null,
|
||||
assigneeAdapterOverrides: null,
|
||||
comments: [],
|
||||
metadata: null,
|
||||
},
|
||||
kickoffIssueManifest(),
|
||||
],
|
||||
envInputs: [],
|
||||
},
|
||||
@@ -720,13 +663,7 @@ describe("import selection catalog", () => {
|
||||
|
||||
it("includes extension file even when all entities are deselected", () => {
|
||||
const preview: CompanyPortabilityPreviewResult = {
|
||||
include: {
|
||||
company: true,
|
||||
agents: true,
|
||||
projects: true,
|
||||
issues: true,
|
||||
skills: true,
|
||||
},
|
||||
include: allImportEntities(),
|
||||
targetCompanyId: "company-123",
|
||||
targetCompanyName: "Imported Co",
|
||||
collisionStrategy: "rename",
|
||||
@@ -744,24 +681,8 @@ describe("import selection catalog", () => {
|
||||
companyId: "company-src",
|
||||
companyName: "Source Co",
|
||||
},
|
||||
includes: {
|
||||
company: true,
|
||||
agents: true,
|
||||
projects: true,
|
||||
issues: true,
|
||||
skills: true,
|
||||
},
|
||||
company: {
|
||||
path: "COMPANY.md",
|
||||
name: "Source Co",
|
||||
description: null,
|
||||
logoPath: null,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
feedbackDataSharingEnabled: false,
|
||||
feedbackDataSharingConsentAt: null,
|
||||
feedbackDataSharingConsentByUserId: null,
|
||||
feedbackDataSharingTermsVersion: null,
|
||||
},
|
||||
includes: allImportEntities(),
|
||||
company: sourceCompanyManifest(),
|
||||
sidebar: {
|
||||
agents: [],
|
||||
projects: [],
|
||||
|
||||
@@ -285,9 +285,37 @@ function buildSourceConfig(): PaperclipConfig {
|
||||
};
|
||||
}
|
||||
|
||||
function writeSeedConfigFiles(
|
||||
sourceConfigPath: string,
|
||||
targetConfigPath: string,
|
||||
targetRoot: string,
|
||||
targetPaths: ReturnType<typeof resolveWorktreeLocalPaths>,
|
||||
sourceConfig: PaperclipConfig,
|
||||
targetConfig: PaperclipConfig,
|
||||
): void {
|
||||
fs.mkdirSync(path.dirname(sourceConfigPath), { recursive: true });
|
||||
fs.mkdirSync(path.dirname(targetConfigPath), { recursive: true });
|
||||
fs.writeFileSync(sourceConfigPath, `${JSON.stringify(sourceConfig)}\n`);
|
||||
fs.writeFileSync(path.join(path.dirname(sourceConfigPath), ".env"), "PAPERCLIP_INSTANCE_ID=source\n");
|
||||
fs.writeFileSync(targetConfigPath, `${JSON.stringify(targetConfig)}\n`);
|
||||
fs.writeFileSync(
|
||||
path.join(targetRoot, ".paperclip", ".env"),
|
||||
`PAPERCLIP_HOME=${targetPaths.homeDir}\nPAPERCLIP_INSTANCE_ID=${targetPaths.instanceId}\n`,
|
||||
);
|
||||
}
|
||||
|
||||
function initializeTestRepo(repoRoot: string): void {
|
||||
execFileSync("git", ["init"], { cwd: repoRoot, stdio: "ignore" });
|
||||
execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: repoRoot, stdio: "ignore" });
|
||||
execFileSync("git", ["config", "user.name", "Test User"], { cwd: repoRoot, stdio: "ignore" });
|
||||
fs.writeFileSync(path.join(repoRoot, "README.md"), "# temp\n", "utf8");
|
||||
execFileSync("git", ["add", "README.md"], { cwd: repoRoot, stdio: "ignore" });
|
||||
execFileSync("git", ["commit", "-m", "Initial commit"], { cwd: repoRoot, stdio: "ignore" });
|
||||
}
|
||||
|
||||
describe("worktree helpers", () => {
|
||||
it("uses the repo-local config for the current worktree", () => {
|
||||
const targetRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-current-worktree-"));
|
||||
const targetRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-current-worktree-"));
|
||||
try {
|
||||
const localConfig = path.join(targetRoot, ".paperclip", "config.json");
|
||||
fs.mkdirSync(path.dirname(localConfig), { recursive: true });
|
||||
@@ -307,7 +335,7 @@ describe("worktree helpers", () => {
|
||||
});
|
||||
|
||||
it("uses the repository config from a nested working directory", () => {
|
||||
const targetRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-current-worktree-nested-"));
|
||||
const targetRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-current-worktree-nested-"));
|
||||
try {
|
||||
execFileSync("git", ["init", "-q"], { cwd: targetRoot });
|
||||
const nestedDirectory = path.join(targetRoot, "packages", "example", "src");
|
||||
@@ -517,7 +545,7 @@ describe("worktree helpers", () => {
|
||||
});
|
||||
|
||||
it("requires the seed process to own the target embedded Postgres lifecycle", async () => {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-live-target-"));
|
||||
const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-live-target-"));
|
||||
try {
|
||||
fs.writeFileSync(
|
||||
path.join(tempRoot, "postmaster.pid"),
|
||||
@@ -531,18 +559,6 @@ describe("worktree helpers", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("surfaces a credential-safe diagnostic when the target shuts down during restore", () => {
|
||||
expect(formatWorktreeSeedFailureDiagnostic(
|
||||
"restore",
|
||||
new Error(
|
||||
"Failed to restore seed.sql.gz: FATAL: the database system is shutting down; psql error: write EPIPE",
|
||||
),
|
||||
)).toBe(
|
||||
"Target embedded PostgreSQL shut down during restore. Stop any competing worktree service and retry the seed.",
|
||||
);
|
||||
expect(formatWorktreeSeedFailureDiagnostic("migrations", new Error("secret connection failure")))
|
||||
.toBe("Seed failed during migrations.");
|
||||
});
|
||||
|
||||
it("surfaces the missing credential artifact for authenticated seed validation", () => {
|
||||
expect(formatWorktreeSeedFailureDiagnostic(
|
||||
@@ -610,7 +626,7 @@ describe("worktree helpers", () => {
|
||||
itEmbeddedPostgres("recognizes positive legacy database schema evidence", async () => {
|
||||
const tempDb = await startEmbeddedPostgresTestDatabase("paperclip-worktree-legacy-evidence-");
|
||||
onTestFinished(() => tempDb.cleanup());
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-legacy-config-"));
|
||||
const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-legacy-config-"));
|
||||
try {
|
||||
const configPath = path.join(tempRoot, "config.json");
|
||||
const sourceConfig = buildSourceConfig();
|
||||
@@ -644,7 +660,7 @@ describe("worktree helpers", () => {
|
||||
});
|
||||
|
||||
it("ensure-seeded seeds once and fast-exits on the verified manifest", async () => {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-ensure-seeded-"));
|
||||
const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-ensure-seeded-"));
|
||||
try {
|
||||
const sourceConfigPath = path.join(tempRoot, "source", "config.json");
|
||||
const targetRoot = path.join(tempRoot, "worktree");
|
||||
@@ -661,15 +677,7 @@ describe("worktree helpers", () => {
|
||||
serverPort: 3199,
|
||||
databasePort: 54999,
|
||||
});
|
||||
fs.mkdirSync(path.dirname(sourceConfigPath), { recursive: true });
|
||||
fs.mkdirSync(path.dirname(targetConfigPath), { recursive: true });
|
||||
fs.writeFileSync(sourceConfigPath, `${JSON.stringify(sourceConfig)}\n`);
|
||||
fs.writeFileSync(path.join(path.dirname(sourceConfigPath), ".env"), "PAPERCLIP_INSTANCE_ID=source\n");
|
||||
fs.writeFileSync(targetConfigPath, `${JSON.stringify(targetConfig)}\n`);
|
||||
fs.writeFileSync(
|
||||
path.join(targetRoot, ".paperclip", ".env"),
|
||||
`PAPERCLIP_HOME=${targetPaths.homeDir}\nPAPERCLIP_INSTANCE_ID=${targetPaths.instanceId}\n`,
|
||||
);
|
||||
writeSeedConfigFiles(sourceConfigPath, targetConfigPath, targetRoot, targetPaths, sourceConfig, targetConfig);
|
||||
markWorktreeSeedPending({ configPath: targetConfigPath, sourceConfigPath });
|
||||
|
||||
const seedDatabase = vi.fn().mockResolvedValue({
|
||||
@@ -715,7 +723,7 @@ describe("worktree helpers", () => {
|
||||
});
|
||||
|
||||
it("treats an unregistered markerless config as a normal non-worktree boot", async () => {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-unregistered-markerless-"));
|
||||
const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-unregistered-markerless-"));
|
||||
try {
|
||||
const configPath = path.join(tempRoot, "config.json");
|
||||
fs.writeFileSync(configPath, `${JSON.stringify(buildSourceConfig())}\n`);
|
||||
@@ -740,7 +748,7 @@ describe("worktree helpers", () => {
|
||||
});
|
||||
|
||||
it("honors a legacy complete marker without resolving a seed source", async () => {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-complete-marker-"));
|
||||
const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-complete-marker-"));
|
||||
try {
|
||||
const configPath = path.join(tempRoot, "config.json");
|
||||
fs.writeFileSync(configPath, `${JSON.stringify(buildSourceConfig())}\n`);
|
||||
@@ -763,7 +771,7 @@ describe("worktree helpers", () => {
|
||||
});
|
||||
|
||||
it("seeds a configured worktree with no seed markers when no legacy database is present", async () => {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-unmarked-empty-"));
|
||||
const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-unmarked-empty-"));
|
||||
try {
|
||||
const sourceConfigPath = path.join(tempRoot, "source", "config.json");
|
||||
const targetRoot = path.join(tempRoot, "worktree");
|
||||
@@ -780,15 +788,7 @@ describe("worktree helpers", () => {
|
||||
serverPort: 3194,
|
||||
databasePort: 54994,
|
||||
});
|
||||
fs.mkdirSync(path.dirname(sourceConfigPath), { recursive: true });
|
||||
fs.mkdirSync(path.dirname(targetConfigPath), { recursive: true });
|
||||
fs.writeFileSync(sourceConfigPath, `${JSON.stringify(sourceConfig)}\n`);
|
||||
fs.writeFileSync(path.join(path.dirname(sourceConfigPath), ".env"), "PAPERCLIP_INSTANCE_ID=source\n");
|
||||
fs.writeFileSync(targetConfigPath, `${JSON.stringify(targetConfig)}\n`);
|
||||
fs.writeFileSync(
|
||||
path.join(targetRoot, ".paperclip", ".env"),
|
||||
`PAPERCLIP_HOME=${targetPaths.homeDir}\nPAPERCLIP_INSTANCE_ID=${targetPaths.instanceId}\n`,
|
||||
);
|
||||
writeSeedConfigFiles(sourceConfigPath, targetConfigPath, targetRoot, targetPaths, sourceConfig, targetConfig);
|
||||
const inspectLegacyDatabase = vi.fn().mockResolvedValue(null);
|
||||
const seedDatabase = vi.fn().mockResolvedValue(mockVerifiedSeedResult());
|
||||
|
||||
@@ -810,7 +810,7 @@ describe("worktree helpers", () => {
|
||||
});
|
||||
|
||||
it("adopts a markerless legacy worktree only after validating its database schema", async () => {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-unmarked-legacy-"));
|
||||
const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-unmarked-legacy-"));
|
||||
try {
|
||||
const sourceConfigPath = path.join(tempRoot, "source", "config.json");
|
||||
const targetRoot = path.join(tempRoot, "worktree");
|
||||
@@ -827,15 +827,7 @@ describe("worktree helpers", () => {
|
||||
serverPort: 3193,
|
||||
databasePort: 54993,
|
||||
});
|
||||
fs.mkdirSync(path.dirname(sourceConfigPath), { recursive: true });
|
||||
fs.mkdirSync(path.dirname(targetConfigPath), { recursive: true });
|
||||
fs.writeFileSync(sourceConfigPath, `${JSON.stringify(sourceConfig)}\n`);
|
||||
fs.writeFileSync(path.join(path.dirname(sourceConfigPath), ".env"), "PAPERCLIP_INSTANCE_ID=source\n");
|
||||
fs.writeFileSync(targetConfigPath, `${JSON.stringify(targetConfig)}\n`);
|
||||
fs.writeFileSync(
|
||||
path.join(targetRoot, ".paperclip", ".env"),
|
||||
`PAPERCLIP_HOME=${targetPaths.homeDir}\nPAPERCLIP_INSTANCE_ID=${targetPaths.instanceId}\n`,
|
||||
);
|
||||
writeSeedConfigFiles(sourceConfigPath, targetConfigPath, targetRoot, targetPaths, sourceConfig, targetConfig);
|
||||
const seedDatabase = vi.fn();
|
||||
|
||||
await expect(ensureWorktreeSeeded(
|
||||
@@ -858,7 +850,7 @@ describe("worktree helpers", () => {
|
||||
});
|
||||
|
||||
it("managed ensure-seeded derives a valid source from the registered base workspace", async () => {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-managed-seed-"));
|
||||
const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-managed-seed-"));
|
||||
try {
|
||||
const baseRoot = path.join(tempRoot, "base");
|
||||
const sourceConfigPath = path.join(baseRoot, ".paperclip", "config.json");
|
||||
@@ -907,7 +899,7 @@ describe("worktree helpers", () => {
|
||||
it.each(["sibling", "foreign_instance", "symlink", "instance_mismatch"] as const)(
|
||||
"managed ensure-seeded re-derives a stale %s manifest source from registration",
|
||||
async (variant) => {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), `paperclip-worktree-managed-${variant}-`));
|
||||
const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), `paperclip-worktree-managed-${variant}-`));
|
||||
try {
|
||||
const baseRoot = path.join(tempRoot, "base");
|
||||
const canonicalSource = path.join(baseRoot, ".paperclip", "config.json");
|
||||
@@ -981,7 +973,9 @@ describe("worktree helpers", () => {
|
||||
);
|
||||
|
||||
it("ensure-seeded records a target shutdown diagnostic when restore fails", async () => {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-ensure-seeded-failure-"));
|
||||
expect(formatWorktreeSeedFailureDiagnostic("migrations", new Error("secret connection failure")))
|
||||
.toBe("Seed failed during migrations.");
|
||||
const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-ensure-seeded-failure-"));
|
||||
try {
|
||||
const sourceConfigPath = path.join(tempRoot, "source", "config.json");
|
||||
const targetRoot = path.join(tempRoot, "worktree");
|
||||
@@ -998,15 +992,7 @@ describe("worktree helpers", () => {
|
||||
serverPort: 3198,
|
||||
databasePort: 54998,
|
||||
});
|
||||
fs.mkdirSync(path.dirname(sourceConfigPath), { recursive: true });
|
||||
fs.mkdirSync(path.dirname(targetConfigPath), { recursive: true });
|
||||
fs.writeFileSync(sourceConfigPath, `${JSON.stringify(sourceConfig)}\n`);
|
||||
fs.writeFileSync(path.join(path.dirname(sourceConfigPath), ".env"), "PAPERCLIP_INSTANCE_ID=source\n");
|
||||
fs.writeFileSync(targetConfigPath, `${JSON.stringify(targetConfig)}\n`);
|
||||
fs.writeFileSync(
|
||||
path.join(targetRoot, ".paperclip", ".env"),
|
||||
`PAPERCLIP_HOME=${targetPaths.homeDir}\nPAPERCLIP_INSTANCE_ID=${targetPaths.instanceId}\n`,
|
||||
);
|
||||
writeSeedConfigFiles(sourceConfigPath, targetConfigPath, targetRoot, targetPaths, sourceConfig, targetConfig);
|
||||
markWorktreeSeedPending({ configPath: targetConfigPath, sourceConfigPath });
|
||||
|
||||
await expect(
|
||||
@@ -1044,7 +1030,7 @@ describe("worktree helpers", () => {
|
||||
});
|
||||
|
||||
it("serializes concurrent ensure-seeded calls across the seed marker lock", async () => {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-ensure-seeded-lock-"));
|
||||
const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-ensure-seeded-lock-"));
|
||||
try {
|
||||
const sourceConfigPath = path.join(tempRoot, "source", "config.json");
|
||||
const targetRoot = path.join(tempRoot, "worktree");
|
||||
@@ -1061,15 +1047,7 @@ describe("worktree helpers", () => {
|
||||
serverPort: 3197,
|
||||
databasePort: 54997,
|
||||
});
|
||||
fs.mkdirSync(path.dirname(sourceConfigPath), { recursive: true });
|
||||
fs.mkdirSync(path.dirname(targetConfigPath), { recursive: true });
|
||||
fs.writeFileSync(sourceConfigPath, `${JSON.stringify(sourceConfig)}\n`);
|
||||
fs.writeFileSync(path.join(path.dirname(sourceConfigPath), ".env"), "PAPERCLIP_INSTANCE_ID=source\n");
|
||||
fs.writeFileSync(targetConfigPath, `${JSON.stringify(targetConfig)}\n`);
|
||||
fs.writeFileSync(
|
||||
path.join(targetRoot, ".paperclip", ".env"),
|
||||
`PAPERCLIP_HOME=${targetPaths.homeDir}\nPAPERCLIP_INSTANCE_ID=${targetPaths.instanceId}\n`,
|
||||
);
|
||||
writeSeedConfigFiles(sourceConfigPath, targetConfigPath, targetRoot, targetPaths, sourceConfig, targetConfig);
|
||||
markWorktreeSeedPending({ configPath: targetConfigPath, sourceConfigPath });
|
||||
|
||||
const seedDatabase = vi.fn(async () => {
|
||||
@@ -1094,7 +1072,7 @@ describe("worktree helpers", () => {
|
||||
});
|
||||
|
||||
it("records an interrupted phase before retrying to a verified terminal state", async () => {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-interrupted-seed-"));
|
||||
const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-interrupted-seed-"));
|
||||
try {
|
||||
const sourceConfigPath = path.join(tempRoot, "source", "config.json");
|
||||
const targetRoot = path.join(tempRoot, "worktree");
|
||||
@@ -1111,15 +1089,7 @@ describe("worktree helpers", () => {
|
||||
serverPort: 3196,
|
||||
databasePort: 54996,
|
||||
});
|
||||
fs.mkdirSync(path.dirname(sourceConfigPath), { recursive: true });
|
||||
fs.mkdirSync(path.dirname(targetConfigPath), { recursive: true });
|
||||
fs.writeFileSync(sourceConfigPath, `${JSON.stringify(sourceConfig)}\n`);
|
||||
fs.writeFileSync(path.join(path.dirname(sourceConfigPath), ".env"), "PAPERCLIP_INSTANCE_ID=source\n");
|
||||
fs.writeFileSync(targetConfigPath, `${JSON.stringify(targetConfig)}\n`);
|
||||
fs.writeFileSync(
|
||||
path.join(targetRoot, ".paperclip", ".env"),
|
||||
`PAPERCLIP_HOME=${targetPaths.homeDir}\nPAPERCLIP_INSTANCE_ID=${targetPaths.instanceId}\n`,
|
||||
);
|
||||
writeSeedConfigFiles(sourceConfigPath, targetConfigPath, targetRoot, targetPaths, sourceConfig, targetConfig);
|
||||
markWorktreeSeedPending({ configPath: targetConfigPath, sourceConfigPath });
|
||||
const interrupted = readWorktreeSeedManifest(targetConfigPath)!;
|
||||
fs.writeFileSync(
|
||||
@@ -1147,7 +1117,7 @@ describe("worktree helpers", () => {
|
||||
});
|
||||
|
||||
it("fails closed instead of racing to reclaim a stale seed lock", async () => {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-ensure-seeded-stale-lock-"));
|
||||
const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-ensure-seeded-stale-lock-"));
|
||||
try {
|
||||
const targetConfigPath = path.join(tempRoot, ".paperclip", "config.json");
|
||||
const lockPath = path.join(tempRoot, ".paperclip", "seed.lock");
|
||||
@@ -1413,7 +1383,7 @@ describe("worktree helpers", () => {
|
||||
});
|
||||
|
||||
it("copies the source local_encrypted secrets key into the seeded worktree instance", () => {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-secrets-"));
|
||||
const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-secrets-"));
|
||||
const originalInlineMasterKey = process.env.PAPERCLIP_SECRETS_MASTER_KEY;
|
||||
const originalKeyFile = process.env.PAPERCLIP_SECRETS_MASTER_KEY_FILE;
|
||||
try {
|
||||
@@ -1452,7 +1422,7 @@ describe("worktree helpers", () => {
|
||||
});
|
||||
|
||||
it("writes the source inline secrets master key into the seeded worktree instance", () => {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-secrets-"));
|
||||
const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-secrets-"));
|
||||
try {
|
||||
const sourceConfigPath = path.join(tempRoot, "source", "config.json");
|
||||
const targetKeyPath = path.join(tempRoot, "target", "secrets", "master.key");
|
||||
@@ -1473,7 +1443,7 @@ describe("worktree helpers", () => {
|
||||
});
|
||||
|
||||
it("creates an explicitly empty worktree without inherited signing secrets or deferred copying", async () => {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-empty-"));
|
||||
const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-empty-"));
|
||||
const originalCwd = process.cwd();
|
||||
const originalJwt = process.env.PAPERCLIP_AGENT_JWT_SECRET;
|
||||
const originalSigning = process.env.PAPERCLIP_TOOL_ACTION_SIGNING_SECRET;
|
||||
@@ -1503,7 +1473,7 @@ describe("worktree helpers", () => {
|
||||
});
|
||||
|
||||
it("persists the current agent jwt secret into the worktree env file", async () => {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-jwt-"));
|
||||
const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-jwt-"));
|
||||
const repoRoot = path.join(tempRoot, "repo");
|
||||
const originalCwd = process.cwd();
|
||||
const originalJwtSecret = process.env.PAPERCLIP_AGENT_JWT_SECRET;
|
||||
@@ -1544,7 +1514,7 @@ describe("worktree helpers", () => {
|
||||
});
|
||||
|
||||
it("preserves repo-managed worktree checkouts when --force re-runs from the source repo", async () => {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-force-preserve-"));
|
||||
const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-force-preserve-"));
|
||||
const repoRoot = path.join(tempRoot, "repo");
|
||||
const originalCwd = process.cwd();
|
||||
|
||||
@@ -1585,7 +1555,7 @@ describe("worktree helpers", () => {
|
||||
itEmbeddedPostgres(
|
||||
"seeds a local-trusted implicit board user without a credential account",
|
||||
async () => {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-local-board-seed-"));
|
||||
const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-local-board-seed-"));
|
||||
const originalCwd = process.cwd();
|
||||
onTestFinished(() => {
|
||||
process.chdir(originalCwd);
|
||||
@@ -1687,7 +1657,7 @@ describe("worktree helpers", () => {
|
||||
itEmbeddedPostgres(
|
||||
"seeds a lagging source whose migration application order differs from filename order",
|
||||
async () => {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-auth-seed-"));
|
||||
const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-auth-seed-"));
|
||||
const originalCwd = process.cwd();
|
||||
onTestFinished(() => {
|
||||
process.chdir(originalCwd);
|
||||
@@ -1866,7 +1836,7 @@ describe("worktree helpers", () => {
|
||||
);
|
||||
|
||||
it("avoids ports already claimed by sibling worktree instance configs", async () => {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-claimed-ports-"));
|
||||
const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-claimed-ports-"));
|
||||
const repoRoot = path.join(tempRoot, "repo");
|
||||
const homeDir = path.join(tempRoot, ".paperclip-worktrees");
|
||||
const siblingInstanceRoot = path.join(homeDir, "instances", "existing-worktree");
|
||||
@@ -1947,7 +1917,7 @@ describe("worktree helpers", () => {
|
||||
});
|
||||
|
||||
it("reserves distinct ports for postgres-mode siblings under a custom worktree parent", async () => {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-custom-parent-"));
|
||||
const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-custom-parent-"));
|
||||
const homeDir = path.join(tempRoot, ".paperclip-worktrees");
|
||||
const customParentDir = path.join(tempRoot, "custom", "workspace-lanes");
|
||||
const firstWorktreeRoot = path.join(customParentDir, "lane-one");
|
||||
@@ -2002,7 +1972,7 @@ describe("worktree helpers", () => {
|
||||
});
|
||||
|
||||
it("defaults the seed source config to the current repo-local Paperclip config", () => {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-source-config-"));
|
||||
const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-source-config-"));
|
||||
const repoRoot = path.join(tempRoot, "repo");
|
||||
const localConfigPath = path.join(repoRoot, ".paperclip", "config.json");
|
||||
const originalCwd = process.cwd();
|
||||
@@ -2027,7 +1997,7 @@ describe("worktree helpers", () => {
|
||||
});
|
||||
|
||||
it("preserves the source config path across worktree:make cwd changes", () => {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-source-override-"));
|
||||
const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-source-override-"));
|
||||
const sourceConfigPath = path.join(tempRoot, "source", "config.json");
|
||||
const targetRoot = path.join(tempRoot, "target");
|
||||
const originalCwd = process.cwd();
|
||||
@@ -2070,7 +2040,7 @@ describe("worktree helpers", () => {
|
||||
});
|
||||
|
||||
it("derives worktree reseed target paths from the adjacent env file", () => {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-reseed-target-"));
|
||||
const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-reseed-target-"));
|
||||
const worktreeRoot = path.join(tempRoot, "repo");
|
||||
const configPath = path.join(worktreeRoot, ".paperclip", "config.json");
|
||||
const envPath = path.join(worktreeRoot, ".paperclip", ".env");
|
||||
@@ -2102,7 +2072,7 @@ describe("worktree helpers", () => {
|
||||
});
|
||||
|
||||
it("rejects reseed targets without worktree env metadata", () => {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-reseed-target-missing-"));
|
||||
const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-reseed-target-missing-"));
|
||||
const worktreeRoot = path.join(tempRoot, "repo");
|
||||
const configPath = path.join(worktreeRoot, ".paperclip", "config.json");
|
||||
|
||||
@@ -2127,7 +2097,7 @@ describe("worktree helpers", () => {
|
||||
});
|
||||
|
||||
itEmbeddedPostgres("reseed preserves the current worktree ports, instance id, and branding", async () => {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-reseed-"));
|
||||
const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-reseed-"));
|
||||
const repoRoot = path.join(tempRoot, "repo");
|
||||
const sourceRoot = path.join(tempRoot, "source");
|
||||
const homeDir = path.join(tempRoot, ".paperclip-worktrees");
|
||||
@@ -2232,7 +2202,7 @@ describe("worktree helpers", () => {
|
||||
});
|
||||
|
||||
it("restores the current worktree config and instance data if reseed fails", async () => {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-reseed-rollback-"));
|
||||
const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-reseed-rollback-"));
|
||||
const repoRoot = path.join(tempRoot, "repo");
|
||||
const sourceRoot = path.join(tempRoot, "source");
|
||||
const homeDir = path.join(tempRoot, ".paperclip-worktrees");
|
||||
@@ -2341,18 +2311,13 @@ describe("worktree helpers", () => {
|
||||
});
|
||||
|
||||
it("copies shared git hooks into a linked worktree git dir", () => {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-hooks-"));
|
||||
const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-hooks-"));
|
||||
const repoRoot = path.join(tempRoot, "repo");
|
||||
const worktreePath = path.join(tempRoot, "repo-feature");
|
||||
|
||||
try {
|
||||
fs.mkdirSync(repoRoot, { recursive: true });
|
||||
execFileSync("git", ["init"], { cwd: repoRoot, stdio: "ignore" });
|
||||
execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: repoRoot, stdio: "ignore" });
|
||||
execFileSync("git", ["config", "user.name", "Test User"], { cwd: repoRoot, stdio: "ignore" });
|
||||
fs.writeFileSync(path.join(repoRoot, "README.md"), "# temp\n", "utf8");
|
||||
execFileSync("git", ["add", "README.md"], { cwd: repoRoot, stdio: "ignore" });
|
||||
execFileSync("git", ["commit", "-m", "Initial commit"], { cwd: repoRoot, stdio: "ignore" });
|
||||
initializeTestRepo(repoRoot);
|
||||
|
||||
const sourceHooksDir = path.join(repoRoot, ".git", "hooks");
|
||||
const sourceHookPath = path.join(sourceHooksDir, "pre-commit");
|
||||
@@ -2389,7 +2354,7 @@ describe("worktree helpers", () => {
|
||||
}, 15_000);
|
||||
|
||||
it("creates and initializes a worktree from the top-level worktree:make command", async () => {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-make-"));
|
||||
const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-make-"));
|
||||
const repoRoot = path.join(tempRoot, "repo");
|
||||
const fakeHome = path.join(tempRoot, "home");
|
||||
const worktreePath = path.join(fakeHome, "paperclip-make-test");
|
||||
@@ -2399,12 +2364,7 @@ describe("worktree helpers", () => {
|
||||
try {
|
||||
fs.mkdirSync(repoRoot, { recursive: true });
|
||||
fs.mkdirSync(fakeHome, { recursive: true });
|
||||
execFileSync("git", ["init"], { cwd: repoRoot, stdio: "ignore" });
|
||||
execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: repoRoot, stdio: "ignore" });
|
||||
execFileSync("git", ["config", "user.name", "Test User"], { cwd: repoRoot, stdio: "ignore" });
|
||||
fs.writeFileSync(path.join(repoRoot, "README.md"), "# temp\n", "utf8");
|
||||
execFileSync("git", ["add", "README.md"], { cwd: repoRoot, stdio: "ignore" });
|
||||
execFileSync("git", ["commit", "-m", "Initial commit"], { cwd: repoRoot, stdio: "ignore" });
|
||||
initializeTestRepo(repoRoot);
|
||||
|
||||
process.chdir(repoRoot);
|
||||
|
||||
@@ -2424,18 +2384,13 @@ describe("worktree helpers", () => {
|
||||
}, 20_000);
|
||||
|
||||
it("no-ops on the primary checkout unless --branch is provided", async () => {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-repair-primary-"));
|
||||
const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-repair-primary-"));
|
||||
const repoRoot = path.join(tempRoot, "repo");
|
||||
const originalCwd = process.cwd();
|
||||
|
||||
try {
|
||||
fs.mkdirSync(repoRoot, { recursive: true });
|
||||
execFileSync("git", ["init"], { cwd: repoRoot, stdio: "ignore" });
|
||||
execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: repoRoot, stdio: "ignore" });
|
||||
execFileSync("git", ["config", "user.name", "Test User"], { cwd: repoRoot, stdio: "ignore" });
|
||||
fs.writeFileSync(path.join(repoRoot, "README.md"), "# temp\n", "utf8");
|
||||
execFileSync("git", ["add", "README.md"], { cwd: repoRoot, stdio: "ignore" });
|
||||
execFileSync("git", ["commit", "-m", "Initial commit"], { cwd: repoRoot, stdio: "ignore" });
|
||||
initializeTestRepo(repoRoot);
|
||||
|
||||
process.chdir(repoRoot);
|
||||
await worktreeRepairCommand({});
|
||||
@@ -2449,7 +2404,7 @@ describe("worktree helpers", () => {
|
||||
});
|
||||
|
||||
it("repairs the current linked worktree when Paperclip metadata is missing", async () => {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-repair-current-"));
|
||||
const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-repair-current-"));
|
||||
const repoRoot = path.join(tempRoot, "repo");
|
||||
const worktreePath = path.join(repoRoot, ".paperclip", "worktrees", "repair-me");
|
||||
const sourceConfigPath = path.join(tempRoot, "source-config.json");
|
||||
@@ -2463,12 +2418,7 @@ describe("worktree helpers", () => {
|
||||
|
||||
try {
|
||||
fs.mkdirSync(repoRoot, { recursive: true });
|
||||
execFileSync("git", ["init"], { cwd: repoRoot, stdio: "ignore" });
|
||||
execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: repoRoot, stdio: "ignore" });
|
||||
execFileSync("git", ["config", "user.name", "Test User"], { cwd: repoRoot, stdio: "ignore" });
|
||||
fs.writeFileSync(path.join(repoRoot, "README.md"), "# temp\n", "utf8");
|
||||
execFileSync("git", ["add", "README.md"], { cwd: repoRoot, stdio: "ignore" });
|
||||
execFileSync("git", ["commit", "-m", "Initial commit"], { cwd: repoRoot, stdio: "ignore" });
|
||||
initializeTestRepo(repoRoot);
|
||||
fs.mkdirSync(path.dirname(worktreePath), { recursive: true });
|
||||
execFileSync("git", ["worktree", "add", "-b", "repair-me", worktreePath, "HEAD"], {
|
||||
cwd: repoRoot,
|
||||
@@ -2496,7 +2446,7 @@ describe("worktree helpers", () => {
|
||||
}, 20_000);
|
||||
|
||||
it("creates and repairs a missing branch worktree when --branch is provided", async () => {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-repair-branch-"));
|
||||
const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-repair-branch-"));
|
||||
const repoRoot = path.join(tempRoot, "repo");
|
||||
const sourceConfigPath = path.join(tempRoot, "source-config.json");
|
||||
const worktreeHome = path.join(tempRoot, ".paperclip-worktrees");
|
||||
@@ -2505,12 +2455,7 @@ describe("worktree helpers", () => {
|
||||
|
||||
try {
|
||||
fs.mkdirSync(repoRoot, { recursive: true });
|
||||
execFileSync("git", ["init"], { cwd: repoRoot, stdio: "ignore" });
|
||||
execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: repoRoot, stdio: "ignore" });
|
||||
execFileSync("git", ["config", "user.name", "Test User"], { cwd: repoRoot, stdio: "ignore" });
|
||||
fs.writeFileSync(path.join(repoRoot, "README.md"), "# temp\n", "utf8");
|
||||
execFileSync("git", ["add", "README.md"], { cwd: repoRoot, stdio: "ignore" });
|
||||
execFileSync("git", ["commit", "-m", "Initial commit"], { cwd: repoRoot, stdio: "ignore" });
|
||||
initializeTestRepo(repoRoot);
|
||||
fs.writeFileSync(sourceConfigPath, JSON.stringify(buildSourceConfig(), null, 2), "utf8");
|
||||
|
||||
process.chdir(repoRoot);
|
||||
|
||||
@@ -701,6 +701,18 @@ this non-secret binding for this namespace; Slack bot installation does not use
|
||||
PKCE. Removal and manual recovery invalidate outstanding attempts under the same
|
||||
credential-mutation lease used by configuration.
|
||||
|
||||
### Slack manager workspace grants
|
||||
|
||||
`chat_slack_manager_grants` stores a personal workspace authorization, scoped by
|
||||
company, Paperclip user, Slack user, workspace and manager app. Access/refresh
|
||||
credentials are references to user-scoped vault entries, never plaintext columns.
|
||||
Revision fencing protects reauthorization from concurrent revocation. Per-method
|
||||
rate-limit deadlines retain Slack's Retry-After across setup retries.
|
||||
|
||||
`chat_slack_registrations.manager_grant_id` is optional, with a composite company
|
||||
foreign key. Existing registrations remain customer owned with a null reference.
|
||||
Deleting one endpoint removes its registration credentials and attempts, but does
|
||||
not revoke grants used by other bots. See [Slack setup](connections/slack-setup.md).
|
||||
|
||||
## Transaction-aware delivery work signals
|
||||
|
||||
|
||||
@@ -35,6 +35,28 @@ New workflow versions merged into master then receive runner access without a
|
||||
separate SHA allowlist update. Dependabot leaves this first-party reference on
|
||||
master.
|
||||
|
||||
Runner selection calls `ci-routing.yml@master` on the dedicated
|
||||
`paperclip-ci-routing-x64` AWS Fleet. The `paperclip-ci-routing` runner group
|
||||
admits only `paperclipai/paperclip/.github/workflows/ci-routing.yml@refs/heads/master`
|
||||
and repository ID `1170821064`. GitHub evaluates the canonical repository, author, and sender checks before
|
||||
allocating reserved capacity. The validated admission snapshot (`AWS_CI_ROUTING_USER_IDS_SOURCE` and
|
||||
`AWS_CI_ROUTING_USER_IDS`) must match `AWS_CI_TRUSTED_USER_IDS`. Refresh it through
|
||||
the runner-group configuration helper when the roster changes. Missing, stale,
|
||||
or malformed settings keep hosted routing available without evaluating JSON
|
||||
in the job's placement expression. Unapproved PRs use hosted metadata routing;
|
||||
approved manual canaries on master can use the reserved pool. The routing
|
||||
workflow reads GitHub metadata without checking out or executing PR code. Its author, sender, rerun actor,
|
||||
repository, head, and merge validation still decide whether tests can use AWS.
|
||||
Unapproved test workloads continue to use GitHub-hosted runners.
|
||||
|
||||
The routing Fleet has two warm On-Demand runners and a four-runner ceiling.
|
||||
Test fleets cannot consume its runner slots. Hosted test queues therefore do
|
||||
not block AWS runner selection. A manual dispatch of `ci-routing.yml` on master
|
||||
checks placement without launching the test matrix; its non-PR event returns
|
||||
the hosted test-runner output. Configure and verify the group and Fleet before
|
||||
merging this caller. To roll back, restore the hosted gate and retain the routing
|
||||
workflow authorization until assigned jobs finish.
|
||||
|
||||
Keep the `.github/**` rule in `.github/CODEOWNERS` and the active master ruleset's
|
||||
code-owner review requirement enabled. This covers the caller, the trusted
|
||||
workflow, and CODEOWNERS itself. Existing administrator pull-request bypasses
|
||||
@@ -102,6 +124,20 @@ API responses and MCP transports keep their existing compression behavior.
|
||||
|
||||
The board UI Storybook keeps stories and Storybook config under `ui/storybook/` so component review files stay out of the app source routes.
|
||||
|
||||
Stories are grouped in `ui/storybook/stories/<feature-area>/` and the sidebar follows
|
||||
the feature map's sections and feature names. Shared fixtures live in
|
||||
`ui/storybook/support/`. Keep each suite's explicit meta `id` and catalog `stableId`
|
||||
when changing titles or locations so existing review links remain valid.
|
||||
|
||||
Start from the downstream [Storybook feature index](../maps/storybook-map/INDEX.md)
|
||||
to find existing stories by capability. Its [contract](../maps/storybook-map/README.md)
|
||||
separates production subjects, prototype implementations, simulated data, and
|
||||
source-review status. Use `maintain-storybook` to reconcile stories/fixtures with
|
||||
the feature map, and `review-storybook` to exercise them with existing tools.
|
||||
Run `node scripts/storybook-catalog.mjs` after affected story or review-source
|
||||
changes; review drift before updating fingerprints, then regenerate with `--write`.
|
||||
The feature map remains an upstream input and needs no Storybook-specific fields.
|
||||
|
||||
**Chat & Comments → Human identity** covers other-human names and avatars on
|
||||
left-aligned task messages alongside agents; only the viewer’s own messages are blue/right-aligned. Its review guide explains the data flow; the task journey uses representative
|
||||
fixtures in the production shell and thread with an agent and two humans, both
|
||||
@@ -2263,3 +2299,12 @@ Chromium. The test starts its own loopback Vite server and mocks API responses;
|
||||
it needs no running Paperclip instance or provider credentials. The same spec lives
|
||||
in the default `test:e2e` discovery tree, so the existing Chrome CI shards run it
|
||||
on pull requests.
|
||||
|
||||
### Slack provisioning paths
|
||||
|
||||
Self-hosted development continues to use independent Slack app setup. Managed
|
||||
Slack installation is a disabled-by-default Cloud feature that also requires the
|
||||
companion broker profile and Slack approval. Do not add manager client credentials
|
||||
to a local `.env` or an open-source deployment. See
|
||||
[Slack setup and Cloud qualification](connections/slack-setup.md) for the gates,
|
||||
recovery behavior, and production-component stories.
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Current source integration — 2026-10-12
|
||||
|
||||
Pi profile 24 (`sha256:d25d674cda9a0853bdbf0521025099f1424c573d2f533630cb67d90e1bfcda3b`)
|
||||
Pi profile 25 (`sha256:dd67387487dadb08da76534c057dc0aa059f7698d1779cbd5fc452813828167a`)
|
||||
advances the shared sandbox source attestation after Claude settings initialization
|
||||
moved behind provider lifetime ownership. Profile 23 remains an immutable
|
||||
historical identity. Pi's provider executable closures, model/effort options and
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Current source integration — 2026-10-12
|
||||
|
||||
Pi profile 24 (`sha256:d25d674cda9a0853bdbf0521025099f1424c573d2f533630cb67d90e1bfcda3b`)
|
||||
Pi profile 25 (`sha256:dd67387487dadb08da76534c057dc0aa059f7698d1779cbd5fc452813828167a`)
|
||||
advances the shared sandbox source attestation after Claude settings initialization
|
||||
moved behind provider lifetime ownership. Profile 23 remains an immutable
|
||||
historical identity. Pi's provider executable closures, model/effort options and
|
||||
|
||||
@@ -77,4 +77,4 @@ Task setup defaults to personal identity when supported and the requesting agent
|
||||
|
||||
Service and native-authority tests cover discovery, current identity, company boundaries, cross-run deduplication, additive grants and installs, permission denial, resolution atomicity, restart delivery and stale assignment. `tests/e2e/in-feed-native/playwright.config.ts` starts source `test-drive` instances with fresh data directories and a deterministic fake Codex provider plus MCP server. It exercises the real native runner and gateway; it is fixture proof, not live Notion or GitHub proof. Run with `pnpm exec playwright test -c tests/e2e/in-feed-native/playwright.config.ts`.
|
||||
|
||||
Offline Storybook examples live in `ui/storybook/stories/in-feed-connections.stories.tsx`. Build with `pnpm --filter @paperclipai/ui build-storybook`, then run `pnpm exec playwright test -c tests/storybook-visual/in-feed-connections.config.ts`. The suite checks every independently addressable story in both themes, catches play-function failures, and saves screenshots. Live provider acceptance additionally requires a model credential and a test workspace/account; do not describe fixture results or local-trusted testing as authenticated/cloud acceptance.
|
||||
Offline Storybook examples live in `ui/storybook/stories/connection-setup/in-feed-connections.stories.tsx`. Build with `pnpm --filter @paperclipai/ui build-storybook`, then run `pnpm exec playwright test -c tests/storybook-visual/in-feed-connections.config.ts`. The suite checks every independently addressable story in both themes, catches play-function failures, and saves screenshots. Live provider acceptance additionally requires a model credential and a test workspace/account; do not describe fixture results or local-trusted testing as authenticated/cloud acceptance.
|
||||
@@ -0,0 +1,162 @@
|
||||
# Slack app ownership and setup
|
||||
|
||||
Slack has one chat runtime and two provisioning paths. A saved connection's
|
||||
`slackSetupMethod` selects provisioning; it does not change tools, scopes,
|
||||
conversation routing, people permissions, or channel access.
|
||||
|
||||
| Method | App creation and installation | Default |
|
||||
| --- | --- | --- |
|
||||
| `managed` | A personal Slack manager grant creates a child app; managed installation supplies its bot token. Slack consent/admin approval remains a fallback. | New drafts on qualified Cloud deployments only |
|
||||
| `automatic` | A temporary app configuration access token creates a customer-owned app; the user approves its OAuth installation. | New self-hosted drafts and Cloud without managed availability |
|
||||
| `manual` | User creates an app from the canonical manifest and enters credentials. | Legacy drafts with no saved method |
|
||||
| `existing` | User supplies credentials for an existing app. | Explicit selection |
|
||||
|
||||
Existing drafts and active bots retain their method. Enabling managed setup never
|
||||
adopts existing apps. Choose agent offers **Use your own app** under **Advanced**, below Slash command,
|
||||
when the instance supports managed setup and creation has not started. The own-app flow retains **Create manually** and **Use an existing
|
||||
app** under Advanced. After creation dispatch, a managed draft cannot switch paths.
|
||||
|
||||
Managed setup has two visible stops: Choose agent and Connect. **Continue** starts
|
||||
manager authorization or installation in the saved workspace automatically. Multiple
|
||||
workspaces still require a selection. Recovery actions remain explicit; opening
|
||||
a failed or uncertain registration never retries creation automatically.
|
||||
Creation, installation, message waiting, and success use the same production
|
||||
components and server evidence as the own-app flow. A click or OAuth return does
|
||||
not constitute successful configuration.
|
||||
|
||||
## Cloud deployment prerequisites
|
||||
|
||||
Managed setup is disabled by default. Qualification requires all of:
|
||||
|
||||
- The instance chat-connector rollout gate and connection-management permission.
|
||||
- Verified Cloud runtime identity, connector broker signing/sealing configuration,
|
||||
and valid independently configured public board and webhook HTTPS origins.
|
||||
- `PAPERCLIP_SLACK_MANAGED_SETUP_ENABLED=true` on the approved tenant deployment.
|
||||
- The companion Cloud broker deployment with the explicitly enabled `slack.manager`
|
||||
profile, approved Slack manager app, token rotation, and its client secret in
|
||||
the broker's secret store. No manager client secret belongs in the tenant.
|
||||
|
||||
The broker must advertise its manager app identity as well as the enabled profile.
|
||||
Previously saved grants from a different manager app are not reused. A broker
|
||||
without this capability leaves managed setup unavailable. Do not enable the tenant
|
||||
gate until real Slack qualification has passed; fixture coverage does not prove
|
||||
Marketplace eligibility or a workspace's approval policy.
|
||||
|
||||
Self-hosted instances do not need a Paperclip account, broker enrollment, manager
|
||||
credentials, or hosted-service calls. They continue using the own-app flow.
|
||||
|
||||
## Personal workspace authorization
|
||||
|
||||
Manager grants belong to a company **and one Paperclip user**, and bind a verified
|
||||
Slack user, workspace, and manager app. Another company member must authorize their
|
||||
own account, even for the same workspace. Access and rotating refresh tokens are
|
||||
user-scoped vault entries. Refreshes serialize across connections, persist the new
|
||||
refresh token, and require fresh authorization after an ambiguous exchange.
|
||||
|
||||
Revoking a grant requests provider revocation, removes its local credentials, invalidates pending manager
|
||||
authorization, and prevents future provisioning with it. The local activity records whether provider revocation succeeded. Provider errors never restore local access. It does not uninstall
|
||||
apps or disable already-installed bots. Removing a bot deletes endpoint-specific
|
||||
secrets and pending setup attempts; a reusable personal manager grant survives.
|
||||
Provider-side app removal remains explicit through the saved Slack management URL.
|
||||
|
||||
## Recovery
|
||||
|
||||
- A dispatched creation with an unknown result is **uncertain**. Never automatically
|
||||
create again. The user must inspect Slack and explicitly confirm that no app was
|
||||
created before a new request identity is accepted.
|
||||
- Denied or pending installation retains the app. Resume with the same grant or
|
||||
complete the child app's state-bound OAuth approval; the installer must match the
|
||||
grant's Slack identity before personal linking.
|
||||
- Returned bot credentials are vaulted before inventory or runtime configuration.
|
||||
Retry connecting reuses those credentials after network or inventory failures.
|
||||
Rejected or under-scoped bot credentials instead reinstall the saved app; they
|
||||
never trigger another app creation.
|
||||
- Manifest updates finish before managed installation. Changes requiring updated
|
||||
permissions therefore pass through installation again before activation.
|
||||
- Expired or revoked manager grants require reconnection. Losing managed
|
||||
availability pauses setup; it never changes the saved ownership method.
|
||||
- Slack rate limits are grouped by manager/workspace/method. Provider `Retry-After`
|
||||
is persisted across matching grants in the company, survives restarts, and blocks
|
||||
early retries. Other tenant instances must observe Slack's limits independently;
|
||||
no background loop retries app creation.
|
||||
|
||||
All OAuth attempts bind company, actor/session, endpoint, registration revision,
|
||||
and configured origins, expire after ten minutes, and are claimed once before
|
||||
exchange. HTTP diagnostics redact manager tokens, refresh tokens, codes, and sealed
|
||||
handoffs. Local activity records contain only IDs and safe outcome codes. This adds
|
||||
no first-party telemetry.
|
||||
|
||||
## Code and verification
|
||||
|
||||
Server provisioning lives in `server/src/services/connectors/slack/setup/`;
|
||||
`chat-slack-registration.ts` remains the compatibility facade. Shared completion
|
||||
owns credential binding, personal linking, welcome messages, and verification.
|
||||
Removal cleanup stays independently callable. Slack wizard components and stage
|
||||
selection live in `ui/src/pages/apps/chat/slack/`.
|
||||
|
||||
Production Storybook journeys: **Connections / Slack / Managed setup** and
|
||||
**Connections / Slack / Automatic setup** (the persisted own-app automatic path).
|
||||
Server integration cases cover personal grants, duplicate requests, uncertain
|
||||
creation, approval fallback, refresh, revocation, removal, saved-credential recovery,
|
||||
rate limits, and secret canaries. Browser cases are in
|
||||
`tests/e2e/chat-adapters-ui-providers.spec.ts`; companion broker tests cover manager
|
||||
user OAuth and the authenticated tenant handoff.
|
||||
|
||||
Before enabling a deployment, create two test agents in the authorized workspace.
|
||||
The second must reuse the personal grant without another configuration token or
|
||||
manager authorization. Verify avatars, identity linking, signed inbound events,
|
||||
and same-thread replies. Record real approval behavior separately from fixtures.
|
||||
|
||||
### Managed setup during broker outages
|
||||
|
||||
Cloud deployments configured for managed Slack keep the managed default when the broker profile is temporarily unavailable. New drafts remain on **Connect** and offer **Retry**. **Use your own app** remains available under **Advanced** on Choose agent before creation. Discovery loading or failure never selects the configuration-token page. Self-hosted instances without managed configuration still use their independent own-app flow; saved draft methods do not change automatically. Provisioning continues to require current broker availability.
|
||||
|
||||
### Local managed-bot qualification
|
||||
|
||||
Maintainers with an approved Slack manager app can exercise the real managed
|
||||
journey from an authenticated local test drive exposed through HTTPS (including
|
||||
TryCloudflare). This development transport talks directly to Slack and reuses
|
||||
the production manager-grant vault, state claims, provisioning, completion, and
|
||||
runtime. It does not exercise the Cloud broker's sealed handoff; qualify that
|
||||
separately on staging afterward.
|
||||
|
||||
Set `NODE_ENV=development`, `PAPERCLIP_SLACK_MANAGED_SETUP_ENABLED=true`, and
|
||||
`PAPERCLIP_SLACK_MANAGER_DEVELOPMENT_CONFIG` to an absolute path outside the
|
||||
checkout containing a mode-0600 JSON file with `appId`, `clientId`, and
|
||||
`clientSecret`. Use only an authorized qualification manager app. No credentials
|
||||
are shipped with Paperclip. Production ignores this development configuration,
|
||||
and Cloud runtime identity continues to select the Cloud broker.
|
||||
|
||||
Configure the local instance for authenticated public access, with its public
|
||||
URL and chat webhook URL pointing to the HTTPS tunnel. Add the exact public URL
|
||||
plus `/api/chat-slack/managed/oauth/callback` to the manager app's OAuth redirect
|
||||
URLs, preserving any existing redirects. Start a **new** Slack setup: saved
|
||||
own-app drafts retain their selected method. Authorize the manager app normally;
|
||||
do not copy user grants from another instance. The second bot should reuse the
|
||||
same local user's vaulted grant. A changed tunnel requires updating public
|
||||
origins and the manager redirect before starting fresh authorization.
|
||||
|
||||
Remove the temporary redirect when retiring the test drive and securely remove
|
||||
its development credential file. Removing a bot does not revoke a manager grant
|
||||
used by other bots.
|
||||
|
||||
### Test messages from Settings
|
||||
|
||||
**Send me a test message** sends a DM to the signed-in connection manager,
|
||||
using their linked identity in this workspace. If this bot is not linked yet,
|
||||
Paperclip can reuse that user's verified manager grant or another bot's linked
|
||||
identity in the same company and workspace. Otherwise, the user selects their
|
||||
Slack account and receives a private connection invitation in Slack. They must
|
||||
open that invitation, sign in, and confirm before Paperclip links the account and
|
||||
sends the test DM. The account picker loads further workspace pages on request. Existing revoked or conflicting identity links are not overwritten.
|
||||
Direct messages must be enabled, and the user must be an active company operator.
|
||||
|
||||
The action records a durable request before sending. Repeating the same request
|
||||
returns its saved result; an uncertain delivery is never automatically resent.
|
||||
**Open Slack configuration** opens the saved app's OAuth & Permissions page.
|
||||
Legacy connections without an app ID open Slack's app list instead.
|
||||
|
||||
An explicit managed reconnect reinstalls the saved app and verifies the replacement
|
||||
bot credentials. Its request ID is saved before dispatch, so a duplicate completed
|
||||
request does not reinstall again. Credential-configuration recovery still resumes
|
||||
the saved token after a downstream failure. No reconnect creates another Slack app.
|
||||
@@ -2,10 +2,90 @@
|
||||
|
||||
Updated 2026-10-12.
|
||||
|
||||
## Production readiness: owned startup correction
|
||||
|
||||
The human requests production readiness; preparation and normal sequential
|
||||
merges remain authorized. Production deployment and provisioning-default
|
||||
promotion remain separate actions. Nothing in this checkpoint is merged or
|
||||
deployed, and the stack is not production-ready.
|
||||
|
||||
Runtime prerequisite behavior is `ce23716503a2c9158b5a7ab74ab61571ff4ee7a3`
|
||||
on `codex/runner-native-platform-prerequisites`, in
|
||||
[#16028](https://github.com/paperclipai/paperclip/pull/16028). Final qualification
|
||||
reproduced close returning before credential preparation drained, followed by
|
||||
registration and launch. Both direct close and real driver cancellation failed
|
||||
on literal `5a9ce11`. The initial shared repair from Claude commit `463ebb` is
|
||||
reused, with independently identified duplicate-admission, shutdown-readiness and
|
||||
resume ordering corrections. All five source findings are closed in matching
|
||||
source review. Absolute cleanup deadlines, primary cancellation errors,
|
||||
unauthenticated-process protection and existing agents' runner choices remain.
|
||||
|
||||
The existing suites pass 32 credential-free controls across three files; both
|
||||
canonical runner TypeScript no-emit checks pass. No new suite, local Rust/Docker
|
||||
build, provider call or credential-content read was used. Actual daemon
|
||||
readiness/recovery controls remain **hosted-CI required**: local worktrees lack
|
||||
the existing fake-Codex/fake-harness executables. The failed local native attempt
|
||||
never reached the readiness assertion and is not qualification. Receipts are
|
||||
`/private/tmp/pr2-463ebb-owned-startup-review-closure-receipt.json` and
|
||||
`/private/tmp/ci-final-owned-startup-five-gap-closure-review.json`.
|
||||
|
||||
Normal automatic merge is disabled and the prerequisite is draft while this
|
||||
correction receives fresh full CI, package qualification and 5/5 review.
|
||||
Earlier green checks at `5a9ce11` remain historical. Actual code-owner approval
|
||||
is still required; no protection is bypassed. The updated runs/adapters map pair
|
||||
records source-only scope, bringing the prerequisite review to 63 files after the required Storybook
|
||||
catalog review below.
|
||||
|
||||
Codex remains `1a0ca66242121e39d88455aa2b52a07acc4a07d9` in
|
||||
[#15683](https://github.com/paperclipai/paperclip/pull/15683), based on the previous
|
||||
prerequisite. The Claude owner retains its branches, cached guided-setup fix,
|
||||
verification and separate $50 allowance. Coordinate reconciliation of the common
|
||||
repair before rewriting those branches; don't duplicate it in a leaf slice.
|
||||
|
||||
The idle [local preview](http://127.0.0.1:3125/COD/agents/all) still serves literal
|
||||
behavior `fe1ecf1c34`, recovery ready and zero active runs. Its four saved
|
||||
legacy/native configurations and two historical native records remain preserved;
|
||||
it has not been refreshed to this new runtime. Final local/managed acceptance
|
||||
must use the tested source, independent file checks and follow-up continuity.
|
||||
|
||||
The prepared private execution checklist reuses existing evals, installation,
|
||||
Cloud composition and campaign paths. Required holds are final source/image/
|
||||
migrator, the actual verified-master private distribution, pinned QA onboarding,
|
||||
warm/cold signup, production baseline and previous-image compatibility. Production
|
||||
Fleet Admin sign-in is pending. Current paid holds remain $248.179254104 of $250;
|
||||
unknown/cleanup holds stay reserved, and the unstarted managed envelope is $2.50.
|
||||
No new image, paid resource, rollout or default promotion was dispatched.
|
||||
|
||||
Master reconciliation uses `4b958ecbd8f33a7782fe6f29ec825f80f636b833`.
|
||||
The new contribution rules are satisfied: 157 added comment annotations were
|
||||
removed from 35 files, with identical executable JavaScript/TypeScript emission,
|
||||
parsed workflow values, 77 action pins and shell commands. No code file crosses
|
||||
from below 1,000 lines to above that limit. Final focused checks pass 248 tests
|
||||
(107 workflow/package, 54 runner, 42 server, 45 E2E support). Both runner no-emit
|
||||
checks and the canonical server/E2E no-emit checks pass with isolated matching
|
||||
workspace dependencies. Only JavaScript/TypeScript preparation ran locally;
|
||||
root preview links and data stayed unchanged. The earlier linked-source failures
|
||||
remain recorded and are superseded only by the new canonical check receipt.
|
||||
|
||||
The existing Storybook catalog has 21 source-review fingerprint updates because
|
||||
its declared runs/adapters map input changed. All affected stories, subjects,
|
||||
wrappers and fixtures remain byte-identical to master. Review classifications
|
||||
stay unchanged. The catalog check passes 223 files/2,210 stories; this is
|
||||
structural/source evidence, not visual or live proof. Source receipts are
|
||||
`/private/tmp/pr2-9f59-comments-cleanup-receipt.json`,
|
||||
`/private/tmp/pr2-storybook-owned-final-receipt.json` and
|
||||
`/private/tmp/pr2-frozen-canonical-noemit-receipt.json`.
|
||||
|
||||
Next action: publish this reviewed prerequisite correction, require fresh hosted
|
||||
gates, reconcile Codex and Claude with its actual source, then resume the existing
|
||||
remote package/immutable preview and dedicated staging qualification. Merge ready
|
||||
slices normally in the agreed order; actual production readiness requires the
|
||||
private distribution and rollback/forward-recovery evidence afterward.
|
||||
|
||||
## CI fixture, network policy and map synchronization checkpoint
|
||||
|
||||
Runtime prerequisite source is `5a9ce11c49db31440863d633426ebe30bcc850ca`
|
||||
with 60 changed files. The hosted static check on literal Codex source `eee0534`
|
||||
with 59 changed files. The hosted static check on literal Codex source `eee0534`
|
||||
passed 133 cases and failed one: its isolated assembly fixture omitted the new
|
||||
target JSON, so the helper failed before its existing Cursor-identity assertion.
|
||||
The fixture now copies that exact inert JSON; all negative assertions remain.
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
# Chat channel callback and delivery boundaries, batch four
|
||||
|
||||
Base: 4b766b872993045a2e787404f0475503683e5ded. Batch three (#15995, #15996, #15999, #16001, #16002)
|
||||
is merged. The service starts at 26,461 lines / 1,046,711 bytes.
|
||||
This batch may create up to ten stacked PRs. Merging the new batch is separate work.
|
||||
|
||||
| Order / boundary | Destination | Approx. moved / net lines | Contract and state | Risks and existing verification |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 1. Slack callback verification | `slack/callback-verification.ts` | 250 / 230 | Pure signature/workspace/URL inspection plus signed URL verification; explicit vault lookup, URL getter, scheduling and registration callbacks | Preserve signing-secret-only setup, original request, TLS/proxy evidence and workspace checks. Signed verification and callback health cases before/after. |
|
||||
| 2. Discord Gateway events | `discord/gateway-events.ts` | 200 / 185 | Runtime fence and lease-owner predicates, endpoint lookup and lifecycle callback; original per-runtime event queue and sequence | Preserve stale-owner fencing, event order, transient guild recovery and terminal revocation. Gateway lifecycle cases before/after. |
|
||||
| 3. Discord root threads | `discord/root-threads.ts` | 500 / 460 | Credential lease, current principal authority, runtime, message admission and conversation drain callbacks | Preserve uncertain thread creation, restart reconciliation, no duplicate starter and pause/credential serialization. Root-thread public-service cases. |
|
||||
| 4. Receipt reactions | `receipt-reactions.ts` and Slack error classifier beside Slack messages | 545 / 500 | Durable reaction claim/transport/settlement with runtime and credential fences; no scheduling owner moves | Preserve retries, legacy GitHub receipts, terminal-removal supersession and same-attempt settlement. Receipt recovery cases before/after. |
|
||||
| 5. Inbound reactions | `reactions.ts` | 800 / 740 | Conversation lookup/locks, reaction admission and deferred replay together; current principal/runtime callbacks | Preserve exact event dedupe, completed generation links, bounded replay age and no task wakeup. Reaction race/restart/authorization cases. |
|
||||
| 6. Inbound attachments | `inbound-attachments.ts` | 600 / 550 | Existing storage and issue service, retained source authorization and live options; return same attachment results | Preserve size limits, source authority around I/O, omissions and text delivery. Provider attachment/restart/stale-authority cases. |
|
||||
| 7. Outbound attachments | `outbound-attachments.ts`; provider Markdown wrappers beside provider messages | 245 / 220 | Metadata validation and bounded integrity-checked hydration; existing storage object. Keep public hydration export compatible | Preserve stream cleanup, deadlines, byte/digest checks and retry classification. Hydration and publication attachment cases. |
|
||||
| 8. Provider effects | `provider-effects.ts` | 950 / 850 | Durable reply claim, authorization, transport and settlement; endpoint/runtime/credential/principal callbacks | Preserve uncertain-delivery quarantine, credential fence and transaction ordering. Native callback, task-control and reply recovery cases. Put Telegram-only authority under `telegram/` if its contract is narrow. |
|
||||
| 9. Conversation and activity reads | `activity.ts` | 540 / 520 | Endpoint lookup and read-only Teams conflict projection; return existing API models | Preserve company/endpoint joins, mixed-source cursor ties, replay eligibility and read-only GETs. Activity, file receipt projection and conversation-state cases. |
|
||||
| 10. Setup probes | `setup-probes.ts` | 280 / 250 | Credential lease, current identity/principal authority, provider fetch, endpoint projection and live activation hook | Preserve real-message requirement, generation and linked-user activation. Setup-test, reconnect and authorization cases. |
|
||||
|
||||
Each slice must earn its place after reading its complete path and callers.
|
||||
Reassess before implementation; stop short of ten if the remaining contracts are
|
||||
not cohesive. Keep provider-specific code in channel folders and common durable
|
||||
workflows in the shared chat-channels directory.
|
||||
|
||||
Receiver lease ownership stays shared: the existing Discord-named owner is also
|
||||
used by Photon. Shared lifecycle/application admission, queue-drain ownership,
|
||||
worker scheduling, and the largest action/message handlers remain in the service.
|
||||
Do not hide their dependencies in a mutable context object. Preserve live option
|
||||
hooks, runtime-map identity, partial-failure cleanup, and initialization order.
|
||||
|
||||
Run relevant existing public-service tests before and after each mechanical move.
|
||||
Compare moved declarations and remaining statements with normalized syntax, then
|
||||
run server types, module boundaries and whitespace checks. Finish local verification and a fresh
|
||||
5/5 review before opening the next draft PR. GitHub runner-selection jobs remained
|
||||
queued without steps for over 25 minutes in this batch. To continue the authorized
|
||||
stack, publish drafts in review order while tracking CI as pending; CI remains a
|
||||
merge gate. Run cumulative repository gates at handoff.
|
||||
Retain failures and skipped/unrun coverage even when an isolated retry passes.
|
||||
Use fixture-backed assembled service/database tests; do not claim live provider
|
||||
execution. Remap LOC/bytes and the remaining orchestration after the batch.
|
||||
|
||||
## Slice 1: Slack callback verification
|
||||
|
||||
- Service: 26,461 lines / 1,046,711 bytes → 26,201 lines / 1,035,260 bytes.
|
||||
- Destination: 299 lines / 13,038 bytes. Eight declarations move without body
|
||||
changes. Three existing setup types move unchanged into `runtime-types.ts`.
|
||||
- The verification factory initializes after Slack registration. It receives the
|
||||
same registration object, credential resolver, live URL getter and scheduler.
|
||||
Signature/workspace/URL helpers remain callable by shared webhook orchestration.
|
||||
- Nine public-service cases passed before and after on the updated master base.
|
||||
Server typecheck, module boundaries, whitespace and normalized syntax parity
|
||||
passed. Coverage includes signed URL verification, account notification,
|
||||
workspace rejection, callback surfaces, TLS termination and Cloud host evidence.
|
||||
- Next: Discord Gateway event sequencing and health transitions. Keep shared
|
||||
receiver lease ownership and runtime-map lifetime in the service.
|
||||
|
||||
## Batch outcome
|
||||
|
||||
All ten candidates produced cohesive boundaries. Shared receiver ownership, queue
|
||||
management and worker scheduling remain in the service. The only intentional
|
||||
control-wiring change is deferred access to `stageProviderEffect` from identity
|
||||
link construction; typecheck and integration tests caught the initial early read.
|
||||
|
||||
| Slice | Service lines after | Primary module lines | Before/after cases |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| 1. Slack signature, workspace and callback url verification | 26,233 | 300 | 9 |
|
||||
| 2. Discord gateway event sequencing and endpoint health transitions | 26,029 | 253 | 7 |
|
||||
| 3. Discord root-thread admission, provider creation and durable recovery | 25,527 | 570 | 4 |
|
||||
| 4. Durable receipt reaction claims, transport, retries and settlement | 24,975 | 586 | 7 |
|
||||
| 5. Inbound reaction conversation lookup, admission and deferred replay | 24,160 | 891 | 21 |
|
||||
| 6. Inbound attachment authorization, bounded ingestion, registration and omissions | 23,565 | 674 | 39 |
|
||||
| 7. Outbound attachment metadata validation and bounded integrity-checked hydration | 23,298 | 294 | 22 |
|
||||
| 8. Durable provider reply staging, authorization, transport and recovery | 22,331 | 957 | 24 |
|
||||
| 9. Conversation summaries and paginated activity projections | 21,741 | 616 | 16 |
|
||||
| 10. Setup message detection and credential-fenced round-trip qualification | 21,460 | 353 | 7 |
|
||||
|
||||
After rebasing on master `8e7b5db822`, the service shrank from
|
||||
**26,493 lines / 1,048,769 bytes** to **21,460 lines /
|
||||
862,284 bytes** (5,033 fewer lines, about 19%). Normalized syntax comparison
|
||||
checks all 69 moved declarations/types and every remaining original statement.
|
||||
Server types, targeted tests, module boundaries and whitespace checks are the
|
||||
per-slice gates. Cumulative repository gates and remote reviews are recorded in
|
||||
the PR descriptions as they complete.
|
||||
|
||||
Orchestration measurement: the service function has 150 nested functions
|
||||
(19,399 lines) and 67 state/wiring statements (459 lines), versus 191 functions
|
||||
(23,829 lines) and 54 statements (425 lines) at the start. Imports take 400 lines.
|
||||
The remaining large paths are `processMessage` (2,166), `handleAction` (1,074),
|
||||
`configureWithCredentialLease` (868), `processSelectedPublication` (729),
|
||||
`handleModalSubmit` (708), `handleWebhook` (685), and lifecycle processing (657).
|
||||
|
||||
A future pass should inspect publication admission/transport/settlement as one
|
||||
workflow, then inbound delivery authorization and drain ownership. Those cuts
|
||||
need explicit ownership contracts; avoid separating individual checks from their
|
||||
transactions merely to reduce the remaining line count.
|
||||
|
||||
The rebase preserves the merged managed Slack setup flow, including its shared
|
||||
`isProvisionedSlackSetup` predicate in the extracted callback verification
|
||||
module. A fresh syntax audit compares all moved declarations and retained
|
||||
statement ordering with this updated master.
|
||||
@@ -756,3 +756,84 @@ Report attachment: `e6206748-a785-4c02-9bbf-f4db8a3b22f9`, work product
|
||||
Local receipts: `/private/tmp/claude-responsive-preview-evidence.json`,
|
||||
`/private/tmp/claude-responsive-evidence-upload.json`, and
|
||||
`/private/tmp/claude-responsive-evidence-independent-check.json`.
|
||||
|
||||
## Canonical composition and current contribution rules — 2026-10-12
|
||||
|
||||
The source owner committed the stronger shared startup repair as
|
||||
`ce23716503a2c9158b5a7ab74ab61571ff4ee7a3`. Local additive merges preserve
|
||||
that parent and this chat's Claude effort/resume changes. Eight controls that do
|
||||
not need native executables and runner noemit pass in the combined protocol.
|
||||
The ninth selected readiness-cycle case timed out while waiting for readiness;
|
||||
read-only resolution confirms both the default runner artifact and fake provider
|
||||
fixture are absent, with no private runner override. That native case remains a
|
||||
hosted CI gate, not a pass. No local native build was performed. Logs:
|
||||
`/private/tmp/claude-canonical-startup-composition-controls.log`,
|
||||
`/private/tmp/claude-canonical-startup-no-process-controls.log`, and
|
||||
`/private/tmp/claude-canonical-startup-composition-types.log`.
|
||||
|
||||
The source owner then reconciled current master in `9f59ff36cf`, adding the
|
||||
Storybook catalog and contribution rules against new code comments and code files
|
||||
crossing 1000 lines. The guided-creation fixture is extracted to
|
||||
`ui/src/pages/NewAgent.test-support.tsx`; all 65 cases remain and pass, with UI
|
||||
noemit and token gates passing. Fixture component rendering stays inside its
|
||||
mock-owning module, including the saved-confirmation reload case. The active test
|
||||
file no longer crosses the threshold. Our added explanatory comments are removed;
|
||||
the source owner handles the common transport's comment cleanup. Semantic code
|
||||
and internal deadlines stay unchanged.
|
||||
|
||||
Removing this chat's added sandbox comments changes its raw source attestation to
|
||||
`144d62b3415cfedf272b5b052e14af1e41fe50a644af84247fc9c20c7ef7edde`.
|
||||
Immutable Pi profiles 23 and 24 remain retained. New Pi profile 25 binds only that
|
||||
source revision plus its profile version, digest
|
||||
`sha256:dd67387487dadb08da76534c057dc0aa059f7698d1779cbd5fc452813828167a`.
|
||||
SDK/executable closures, platforms, model/effort contract and other providers'
|
||||
qualification boundaries do not change. Copilot remains pending profile 18.
|
||||
Generated TS/Rust descriptors, Rust current/prior fixtures, Docker context and
|
||||
current documentation are synchronized. All 119 profile/factory/admission/effort
|
||||
controls pass with one existing historical skip, including corrupt-byte rejection
|
||||
before spawn/bridge; runner noemit and generation checks pass. The first restricted
|
||||
attempt stopped at loopback lease admission and is retained, not counted as byte
|
||||
validation. Native/Rust execution and final package qualification remain cloud gates.
|
||||
|
||||
The maintain-storybook source review found concrete fixture incompatibilities:
|
||||
the general-settings endpoint and scoped runner-discovery defaults were absent.
|
||||
The shared preview now returns that settings contract and explicitly simulated
|
||||
Legacy discovery for Claude/Codex, exposes the loaded native adapter, and includes
|
||||
Opus 5.5 while retaining explicit older-model examples. These fixture defaults are
|
||||
not rollout qualification. Catalog fingerprints/classification and affected story
|
||||
rendering must be reconciled after the final parent is frozen; source-only review
|
||||
is not an executed Storybook or provider pass. Published draft heads remain fixed
|
||||
while the source owner finishes the canonical contribution/catalog cleanup.
|
||||
|
||||
## Published canonical parent and later master blocker — 2026-10-12
|
||||
|
||||
The canonical prerequisite is published as
|
||||
`64c074fb8da1b31f8d6042c4fef48dc4164e759b`; all 54 exact-head checks pass
|
||||
with four conditional skips, fresh 5/5 and all review threads resolved. Its hosted
|
||||
native transport suite passes 246/246, closing the fake-native-fixture limitation
|
||||
for that literal source. Its four provider-free producers also pass, while clean
|
||||
consumers remain in progress. Those checks are not relabeled as this chat's Claude
|
||||
input/profile composition. Root retains sequential normal merge/staging ownership;
|
||||
code-owner approval is still required and production is not promoted.
|
||||
|
||||
Local composition preserves the source repair with no duplicate lifecycle delta:
|
||||
the transport diff against 64 contains only Claude effort additions. The old Codex
|
||||
base branch `1a0ca66242121e39d88455aa2b52a07acc4a07d9` still lacks the current-
|
||||
master parent, so publishing the current protocol would expose 468 upstream files.
|
||||
The root-owned dependency must be reconciled before the review slices can again
|
||||
be verified under the 100-file limit. Cross-chat dispatch still needs direct
|
||||
human authorization in this chat; a concrete dependency-coordination question is
|
||||
pending. No frozen root branch was mutated.
|
||||
|
||||
Master then advanced to task-workspace source
|
||||
`f329b24ee12496e710ad17377ec7716e0eee22ef`, making the canonical prerequisite
|
||||
conflicted. The source owner is reconciling its three conflicts and requested that
|
||||
leaf CI/package campaigns wait for the next published parent. All local Claude
|
||||
changes and earlier package/task evidence are preserved, and no new paid turn,
|
||||
local Rust/Docker build or leaf package campaign is dispatched during that hold.
|
||||
|
||||
The current Storybook catalog is reconciled with 224 story files and all 2,223
|
||||
pre-move IDs preserved. Fifty-three source-affected entries retain their prior
|
||||
review classifications. Six bounded fixture plays and reset pass; exact cases,
|
||||
source scope, simulated boundaries and omitted coverage are recorded in
|
||||
`maps/storybook-map/REVIEW.md`. This does not qualify the final installed runtime.
|
||||
@@ -0,0 +1,539 @@
|
||||
{
|
||||
"date": "2026-10-12",
|
||||
"policy": "Each mutation changes one actual production guard or outcome. Only the selected case runs. Every changed source is restored to its exact SHA-256 in finally. Complete unmutated owner and sibling controls run separately.",
|
||||
"controls": [
|
||||
{
|
||||
"id": "api-rejection-eviction",
|
||||
"codeHead": "dfdc39b3a71c74c40e267010c6d8428af523bb65",
|
||||
"owner": "ui/src/api/client.ts",
|
||||
"sourceSha256": "1e1b3f03e3269a5943f55d294f1c7aa9f5c3eca569858d9fdc7c5d976de88bba",
|
||||
"case": "tenant-session recovery preserves ApiError handling for an unrelated 401",
|
||||
"expected": "one intended assertion failure",
|
||||
"actualFailedCases": 1,
|
||||
"intendedAssertionFailed": true,
|
||||
"sourceRestored": true,
|
||||
"failure": "Error: promise rejected \"ApiError: unauthorized { \u2026(2) }\" instead of resolving\n at Assertion.__VITEST_RESOLVES__ (file://<workspace>/node_modules/.pnpm/vitest@5.0.3_@opentelemetry+api@1.9.1_@types+node@24.13.3_jsdom@30.0.1_@noble+hashes@2.4.0__v_wr5mxfuu6kgge6st623q4v5egm/node_modules/vitest/dist/chunks/index.C4tKZ0yW.js:2434:33)\n at Assertion.propertyGetter (file://<workspace>/node_modules/.pnpm/chai@6.3.0/node_modules/chai/index.js:1607:27)\n at Reflect.get (<anonymous>)\n at Object.proxyGetter [as get] (file:///Users/dotta/.codex/worktrees/tes"
|
||||
},
|
||||
{
|
||||
"id": "api-all-abort-eviction",
|
||||
"codeHead": "dfdc39b3a71c74c40e267010c6d8428af523bb65",
|
||||
"owner": "ui/src/api/client.ts",
|
||||
"sourceSha256": "1e1b3f03e3269a5943f55d294f1c7aa9f5c3eca569858d9fdc7c5d976de88bba",
|
||||
"case": "per-caller abort semantics aborts the shared fetch after all callers cancel and permits a fresh request",
|
||||
"expected": "one intended assertion failure",
|
||||
"actualFailedCases": 1,
|
||||
"intendedAssertionFailed": true,
|
||||
"sourceRestored": true,
|
||||
"failure": "AssertionError: expected \"vi.fn()\" to be called 2 times, but got 1 times\n at <workspace>/ui/src/api/client.test.ts:210:23\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at file://<workspace>/node_modules/.pnpm/vitest@5.0.3_@opentelemetry+api@1.9.1_@types+node@24.13.3_jsdom@30.0.1_@noble+hashes@2.4.0__v_wr5mxfuu6kgge6st623q4v5egm/node_modules/vitest/dist/chunks/run.BTlFXlnv.js:2783:20"
|
||||
},
|
||||
{
|
||||
"id": "ticker-one-interval",
|
||||
"codeHead": "dfdc39b3a71c74c40e267010c6d8428af523bb65",
|
||||
"owner": "ui/src/hooks/useSecondTick.ts",
|
||||
"sourceSha256": "fd51875adf5f30295d847df58af19f75dcbb59c810b4f02919427cbc6ea4d3b3",
|
||||
"case": "useSecondTick shares a single timer across multiple active subscribers and stops when idle",
|
||||
"expected": "one intended assertion failure",
|
||||
"actualFailedCases": 1,
|
||||
"intendedAssertionFailed": true,
|
||||
"sourceRestored": true,
|
||||
"failure": "AssertionError: expected 2 to be 1 // Object.is equality\n at <workspace>/ui/src/hooks/useSecondTick.test.tsx:26:32\n at file://<workspace>/node_modules/.pnpm/vitest@5.0.3_@opentelemetry+api@1.9.1_@types+node@24.13.3_jsdom@30.0.1_@noble+hashes@2.4.0__v_wr5mxfuu6kgge6st623q4v5egm/node_modules/vitest/dist/chunks/run.BTlFXlnv.js:1628:35\n at file://<workspace>/node_modules/.pnpm/vitest@5.0.3_@opentelemetry+api@1.9.1_@types+node@24.13.3_jsdom@30.0.1_@noble+hashes@2.4.0__v_wr5mxfuu6kgge6st623q4v5egm/node_modules"
|
||||
},
|
||||
{
|
||||
"id": "ticker-delivery",
|
||||
"codeHead": "dfdc39b3a71c74c40e267010c6d8428af523bb65",
|
||||
"owner": "ui/src/hooks/useSecondTick.ts",
|
||||
"sourceSha256": "fd51875adf5f30295d847df58af19f75dcbb59c810b4f02919427cbc6ea4d3b3",
|
||||
"case": "useSecondTick shares a single timer across multiple active subscribers and stops when idle",
|
||||
"expected": "one intended assertion failure",
|
||||
"actualFailedCases": 1,
|
||||
"intendedAssertionFailed": true,
|
||||
"sourceRestored": true,
|
||||
"failure": "AssertionError: expected '0' to be '1' // Object.is equality\n at <workspace>/ui/src/hooks/useSecondTick.test.tsx:28:37\n at file://<workspace>/node_modules/.pnpm/vitest@5.0.3_@opentelemetry+api@1.9.1_@types+node@24.13.3_jsdom@30.0.1_@noble+hashes@2.4.0__v_wr5mxfuu6kgge6st623q4v5egm/node_modules/vitest/dist/chunks/run.BTlFXlnv.js:1628:35\n at file://<workspace>/node_modules/.pnpm/vitest@5.0.3_@opentelemetry+api@1.9.1_@types+node@24.13.3_jsdom@30.0.1_@noble+hashes@2.4.0__v_wr5mxfuu6kgge6st623q4v5egm/node_mod"
|
||||
},
|
||||
{
|
||||
"id": "ticker-idle-cleanup",
|
||||
"codeHead": "dfdc39b3a71c74c40e267010c6d8428af523bb65",
|
||||
"owner": "ui/src/hooks/useSecondTick.ts",
|
||||
"sourceSha256": "fd51875adf5f30295d847df58af19f75dcbb59c810b4f02919427cbc6ea4d3b3",
|
||||
"case": "useSecondTick shares a single timer across multiple active subscribers and stops when idle",
|
||||
"expected": "one intended assertion failure",
|
||||
"actualFailedCases": 1,
|
||||
"intendedAssertionFailed": true,
|
||||
"sourceRestored": true,
|
||||
"failure": "AssertionError: expected 1 to be +0 // Object.is equality\n at <workspace>/ui/src/hooks/useSecondTick.test.tsx:37:32\n at file://<workspace>/node_modules/.pnpm/vitest@5.0.3_@opentelemetry+api@1.9.1_@types+node@24.13.3_jsdom@30.0.1_@noble+hashes@2.4.0__v_wr5mxfuu6kgge6st623q4v5egm/node_modules/vitest/dist/chunks/run.BTlFXlnv.js:1628:35\n at file://<workspace>/node_modules/.pnpm/vitest@5.0.3_@opentelemetry+api@1.9.1_@types+node@24.13.3_jsdom@30.0.1_@noble+hashes@2.4.0__v_wr5mxfuu6kgge6st623q4v5egm/node_module"
|
||||
},
|
||||
{
|
||||
"id": "ticker-inactive-peer",
|
||||
"codeHead": "dfdc39b3a71c74c40e267010c6d8428af523bb65",
|
||||
"owner": "ui/src/hooks/useSecondTick.ts",
|
||||
"sourceSha256": "fd51875adf5f30295d847df58af19f75dcbb59c810b4f02919427cbc6ea4d3b3",
|
||||
"case": "useSecondTick does not subscribe or start a timer when inactive",
|
||||
"expected": "one intended assertion failure",
|
||||
"actualFailedCases": 1,
|
||||
"intendedAssertionFailed": true,
|
||||
"sourceRestored": true,
|
||||
"failure": "AssertionError: expected '1' to be '0' // Object.is equality\n at <workspace>/ui/src/hooks/useSecondTick.test.tsx:48:37\n at file://<workspace>/node_modules/.pnpm/vitest@5.0.3_@opentelemetry+api@1.9.1_@types+node@24.13.3_jsdom@30.0.1_@noble+hashes@2.4.0__v_wr5mxfuu6kgge6st623q4v5egm/node_modules/vitest/dist/chunks/run.BTlFXlnv.js:1628:35\n at file://<workspace>/node_modules/.pnpm/vitest@5.0.3_@opentelemetry+api@1.9.1_@types+node@24.13.3_jsdom@30.0.1_@noble+hashes@2.4.0__v_wr5mxfuu6kgge6st623q4v5egm/node_mod"
|
||||
},
|
||||
{
|
||||
"id": "ui-bound-caption",
|
||||
"codeHead": "dfdc39b3a71c74c40e267010c6d8428af523bb65",
|
||||
"owner": "ui/src/components/AgentConfigForm.tsx",
|
||||
"sourceSha256": "bf999102f39bfc762747ce5d598e1e6ec2f926ac5fb28de55fa1baf5493f28b1",
|
||||
"case": "AgentConfigForm environment selector reports the account-binding claim upward exactly once when the session authenticates",
|
||||
"expected": "one intended assertion failure",
|
||||
"actualFailedCases": 1,
|
||||
"intendedAssertionFailed": true,
|
||||
"sourceRestored": true,
|
||||
"failure": "AssertionError: expected 'Sign in to OpenAISign inAuthenticated\u2026' to contain 'Agent bound to the signed-in account'\n at <workspace>/ui/src/components/AgentConfigForm.render.test.tsx:1773:35\n at file://<workspace>/node_modules/.pnpm/vitest@5.0.3_@opentelemetry+api@1.9.1_@types+node@24.13.3_jsdom@30.0.1_@noble+hashes@2.4.0__v_wr5mxfuu6kgge6st623q4v5egm/node_modules/vitest/dist/chunks/run.BTlFXlnv.js:2783:20"
|
||||
},
|
||||
{
|
||||
"id": "ui-onboarding-saved-login",
|
||||
"codeHead": "dfdc39b3a71c74c40e267010c6d8428af523bb65",
|
||||
"owner": "ui/src/components/AgentConfigForm.tsx",
|
||||
"sourceSha256": "bf999102f39bfc762747ce5d598e1e6ec2f926ac5fb28de55fa1baf5493f28b1",
|
||||
"case": "OnboardingWizard restore-gate (stale localStorage across accounts) the adapter step login panel (cheap auth signal, no adapter test) renders no 'Use saved login' control",
|
||||
"expected": "one intended assertion failure",
|
||||
"actualFailedCases": 1,
|
||||
"intendedAssertionFailed": true,
|
||||
"sourceRestored": true,
|
||||
"failure": "AssertionError: expected 'Step 2 of 3Ada Connect a modelPapercl\u2026' not to contain 'Use saved login'\n at <workspace>/ui/src/components/OnboardingWizard.test.tsx:2931:45\n at file://<workspace>/node_modules/.pnpm/vitest@5.0.3_@opentelemetry+api@1.9.1_@types+node@24.13.3_jsdom@30.0.1_@noble+hashes@2.4.0__v_wr5mxfuu6kgge6st623q4v5egm/node_modules/vitest/dist/chunks/run.BTlFXlnv.js:2783:20"
|
||||
},
|
||||
{
|
||||
"id": "ui-agent-footer",
|
||||
"codeHead": "dfdc39b3a71c74c40e267010c6d8428af523bb65",
|
||||
"owner": "ui/src/components/IssueThreadInteractionCard.tsx",
|
||||
"sourceSha256": "fb9beb27481258354942003e6a5c319a23257ef07f4ce811eb9ccbd605e0a585",
|
||||
"case": "IssueThreadInteractionCard tool-action card marks agent resolution with an audit chip in the resolved footer",
|
||||
"expected": "one intended assertion failure",
|
||||
"actualFailedCases": 1,
|
||||
"intendedAssertionFailed": true,
|
||||
"sourceRestored": true,
|
||||
"failure": "AssertionError: the given combination of arguments (undefined and string) is invalid for this assertion. You can use an array, a map, an object, a set, a string, or a weakset instead of a string\n at Proxy.<anonymous> (file://<workspace>/node_modules/.pnpm/vitest@5.0.3_@opentelemetry+api@1.9.1_@types+node@24.13.3_jsdom@30.0.1_@noble+hashes@2.4.0__v_wr5mxfuu6kgge6st623q4v5egm/node_modules/vitest/dist/chunks/index.C4tKZ0yW.js:2077:15)\n at Proxy.<anonymous> (file://<workspace>/node_modules/.pnpm/vitest@5.0.3_@opentelemetry+api@1.9.1_@types+node@24.13.3_j"
|
||||
},
|
||||
{
|
||||
"id": "ui-markdown-anchor",
|
||||
"codeHead": "dfdc39b3a71c74c40e267010c6d8428af523bb65",
|
||||
"owner": "ui/src/components/MarkdownBody.tsx",
|
||||
"sourceSha256": "003b7da23b5ebd0e8cd4de91e0d1dafe697f6602d4d99551b04d0abad699d193",
|
||||
"case": "MarkdownBody opens external links in a new tab with safe rel attributes",
|
||||
"expected": "one intended assertion failure",
|
||||
"actualFailedCases": 1,
|
||||
"intendedAssertionFailed": true,
|
||||
"sourceRestored": true,
|
||||
"failure": "AssertionError: expected '<div class=\"paperclip-markdown prose \u2026' to contain '<a href=\"https://example.com/docs\"'\n at <workspace>/ui/src/components/MarkdownBody.test.tsx:587:18\n at file://<workspace>/node_modules/.pnpm/vitest@5.0.3_@opentelemetry+api@1.9.1_@types+node@24.13.3_jsdom@30.0.1_@noble+hashes@2.4.0__v_wr5mxfuu6kgge6st623q4v5egm/node_modules/vitest/dist/chunks/run.BTlFXlnv.js:1628:35\n at file://<workspace>/node_modules/.pnpm/vitest@5.0.3_@opentelemetry+api@1.9.1_@types+node@24.13.3_jsdom@30.0.1_"
|
||||
},
|
||||
{
|
||||
"id": "server-cancelled-reopen",
|
||||
"codeHead": "dfdc39b3a71c74c40e267010c6d8428af523bb65",
|
||||
"owner": "server/src/routes/issues.ts",
|
||||
"sourceSha256": "641b3d9cb5e744671527f16129dd78518376dca31616b845ec6b8da99d636d8c",
|
||||
"case": "issue comment reopen routes rejects bare agent reopen requests on cancelled issues before writing a comment",
|
||||
"expected": "one intended assertion failure",
|
||||
"actualFailedCases": 1,
|
||||
"intendedAssertionFailed": true,
|
||||
"sourceRestored": true,
|
||||
"failure": "AssertionError: expected 201 to be 409 // Object.is equality\n at <workspace>/server/src/__tests__/issue-comment-reopen-routes.test.ts:1040:24\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at file://<workspace>/node_modules/.pnpm/vitest@5.0.3_@opentelemetry+api@1.9.1_@types+node@24.13.3_jsdom@30.0.1_@noble+hashes@2.4.0__v_wr5mxfuu6kgge6st623q4v5egm/node_modules/vitest/dist/chunks/run.BTlFXlnv.js:2783:20"
|
||||
},
|
||||
{
|
||||
"id": "wiki-genuine-run-id",
|
||||
"codeHead": "dfdc39b3a71c74c40e267010c6d8428af523bb65",
|
||||
"owner": "packages/plugins/plugin-llm-wiki/src/wiki/core.ts",
|
||||
"sourceSha256": "35d8543f67ab80479c624f527d0b94fbb0f3b309d40b1276a07518872212ea06",
|
||||
"case": "LLM Wiki plugin scaffold starts query sessions, records run ids, and forwards session events to plugin streams",
|
||||
"expected": "one intended assertion failure",
|
||||
"actualFailedCases": 1,
|
||||
"intendedAssertionFailed": true,
|
||||
"sourceRestored": true,
|
||||
"failure": "AssertionError: expected [ \u2026(4) ] to deeply equal [ \u2026(4) ]\n at <workspace>/packages/plugins/plugin-llm-wiki/tests/plugin.spec.ts:3610:44\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at file://<workspace>/node_modules/.pnpm/vitest@5.0.3_@opentelemetry+api@1.9.1_@types+node@24.13.3_jsdom@30.0.1_@noble+hashes@2.4.0__v_wr5mxfuu6kgge6st623q4v5egm/node_modules/vitest/dist/chunks/run.BTlFXlnv.js:2783:20"
|
||||
},
|
||||
{
|
||||
"id": "wiki-positive-source-writes",
|
||||
"codeHead": "dfdc39b3a71c74c40e267010c6d8428af523bb65",
|
||||
"owner": "packages/plugins/plugin-llm-wiki/src/wiki/core.ts",
|
||||
"sourceSha256": "35d8543f67ab80479c624f527d0b94fbb0f3b309d40b1276a07518872212ea06",
|
||||
"case": "LLM Wiki plugin scaffold keeps default-space files at the root and isolates managed spaces under slug prefixes",
|
||||
"expected": "one intended assertion failure",
|
||||
"actualFailedCases": 1,
|
||||
"intendedAssertionFailed": true,
|
||||
"sourceRestored": true,
|
||||
"failure": "AssertionError: expected [] to have a length of 2 but got +0\n at Proxy.<anonymous> (file://<workspace>/node_modules/.pnpm/vitest@5.0.3_@opentelemetry+api@1.9.1_@types+node@24.13.3_jsdom@30.0.1_@noble+hashes@2.4.0__v_wr5mxfuu6kgge6st623q4v5egm/node_modules/vitest/dist/chunks/index.C4tKZ0yW.js:2147:20)\n at Proxy.<anonymous> (file://<workspace>/node_modules/.pnpm/vitest@5.0.3_@opentelemetry+api@1.9.1_@types+node@24.13.3_jsdom@30.0.1_@noble+hashes@2.4.0__v_wr5mxfuu6kgge6st623q4v5egm/node_modules/vitest/dist/chunks/index.C4tKZ0yW.js:1944:14)\n at Proxy."
|
||||
},
|
||||
{
|
||||
"id": "wiki-positive-page-writes",
|
||||
"codeHead": "dfdc39b3a71c74c40e267010c6d8428af523bb65",
|
||||
"owner": "packages/plugins/plugin-llm-wiki/src/wiki/core.ts",
|
||||
"sourceSha256": "35d8543f67ab80479c624f527d0b94fbb0f3b309d40b1276a07518872212ea06",
|
||||
"case": "LLM Wiki plugin scaffold keeps default-space files at the root and isolates managed spaces under slug prefixes",
|
||||
"expected": "one intended assertion failure",
|
||||
"actualFailedCases": 1,
|
||||
"intendedAssertionFailed": true,
|
||||
"sourceRestored": true,
|
||||
"failure": "AssertionError: expected [ { \u2026(2) }, { \u2026(2) } ] to have a length of 4 but got 2\n at Proxy.<anonymous> (file://<workspace>/node_modules/.pnpm/vitest@5.0.3_@opentelemetry+api@1.9.1_@types+node@24.13.3_jsdom@30.0.1_@noble+hashes@2.4.0__v_wr5mxfuu6kgge6st623q4v5egm/node_modules/vitest/dist/chunks/index.C4tKZ0yW.js:2147:20)\n at Proxy.<anonymous> (file://<workspace>/node_modules/.pnpm/vitest@5.0.3_@opentelemetry+api@1.9.1_@types+node@24.13.3_jsdom@30.0.1_@noble+hashes@2.4.0__v_wr5mxfuu6kgge6st623q4v5egm/node_modules/vitest/dist/chunks/index.C4tKZ0yW.js:194"
|
||||
},
|
||||
{
|
||||
"id": "aws-create-must-wait-before",
|
||||
"codeHead": "dfdc39b3a71c74c40e267010c6d8428af523bb65",
|
||||
"owner": "server/src/services/secrets.ts",
|
||||
"sourceSha256": "2b7cac8d903802ea47c71033fa2d5dffb4d01a9ccde80837445235b0208b8e97",
|
||||
"case": "secretService serializes an aws_secrets_manager secret create against a concurrent account-home mutation lock holder",
|
||||
"expected": "old weak control passes",
|
||||
"actual": "passed",
|
||||
"intendedResult": true,
|
||||
"sourceRestored": true
|
||||
},
|
||||
{
|
||||
"id": "aws-rotate-must-wait-before",
|
||||
"codeHead": "dfdc39b3a71c74c40e267010c6d8428af523bb65",
|
||||
"owner": "server/src/services/secrets.ts",
|
||||
"sourceSha256": "2b7cac8d903802ea47c71033fa2d5dffb4d01a9ccde80837445235b0208b8e97",
|
||||
"case": "secretService serializes an aws_secrets_manager secret rotate against a concurrent account-home mutation lock holder",
|
||||
"expected": "old weak control passes",
|
||||
"actual": "passed",
|
||||
"intendedResult": true,
|
||||
"sourceRestored": true
|
||||
},
|
||||
{
|
||||
"id": "aws-create-must-wait-after",
|
||||
"codeHead": "445d568ca4e903fc998819cae31d421403514684",
|
||||
"owner": "server/src/services/secrets.ts",
|
||||
"sourceSha256": "2b7cac8d903802ea47c71033fa2d5dffb4d01a9ccde80837445235b0208b8e97",
|
||||
"case": "secretService serializes an aws_secrets_manager secret create against a concurrent account-home mutation lock holder",
|
||||
"expected": "repaired control fails on provider entry before lock release",
|
||||
"actual": "failed",
|
||||
"intendedResult": true,
|
||||
"sourceRestored": true,
|
||||
"failure": "AssertionError: expected [ 'cleanup-enter', \u2026(1) ] to deeply equal [ 'cleanup-enter' ]\n at <workspace>/server/src/__tests__/secrets-service.test.ts:1013:22\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at file://<workspace>/node_modules/.pnpm/vitest@5.0.3_@opentelemetry+api@1.9.1_@types+node@24.13.3_jsdom@30.0.1_@noble+hashes@2.4.0__v_wr5mxfuu6kgge6st623q4v5egm/node_modules/vitest/dist/chunks/run.BTlFXlnv.js:2783:20"
|
||||
},
|
||||
{
|
||||
"id": "aws-rotate-must-wait-after",
|
||||
"codeHead": "445d568ca4e903fc998819cae31d421403514684",
|
||||
"owner": "server/src/services/secrets.ts",
|
||||
"sourceSha256": "2b7cac8d903802ea47c71033fa2d5dffb4d01a9ccde80837445235b0208b8e97",
|
||||
"case": "secretService serializes an aws_secrets_manager secret rotate against a concurrent account-home mutation lock holder",
|
||||
"expected": "repaired control fails on provider entry before lock release",
|
||||
"actual": "failed",
|
||||
"intendedResult": true,
|
||||
"sourceRestored": true,
|
||||
"failure": "AssertionError: expected [ 'cleanup-enter', \u2026(1) ] to deeply equal [ 'cleanup-enter' ]\n at <workspace>/server/src/__tests__/secrets-service.test.ts:1081:22\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at file://<workspace>/node_modules/.pnpm/vitest@5.0.3_@opentelemetry+api@1.9.1_@types+node@24.13.3_jsdom@30.0.1_@noble+hashes@2.4.0__v_wr5mxfuu6kgge6st623q4v5egm/node_modules/vitest/dist/chunks/run.BTlFXlnv.js:2783:20"
|
||||
}
|
||||
],
|
||||
"unmutatedControls": [
|
||||
{
|
||||
"batch": "round4 owners and keepers",
|
||||
"files": 58,
|
||||
"passed": 1455,
|
||||
"failed": 0,
|
||||
"pending": 0
|
||||
},
|
||||
{
|
||||
"batch": "tool-access owners and state keepers",
|
||||
"passed": 623,
|
||||
"failed": 0,
|
||||
"pending": 0
|
||||
},
|
||||
{
|
||||
"batch": "final state-service owners and keepers",
|
||||
"files": 8,
|
||||
"passed": 329,
|
||||
"failed": 0,
|
||||
"pending": 0
|
||||
}
|
||||
],
|
||||
"preservationReviews": [
|
||||
{
|
||||
"batch": "round4-ui",
|
||||
"baseline": "2633dbbba95e853e3cb0de165bdf5116e544e411",
|
||||
"method": "Independent whole-runtime AST comparison after actual new helper expansion and individually reviewed semantic edits. Type-only syntax and source metadata normalized; compiler is a separate gate.",
|
||||
"files": [
|
||||
{
|
||||
"file": "ui/src/components/IssueProperties.test.tsx",
|
||||
"sourceSha256": "6c0a2420fdffeb3ccf6c9f4b43b61b62c3bd0e107276a91402a25ae08ad54bd2",
|
||||
"proposalSha256": "8d604724e4c9c423b560d13dae6e939113f248b050905542970ee0660dfeab98",
|
||||
"fullProgramAstEqualAfterActualHelperExpansionAndEnumeratedBehaviorEdits": true,
|
||||
"helperCalls": {
|
||||
"createRelation": 17
|
||||
}
|
||||
},
|
||||
{
|
||||
"file": "ui/src/components/AgentConfigForm.render.test.tsx",
|
||||
"sourceSha256": "dd3703a491b43ee5b7cd57c48798e8a4cec4dbabc075f222cea412383eca49d7",
|
||||
"proposalSha256": "57282ae3afe8c55ac118f3a658152d8df500a87b15383c8f6b0530a7fdcfb8a0",
|
||||
"fullProgramAstEqualAfterActualHelperExpansionAndEnumeratedBehaviorEdits": true,
|
||||
"helperCalls": {
|
||||
"createFormQueryClient": 17,
|
||||
"renderFormView": 18,
|
||||
"daytonaEnvironments": 12,
|
||||
"e2bEnvironments": 6
|
||||
}
|
||||
},
|
||||
{
|
||||
"file": "ui/src/components/OnboardingWizard.test.tsx",
|
||||
"sourceSha256": "c17e65d7e0604b58d398cdf8a2a8588e7bc388ffe6e2612cee8e567e01a30f20",
|
||||
"proposalSha256": "6215ee2f034686deaf2d6091590030dcdeccff1697422574e45e0351eaf5a7fe",
|
||||
"fullProgramAstEqualAfterActualHelperExpansionAndEnumeratedBehaviorEdits": true,
|
||||
"helperCalls": {
|
||||
"renderWizardView": 19
|
||||
}
|
||||
},
|
||||
{
|
||||
"file": "ui/src/components/IssueThreadInteractionCard.test.tsx",
|
||||
"sourceSha256": "3e3ebbe3da7ad7edd9386760bcf90a83c987956fc4f65b74bb761bd0160ce746",
|
||||
"proposalSha256": "2e8aefda409a2faa7e3a3bc4d3f292a17086c61eac1e34291002b62c7415a184",
|
||||
"fullProgramAstEqualAfterActualHelperExpansionAndEnumeratedBehaviorEdits": true,
|
||||
"helperCalls": {}
|
||||
},
|
||||
{
|
||||
"file": "ui/src/components/MarkdownBody.test.tsx",
|
||||
"sourceSha256": "1be4fe615ed6d9e1b8b49749f291475c12848beb5664758c512f368103121608",
|
||||
"proposalSha256": "7432e047a21ca0a75dc58ca0fd645c7cb0b59edc1719557e4e0cb0aafc8a642a",
|
||||
"fullProgramAstEqualAfterActualHelperExpansionAndEnumeratedBehaviorEdits": true,
|
||||
"helperCalls": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"batch": "round4-server",
|
||||
"baseline": "2633dbbba95e853e3cb0de165bdf5116e544e411",
|
||||
"method": "Independent whole-runtime AST comparison after actual new helper expansion and individually reviewed semantic edits. Type-only syntax and source metadata normalized; compiler is a separate gate.",
|
||||
"files": [
|
||||
{
|
||||
"file": "server/src/__tests__/issue-comment-reopen-routes.test.ts",
|
||||
"sourceSha256": "214aaa47ce1a862e4d06f41a440d0dba2a2f9f2efa63259c635209a3f660c33e",
|
||||
"proposalSha256": "2dc9167f66f379cb5a1fe024faf78a79c7009d570c4ca4acc72448d0a00423c7",
|
||||
"fullProgramAstEqualAfterActualHelperExpansionAndEnumeratedBehaviorEdits": true,
|
||||
"helperCalls": {
|
||||
"createScheduledRetry": 4,
|
||||
"createAgentReviewPolicy": 12,
|
||||
"createReviewIssue": 9
|
||||
}
|
||||
},
|
||||
{
|
||||
"file": "server/src/__tests__/environment-routes.test.ts",
|
||||
"sourceSha256": "8993233f12a3b646f7a9755b817bac68916fda31753c2ffb30c4eba9fbcc9fe8",
|
||||
"proposalSha256": "ef2dd81154c25791d2020362b6a3cd508ae215d60b075e1496ea9c7aa38ee6bd",
|
||||
"fullProgramAstEqualAfterActualHelperExpansionAndEnumeratedBehaviorEdits": true,
|
||||
"helperCalls": {
|
||||
"localBoardActor": 39,
|
||||
"createSecurePluginDriver": 6,
|
||||
"createSshConfig": 4,
|
||||
"createSecretBackedSshEnvironment": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"file": "server/src/__tests__/instance-settings-routes.test.ts",
|
||||
"sourceSha256": "f007e394234f82d0014a1f2d83831942d0c3a2b123e026f4c22edd4d6491754f",
|
||||
"proposalSha256": "85f0ddf9afb42cfc3e243e107b47290d9f8c8e5879eb3932805466019c937dc4",
|
||||
"fullProgramAstEqualAfterActualHelperExpansionAndEnumeratedBehaviorEdits": true,
|
||||
"helperCalls": {
|
||||
"localAdminActor": 15
|
||||
}
|
||||
},
|
||||
{
|
||||
"file": "server/src/__tests__/agent-skills-routes.test.ts",
|
||||
"sourceSha256": "e9c67d26d49c51862cefdde9388893d0e11b32f7d29ee95c664741cd48bc7468",
|
||||
"proposalSha256": "175b4b5f274141a817c2135830c1b73ecdfb9597e1d0579af1d0207c6faf551e",
|
||||
"fullProgramAstEqualAfterActualHelperExpansionAndEnumeratedBehaviorEdits": true,
|
||||
"helperCalls": {
|
||||
"skillSnapshot": 6
|
||||
}
|
||||
},
|
||||
{
|
||||
"file": "server/src/__tests__/company-skills-routes.test.ts",
|
||||
"sourceSha256": "9fdfa5790bd33b9824e3aeed28daa01065eab248712d2f9482f633f7c33165a9",
|
||||
"proposalSha256": "7037feadb73f235f81afeb9499b5bc9b988e2057b43add48c7546f6558085a03",
|
||||
"fullProgramAstEqualAfterActualHelperExpansionAndEnumeratedBehaviorEdits": true,
|
||||
"helperCalls": {
|
||||
"createCatalogSkill": 3,
|
||||
"createSkillTestRunDetails": 5,
|
||||
"boardActor": 25,
|
||||
"assignedAgentActor": 7
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"batch": "round4-cli-api-ticker",
|
||||
"method": "Actual proposed helper bodies expanded. Source metadata/comments/types and object shorthand normalized; object fields, array rows, JSX props, all values/callbacks/promises/awaits, action/write/Git order and cleanup remain strict. Statement-helper parameters are checked to be stable Identifier bindings. Only enumerated reviewed retirement/migration deltas are permitted.",
|
||||
"checks": [
|
||||
{
|
||||
"file": "cli/src/__tests__/company.test.ts",
|
||||
"sourceSha256": "d957294d6113f3b546e15e36289f5085852fc5ab1350529bc025a192fc05d378",
|
||||
"proposalSha256": "1cd0c0445ab57a8c74416a63da6acb4ddde36e8c09aa5b3c74db589792445be1",
|
||||
"actualHelperExpansionWholeRuntimeAstEqual": true,
|
||||
"expandedCalls": {
|
||||
"allImportEntities": 6,
|
||||
"sourceCompanyManifest": 2,
|
||||
"ceoManifest": 2,
|
||||
"alphaProjectManifest": 2,
|
||||
"kickoffIssueManifest": 2,
|
||||
"openAiEnvInput": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"file": "packages/shared/src/app-definitions.test.ts",
|
||||
"sourceSha256": "cf1aec35f1f4c65dd7059f594e04030c339c2a2f329f6e8216a431280d3865c3",
|
||||
"proposalSha256": "d6aa2d7ba801b049358e89f48d39f3602ac936d1c28e7cd5b3313a2227fd1a95",
|
||||
"actualHelperExpansionWholeRuntimeAstEqual": true,
|
||||
"expandedCalls": {}
|
||||
},
|
||||
{
|
||||
"file": "packages/plugins/plugin-llm-wiki/tests/plugin.spec.ts",
|
||||
"sourceSha256": "21ae3eef99bb450f2cc0cac8cb7d781f0dbb043ad4fdf8c1d09b528021114242",
|
||||
"proposalSha256": "bcb816d66be0b0176d599bbb705f79724abeebe8f52f28ed1fc31787b06f9985",
|
||||
"actualHelperExpansionWholeRuntimeAstEqual": true,
|
||||
"expandedCalls": {
|
||||
"renderWikiPage": 22,
|
||||
"captureWikiWrites": 9,
|
||||
"managedSpaceRow": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"file": "cli/src/__tests__/worktree.test.ts",
|
||||
"sourceSha256": "90162a4d544e80d7314649f2d0da5ca34c6968fbd4a63e491c57f5fdf7077db7",
|
||||
"proposalSha256": "4dd193538aaf9bdd0cab522a8647ea5fb6a573e26199561e65c725c8bcb51ce1",
|
||||
"actualHelperExpansionWholeRuntimeAstEqual": true,
|
||||
"expandedCalls": {
|
||||
"writeSeedConfigFiles": 6,
|
||||
"initializeTestRepo": 5
|
||||
}
|
||||
}
|
||||
],
|
||||
"canonicalChecks": [
|
||||
{
|
||||
"file": "cli/src/__tests__/worktree.test.ts",
|
||||
"baselineSha256": "47c51bafca401b39bd976c12ddabef00be8ff6d3c2bc79822c49d11a3f79bc85",
|
||||
"repairedSha256": "90162a4d544e80d7314649f2d0da5ca34c6968fbd4a63e491c57f5fdf7077db7",
|
||||
"wholeRuntimeAstEqualApartFromParentCanonicalization": true,
|
||||
"canonicalTempRootWrappers": 35
|
||||
},
|
||||
{
|
||||
"file": "packages/shared/src/worktree-seed-source.test.ts",
|
||||
"baselineSha256": "bd8708bab463280939f0aa8aa18341b028343fe98626d9be2154c87ab191c1a5",
|
||||
"repairedSha256": "b9d021050cced44bcee00fd3209c225c4e26117693b7491a50ef9d3bd0a67d17",
|
||||
"wholeRuntimeAstEqualApartFromParentCanonicalization": true,
|
||||
"canonicalTempRootWrappers": 3
|
||||
}
|
||||
],
|
||||
"coreChecks": [
|
||||
{
|
||||
"file": "ui/src/api/client.ts",
|
||||
"sourceSha256": "cac548df1d20c87a0ea8eede850cfe5081874262eb8c26c78058923db6947f69",
|
||||
"proposalSha256": "1e1b3f03e3269a5943f55d294f1c7aa9f5c3eca569858d9fdc7c5d976de88bba",
|
||||
"onlyDeletedPrivateGetterExport": "__inflightGetCount"
|
||||
},
|
||||
{
|
||||
"file": "ui/src/hooks/useSecondTick.ts",
|
||||
"sourceSha256": "e108dee2647be4f8f5472a70220d1019b18cce9a842e7bc25b863e298654ab07",
|
||||
"proposalSha256": "fd51875adf5f30295d847df58af19f75dcbb59c810b4f02919427cbc6ea4d3b3",
|
||||
"onlyDeletedPrivateGetterExport": "__secondTickInternals"
|
||||
}
|
||||
],
|
||||
"seamTestChecks": [
|
||||
{
|
||||
"file": "ui/src/api/client.test.ts",
|
||||
"sourceSha256": "b056adfc0a935cdabcc1c05b4473f76bc1f46fc6f280a671404c2735211ebe01",
|
||||
"proposalSha256": "1b3666404821ae4550b1fdf48fe27568077f8074dbdff8afc5f2966e79341ffd",
|
||||
"apiWholeRuntimeAstEqualApartFromReviewedPrivateRemovalAndErrorAbortRetry": true,
|
||||
"oldDeclarations": 12,
|
||||
"newDeclarations": 12
|
||||
},
|
||||
{
|
||||
"file": "ui/src/hooks/useSecondTick.test.tsx",
|
||||
"sourceSha256": "29d8f37a65070f28ddb43bec69e74477cdfb1e563b348aff03b6b5ca17902e95",
|
||||
"proposalSha256": "b59f9bab5e41bc611165577d8e095184094dd5b84b6d436d32656fd33fb5e459",
|
||||
"oldDeclarations": 2,
|
||||
"newDeclarations": 2
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"batch": "round5-tools",
|
||||
"baseline": "3e7ee04591fd82f3effb89370f47aa90dcadd6eb",
|
||||
"method": "Only source metadata/comments and object shorthand differ. Actual candidate helper bodies expanded, removing only their new declarations and type-only IdRef aliases. Object-field, nested object, array, table, JSX-free input, promise, await, callback, actor/company/credential/expected value and statement order remain exact. Newly introduced literal spreads are allowed only when there are no overwritten keys or effectful helper arguments. All helper object bodies are fresh literals without external closure state.",
|
||||
"files": [
|
||||
{
|
||||
"path": "server/src/__tests__/tool-access-service.test.ts",
|
||||
"sourceSha256": "4cd7d6d02710eac514f14d87c316d15391028ae36c477d868f6b5a499a29f406",
|
||||
"proposalSha256": "dc3664a18f9fc6c45b3b5555376ae03def2708129ff5c3dc4f4623ea2f994c15",
|
||||
"completeRuntimeAndTypeAstEqualAfterActualHelperExpansionAndOneDeclarationRemoval": true,
|
||||
"helperOccurrences": 78,
|
||||
"allRemainingMatcherCallsEqual": true,
|
||||
"allAwaitExpressionsEqual": true,
|
||||
"allSupertestCallsEqual": true,
|
||||
"remainingMatcherCalls": 1346,
|
||||
"awaitExpressions": 1930,
|
||||
"beforeDeclarations": 266,
|
||||
"afterDeclarations": 265
|
||||
},
|
||||
{
|
||||
"path": "server/src/__tests__/tool-access-policy-service.test.ts",
|
||||
"sourceSha256": "484408a4eb3481e98d89863943b0fac64cf40e3794c06d0b4326a4b0d0f7625f",
|
||||
"proposalSha256": "6982ad8f42fceeb6428117f22db28a589f9b6d5f1b2465ca87ddd29420e06413",
|
||||
"completeRuntimeAndTypeAstEqualAfterActualHelperExpansionAndOneDeclarationRemoval": true,
|
||||
"helperOccurrences": 31,
|
||||
"allRemainingMatcherCallsEqual": true,
|
||||
"allAwaitExpressionsEqual": true,
|
||||
"allSupertestCallsEqual": true,
|
||||
"remainingMatcherCalls": 80,
|
||||
"awaitExpressions": 284,
|
||||
"beforeDeclarations": 35,
|
||||
"afterDeclarations": 35
|
||||
},
|
||||
{
|
||||
"path": "server/src/__tests__/tool-gateway-service.test.ts",
|
||||
"sourceSha256": "29c9b9739a5f928c49cd2982858a1323c242663450277f45fc9e3aea407cbe98",
|
||||
"proposalSha256": "67d8dfb12ef2215296af87be5c573f40175d71a2917b01113aa82c550fe4b6d6",
|
||||
"completeRuntimeAndTypeAstEqualAfterActualHelperExpansionAndOneDeclarationRemoval": true,
|
||||
"helperOccurrences": 58,
|
||||
"allRemainingMatcherCallsEqual": true,
|
||||
"allAwaitExpressionsEqual": true,
|
||||
"allSupertestCallsEqual": true,
|
||||
"remainingMatcherCalls": 231,
|
||||
"awaitExpressions": 431,
|
||||
"beforeDeclarations": 36,
|
||||
"afterDeclarations": 36
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"batch": "round5-state",
|
||||
"baseline": "3e7ee04591fd82f3effb89370f47aa90dcadd6eb",
|
||||
"method": "Independent full runtime AST expansion outside two individually reviewed AWS lock repairs",
|
||||
"files": [
|
||||
{
|
||||
"file": "server/src/__tests__/issue-thread-interactions-service.test.ts",
|
||||
"beforeSha256": "ddfad204b976a8218b6376007c36048154c4a10e25ef4d891531b87477931c10",
|
||||
"afterSha256": "6c70d8af0815de3d3ed9f87de94d20e7a161aa2db5fe938e80204a100ee418e9",
|
||||
"equivalent": true,
|
||||
"expandedBeforeSha256": "0a7affd96c9e84850e2540edea63bbc6eabf24f124468e2526fa3095f2ddd7af",
|
||||
"expandedAfterSha256": "0a7affd96c9e84850e2540edea63bbc6eabf24f124468e2526fa3095f2ddd7af",
|
||||
"allHelperCalls": {
|
||||
"interactionCompanyRow": 14,
|
||||
"parentIssueRow": 7,
|
||||
"activeCodexAgentRow": 19,
|
||||
"acceptIssueScope": 31
|
||||
},
|
||||
"allHelperArgumentsEffectFree": true,
|
||||
"beforeDeclarations": 89,
|
||||
"afterDeclarations": 89
|
||||
},
|
||||
{
|
||||
"file": "server/src/__tests__/secrets-service.test.ts",
|
||||
"beforeSha256": "ab9c9536ee144d9211bb8a205dd2e57ff928360de6ea695af74afd749e9652be",
|
||||
"afterSha256": "95708c40da78768c43c26a56c73cf9479a4a39bf2173561d9501a1ab6d13dfb1",
|
||||
"equivalent": true,
|
||||
"expandedBeforeSha256": "fb5da5592c1de24b62a44e476554924e0854cd2822767e46132ce63431c11475",
|
||||
"expandedAfterSha256": "fb5da5592c1de24b62a44e476554924e0854cd2822767e46132ce63431c11475",
|
||||
"allHelperCalls": {
|
||||
"awsVaultInput": 29,
|
||||
"preparedAwsVersion": 16,
|
||||
"managedAwsVersionRow": 4
|
||||
},
|
||||
"allHelperArgumentsEffectFree": true,
|
||||
"beforeDeclarations": 91,
|
||||
"afterDeclarations": 91
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
# Test audit: consolidate repeated owner fixtures and rendering
|
||||
|
||||
Date: 2026-10-12
|
||||
Branch: `codex/test-audit-next`
|
||||
Discovery baseline: `b4aee6ea18905690686fb318fa9cc6d4cc71a7e1`
|
||||
Integration base: `1e0cfcb4fc4ed78d10f46cd60990935af4ac9973` (merged master)
|
||||
Verified code commit: `4261a7fc1b964e54059c68246a0a4b7ea74bdcea`
|
||||
Parent cleanup: [PR #15978](https://github.com/paperclipai/paperclip/pull/15978)
|
||||
|
||||
This follow-up removes **2,041 net test lines across five files**. Every
|
||||
registered case and assertion remains. It changes no application source.
|
||||
The seven complete owner/sibling files pass all **990 cases** before and after.
|
||||
Workspace typecheck, build, token gates and diff checks pass. The change
|
||||
consolidates fixture setup while preserving execution count.
|
||||
|
||||
The parent PR merged on 2026-10-12 after all remote checks passed and a
|
||||
fresh Greptile 5/5 review. This branch used a separate managed checkout and
|
||||
dependency installation. Only its new follow-up commit was replayed onto
|
||||
the parent merge, with no conflicts or duplicated cleanup commits. All five
|
||||
test files still exactly match the independently reviewed candidates, and
|
||||
all 990 owner/sibling cases pass again on the merged base. The follow-up remains
|
||||
committed locally on its own branch. Earlier cleanup evidence, including its
|
||||
disclosed local timeout and passing owner reproduction, lives in
|
||||
`2026-10-12-test-audit.md`.
|
||||
|
||||
## Measured change
|
||||
|
||||
| File | Before lines | After lines | Net removed | Cases retained |
|
||||
| --- | ---: | ---: | ---: | ---: |
|
||||
| server/src/__tests__/environment-runtime.test.ts | 8,110 | 7,223 | 887 | 162 |
|
||||
| server/src/__tests__/execution-workspaces-service.test.ts | 4,751 | 4,638 | 113 | 67 |
|
||||
| server/src/__tests__/company-portability.test.ts | 6,194 | 6,100 | 94 | 94 |
|
||||
| ui/src/pages/IssueDetail.test.tsx | 6,652 | 6,338 | 314 | 141 |
|
||||
| ui/src/components/IssueChatThread.test.tsx | 5,032 | 4,399 | 633 | 100 |
|
||||
| **Changed owners** | **30,739** | **28,698** | **2,041** | **564** |
|
||||
|
||||
The remaining 426 validation cases are unchanged TaskChatThread (225) and
|
||||
AppsConnect (201) siblings. Figures exclude this report. No case, timeout,
|
||||
skip condition, filename, CI route or production seam is removed.
|
||||
|
||||
## Owner and keeper decisions
|
||||
|
||||
All nominated cases are retained. Each original case remains the primary
|
||||
keeper of its regression assertions; helpers own only repeated inputs.
|
||||
|
||||
- Environment runtime: 71 repeated plugin/lease/worker/foreign-company
|
||||
expressions become synchronous file-local fixture builders. Complete
|
||||
identity, author, schema, readiness, capability, install-order, stale lease,
|
||||
fingerprint and RPC-budget variants remain defaults or explicit overrides.
|
||||
Preserve real persisted rows and the actual foreign-company rejection,
|
||||
orphan teardown/spool recovery and lease-reuse paths. Dynamic/spread rows
|
||||
and the named-constant secret provider stay inline.
|
||||
- Execution workspaces: 19 repeated task/recovery/branch/runtime/agent inputs
|
||||
become synchronous builders. Real Git operations, active claimant/runtime
|
||||
rows, locked races, clean/dirty divergence and durable recovery assertions
|
||||
remain in their original cases.
|
||||
- Company portability: 15 identical export invocations use one local helper;
|
||||
their distinct fixture data, redaction, permission, import, selected-file,
|
||||
logo and membership expectations remain explicit.
|
||||
- IssueDetail: 98 identical root/provider render frames share one synchronous
|
||||
helper. It retains the same QueryClient and root. Four nondefault children
|
||||
remain explicit JSX; unsafe undefined/key/ref/spread shapes stay inline.
|
||||
- IssueChatThread: 70 MemoryRouter/component frames share one synchronous
|
||||
helper with fresh original defaults. Supplied callback references, props,
|
||||
dynamic expression order, root identity and rerender/remount behavior remain.
|
||||
|
||||
TaskChatThread and AppsConnect already have useful fixtures or distinct
|
||||
transport/identity behavior. A TaskChatThread prototype saved only ten lines
|
||||
after helper overhead and was rejected. No further layer retirement was
|
||||
supported in these owners; separate provider, authentication, lifecycle and
|
||||
storage proof remains valuable.
|
||||
|
||||
## Preservation evidence
|
||||
|
||||
Original/candidate SHA-256 guards were validated before any checkout edit.
|
||||
Proposals were prepared outside the checkout during baseline execution.
|
||||
Independent reviewers checked both groups against the committed baseline.
|
||||
|
||||
Server: expand all 105 calls through the 16 actual helper bodies/defaults and
|
||||
compare all three complete programs. Field order, nested schemas/capabilities,
|
||||
types/casts, table rows, callbacks, hooks, 1,347 awaits and 1,201 expectation
|
||||
calls are unchanged. Helpers add no Promise boundary, transaction, UUID,
|
||||
clock read, receipt, assertion, RPC or reset. Objects and mocks remain fresh.
|
||||
|
||||
UI: independently expand all 98 + 70 actual render calls. The complete program
|
||||
outside those checked expressions is unchanged, including types, callbacks,
|
||||
actions, waits, timers, teardown and budgets. All 778 expectation calls and
|
||||
200 declarations remain. Check every thread root identity and dynamic prop
|
||||
evaluation order; check IssueDetail root/client bindings for shadowing.
|
||||
Only unique JSX attribute order is normalized; key/ref/spread shapes are
|
||||
excluded. No React component wrapper or hook is introduced.
|
||||
|
||||
Both independent reviews found no preservation blocker. No assertion was
|
||||
moved or repaired, so no additional production mutation was needed. The
|
||||
earlier parent PR's mutation proof is recorded in its report.
|
||||
|
||||
## Runtime verification and limits
|
||||
|
||||
- Complete baseline: seven files, 990 passed, zero failed/skipped.
|
||||
- Complete candidate: the same seven files, 990 passed, zero failed/skipped.
|
||||
- Rebased candidate: the same seven files, 990 passed, zero failed/skipped.
|
||||
- `pnpm --filter @paperclipai/ui typecheck`: pass before integration.
|
||||
- `pnpm -r typecheck`: pass before integration and at the verified code commit.
|
||||
- `pnpm build`: pass at the verified code commit.
|
||||
- Token gates, palette synchronization and character synchronization: pass.
|
||||
- `git diff --check`: pass.
|
||||
- The new checkout used its own workspace-linked dependencies and built its
|
||||
own TypeScript runner/shared/plugin SDK prerequisites. No dependencies or
|
||||
source files were linked to the concurrently changing PR checkout.
|
||||
- A complete default `pnpm test:run` has not been repeated for this follow-up.
|
||||
Its test-only fixture changes have complete affected-owner/sibling proof.
|
||||
Browser suites were not run; none of those suites changed. The parent PR
|
||||
has its own full CI evidence. No wall-time speedup is attributed to this work.
|
||||
|
||||
## Additional retirement search
|
||||
|
||||
A bounded independent search found no further defensible retirement of
|
||||
200 or more lines. Provider-specific behavior, separately implemented
|
||||
parsers, CSS contrast/theme/layout contracts and runtime/SDK wiring retain
|
||||
independent proof. Similar source guards or DOM class assertions did not
|
||||
provide complete behavioral keepers, so those layers remain.
|
||||
@@ -0,0 +1,92 @@
|
||||
# Test audit: repeated setup, duplicate cases and false proof
|
||||
|
||||
Date: 2026-10-12. Audit branch: `codex/test-audit-next`. Delivery branch: `codex/test-audit-followup`. Verified code revision: `64370831ebe191e5ce092f85f79b6fb661df7e16`, including master `cc88b61c0918f45f6d3215f5d2d2368c88297046`.
|
||||
|
||||
This follow-up removes **5,027 net test lines across 27 test files** and **15 redundant registered executions**. It separately removes **11 unused application lines** from two private test-inspection exports. Most savings come from sharing repeated setup while retaining every distinct case. No wall-time improvement is inferred from line counts.
|
||||
|
||||
The earlier merged [PR #15978](https://github.com/paperclipai/paperclip/pull/15978) removed 11,976 net test/support lines and 1,073 redundant executions. The two changes together save **17,003 test/support lines**. These counts exclude audit reports, upstream changes and the fixture portability repair.
|
||||
|
||||
## Savings and scope
|
||||
|
||||
| Batch | Net test lines removed | Registered executions removed | Preservation |
|
||||
| --- | ---: | ---: | --- |
|
||||
| Earlier local runtime/workspace/portability and IssueDetail/IssueChatThread | 2,041 | 0 | All original cases and assertions retained; 990 owner/sibling cases passed. See [the earlier follow-up report](2026-10-12-test-audit-next.md). |
|
||||
| Route fixtures and duplicate proof | 970 | 5 | Three redundant cases and two identical table executions removed; cancelled-reopen observation repaired. |
|
||||
| UI fixtures and duplicate proof | 521 | 6 | Exact assertions move into named stronger keepers; two weak observations repaired. |
|
||||
| CLI, shared catalog and wiki plugin | 238 | 3 | Exact keepers and fixture expansion; genuine run-receipt and positive-write witnesses added. |
|
||||
| Public API and ticker observations | -10 | 0 | Tests grow by ten lines to replace private-state inspections with observable results. |
|
||||
| Tool access, policy and gateway | 656 | 1 | One exact omitted-scope duplicate removed; all other matcher and await order retained. |
|
||||
| Interactions and secrets state services | 611 | 0 | All 180 declarations / 197 cases retained; two AWS lock observations repaired. |
|
||||
| **Total follow-up** | **5,027** | **15** | **No independent security or lifecycle contract retired.** |
|
||||
|
||||
Line savings include all newly added local helper code. The application reduction is five lines of `__inflightGetCount` and six lines of `__secondTickInternals`. Repository-wide caller searches found only their tests. Both module cores otherwise remain byte-identical. Their tests retain sharing, cancellation, stale-account isolation, rendered ticks and timer lifecycle.
|
||||
|
||||
The final intended diff has 27 test files, those two application files and audit evidence. No schema, telemetry, runtime service, package manifest, lockfile or CI routing change is part of this cleanup. Incoming master changes were merged, so absent upstream files cannot inflate the savings.
|
||||
|
||||
## Removed execution to keeper map
|
||||
|
||||
Each retained declaration is its own keeper for setup-only consolidation. The following is the complete map for the 15 removed executions; the three-row influence table accounts for two of them.
|
||||
|
||||
| Removed proof | Retained keeper and boundary |
|
||||
| --- | --- |
|
||||
| Managed-sandbox duplicate DELETE rejection | Same owner/admin, platform row, DELETE, 403 and code; stronger keeper also forbids blast-radius/removal calls. |
|
||||
| Agent-skills duplicate GET/POST secret-context check | Mandatory resolver callbacks in both successful route keepers check the same actor/user/source/responsible literal context, adapter type, skip-user-secrets and filtered environment. |
|
||||
| Company-skills open-default legacy-consent duplicate | Same actor/company/run, deny-missing-consent decision and POST input; successful write keeper checks no consent-gate invocation and the actual write. |
|
||||
| Two extra missing/wrong-run influence table rows | All three ran the identical zero-argument callback. One retains both POST/PATCH errors, details, two propagation calls and no writes. The identity-variant service sibling and real PostgreSQL race sibling remain. |
|
||||
| Onboarding missing-auth/no-stored-login duplicate | Warning text moves into `blocks the hire on a warn result that holds adapter_auth_missing, and shows the returned checks`. |
|
||||
| Interaction connection controls for another viewer | `shows another viewer only who it is waiting for` retains waiting copy, zero buttons, no actions/auth URL and absence of Not now. |
|
||||
| Legitimate question-card rendering duplicate | `exposes pending question options as selectable radio and checkbox controls` gains positive child count and retains stronger exact radio counts. |
|
||||
| Addressed-user Connect/Not-now duplicate | `opens the shared connection setup for the addressed user` gains the exact copy and absence checks before executing the real setup action. |
|
||||
| Non-GitHub-link icon duplicate | `opens external links in a new tab with safe rel attributes` uses the same docs input and retains exact anchor, no GitHub icon, target and rel. |
|
||||
| External-link trailing-icon duplicate | The same docs-link keeper retains the icon glued to the final character and the original glyph matcher. |
|
||||
| Hugging Face scope duplicate | `requests Airtable record, schema and comment writes, and Hugging Face repository/job actions` retains the same independent `[read-mcp, read-repos, contribute-repos, jobs]` literal. |
|
||||
| Whole-catalog method/field invariants duplicate | Full-catalog `appDefinitionsSchema.parse` enforces nonempty ownership modes, required credential placeholders and API-key key placement for every definition; provider-specific permission literals remain. |
|
||||
| Worktree shutdown formatter duplicate | Real `ensure-seeded` failure retains restore error, safe diagnostic, failed manifest and lock release. The unique migrations fallback expectation moves unchanged into that keeper. |
|
||||
| Tool-access omitted-provider-scope duplicate | `marks a scope inferred from the request when the provider omits one` calls the identical `connectSlackWithTokenScope(null, ["channels:read"])`, retains all three persisted scope fields and also checks the outgoing authorization scope. Reported over-grants remain separately tested. |
|
||||
|
||||
The influence identity sibling uses a fake `counterDb` adapter for locked-row variants. The PostgreSQL sibling independently inserts real rows and races writes. Neither is removed, and fake SQL inputs are not described as durable persistence proof.
|
||||
|
||||
## Preservation review and repairs
|
||||
|
||||
Every nominated owner was read in full, including hooks, declarations, tables and cleanup, before classification as retain, fix false proof, consolidate or delete. Reviews checked actual owner/caller/history/CI boundaries rather than treating similar names as proof. Separate reviewers expanded the actual new helper bodies and arguments against complete original runtime ASTs. Types and source metadata are normalized separately from the compiler gate. Fields, nested values, fresh allocations, action/query/UUID/Date/callback/await order, tables, timeouts and teardown remain strict outside explicitly reviewed semantic edits.
|
||||
|
||||
The tool-access expansion covers 167 helper occurrences, 1,657 remaining matcher calls and 2,645 awaits. The state-service expansion covers 120 helper calls; no DB query is hidden in a new helper, and no new helper manufactures a tested service result. All 106 interaction and 91 secret cases remain. Two AWS repairs add two provider-call assertions while retaining the original durable results.
|
||||
|
||||
Review restored or strengthened these contracts before cutover:
|
||||
|
||||
- Restore the no-agentMap resolved footer/chip case byte-for-byte; a richer mapped-agent case cannot prove its fallback.
|
||||
- Match the exact external `<a href="https://example.com/docs"` prefix; href/target/rel alone could pass on a span.
|
||||
- Issue a fresh successful same-path GET after an unrelated 401 and after all callers abort, with exact two-fetch counts and independent values.
|
||||
- Keep an inactive ticker probe beside an active peer; only the active output may tick, and its unmount must clear the last timer.
|
||||
- Observe actual authenticated account-binding text after waiting, and mount the Claude login card positively before asserting absence of Use saved login.
|
||||
- Require cancelled bare-agent reopen to return the exact 409/error before writing a comment. The old broad status allowance accepted 201.
|
||||
- Capture and return the original wiki SDK `sendMessage` receipt unchanged, and compare its genuine run ID to the operation UPDATE bindings. Positive source/page write counts prevent empty `.every` predicates passing.
|
||||
- Confirm real account-home lock-holder entry, observe actual AWS create/version provider entry, assert exclusion while parked and exact eventual event order. A separate baseline secret preserves the tested rotation's original 1 → 2 result. Immediate rejection observers and `finally` release/all-settle preserve failure cleanup.
|
||||
|
||||
Wiki database witnesses inspect emitted SQL and bindings through the SDK's recorder. They do not claim that the recorder executed real SQL. Assertions guaranteed by its unconditional host-list markup were removed without retiring the real title/binding/configuration-link behavior.
|
||||
|
||||
## Falsification and complete positive controls
|
||||
|
||||
[The compact evidence record](2026-10-12-test-audit-controls.json) preserves actual source SHA guards, independent expansion results and mutation assertion receipts. It records 18 executions: 14 route/UI/wiki mutations, two original AWS weak controls and two repaired AWS controls. The original AWS cases pass their deliberate bypasses; the repaired cases fail on actual provider entry before lock release. All **16 repaired or retained keeper mutations** fail at the intended assertion, and every mutated source is restored byte-for-byte in `finally` before ordinary testing.
|
||||
|
||||
| Complete unmutated control | Result |
|
||||
| --- | --- |
|
||||
| Round 4 selected owners and siblings, including standalone wiki | 1,455 / 1,455 passed across 58 files; zero failed/pending. |
|
||||
| Tool-access owners and state-service keeper baselines | 623 / 623 passed: 491 tool cases plus 132 state siblings; zero failed/pending. |
|
||||
| Final state-service owners and siblings | 329 / 329 passed across eight files: 197 owners plus 132 siblings; zero failed/pending. |
|
||||
| Canonical worktree/shared seed-source control | 80 / 80 passed; zero failed/pending. |
|
||||
| Workspace `pnpm -r typecheck` | Passed at the verified code revision. |
|
||||
| UI `pnpm check:token-gates` | Passed at the verified code revision. |
|
||||
| Workspace `pnpm build` | Passed at the verified code revision. |
|
||||
| Full default `pnpm test:run` | Invoked at the verified code revision. The completed result is recorded in the follow-up PR’s Verification section. |
|
||||
| `git diff --check` | Passed before evidence publication; delivery also checks its final evidence diff. |
|
||||
|
||||
Rows overlap; they are separate receipts and must not be summed into a unique test count. Root Vitest excludes standalone wiki, so its three complete files were also run with its own existing configuration. Browser suites were not run: this change does not touch browser-suite sources or change a product journey. No deadline, retry, skip or shard was weakened for this audit.
|
||||
|
||||
## Portability and environment controls excluded from savings
|
||||
|
||||
The initial 1,157-case main baseline had 14 failures, all in CLI worktree fixtures using `/var` where the production guard correctly requires canonical `/private/var`. A separate shared seed-source baseline had the same alias failure in eight cases. A separate commit wraps only 38 fixture-allocation parents with `fs.realpathSync(os.tmpdir())` (35 CLI, three shared). It preserves every case/assertion and every explicit attacker-symlink negative. The complete 80-case repaired control passed; none of this is counted as pruning.
|
||||
|
||||
The standalone wiki package declares React as a peer, and this checkout disables automatic peer installation. Its baseline initially could not collect two files. An ignored node_modules link supplies the existing pinned React 19.2.8 from this same worktree. All 101 standalone baseline cases then passed. This setup changes no manifest or lockfile and links no dependencies across worktrees.
|
||||
|
||||
PR CI and exact-head review results belong on the follow-up PR. The report records local evidence at an immutable code revision; later evidence-only commits do not change the tested source. Full source ledgers and proposal staging files remain temporary, while the decisions, keeper map, actual preservation receipts and falsification outcomes above are preserved in the repository.
|
||||
@@ -14,6 +14,11 @@ Scoped runner-default source review: 2026-10-12 at
|
||||
selected-runtime testing and saved execution/session preservation. The original
|
||||
baseline source and review depth remain unchanged for unrelated operations.
|
||||
|
||||
Scoped native cancellation source review: 2026-10-12 at
|
||||
`ce23716503a2c9158b5a7ab74ab61571ff4ee7a3`, limited to owned startup and
|
||||
shutdown/readiness ordering. The original baseline and review depth remain for
|
||||
unrelated operations; no live provider or agent authorization is claimed.
|
||||
|
||||
## `availability`
|
||||
|
||||
Review: `baseline`.
|
||||
@@ -152,10 +157,15 @@ for active/recovered runs. Explicit runner changes use revisions and session
|
||||
invalidation. Verify saved configuration, current versus historical run identity,
|
||||
restart and follow-up without granting session-reset authority to an agent.
|
||||
|
||||
Scoped source-only evidence: the
|
||||
Scoped Evidence: `source-only` — the
|
||||
[native transport](../../packages/paperclip-runner/src/live/runnerd-codex-transport.ts)
|
||||
joins owned startup before certifying cleanup. Unconfirmed preparation or
|
||||
registration retains recovery state; late registration may retire only its own
|
||||
route and cannot launch a provider after close. This does not grant an agent
|
||||
account-setup, cleanup-override or session-reset authority. Verify the no-provider
|
||||
ownership controls independently from installed/live provider acceptance.
|
||||
joins owned startup before certifying cleanup. Rejected duplicates cannot replace
|
||||
that owner or mutate recovery state. Unconfirmed preparation or registration
|
||||
retains recovery state; late registration may release only its own route and
|
||||
cannot launch after close. Shutdown interrupts readiness without granting
|
||||
authority over an unauthenticated adopted process.
|
||||
|
||||
Scoped Verification: `not-run` — verify allowed/denied access using the actual
|
||||
actor independently from the credential-free ownership controls. This correction
|
||||
adds no account-setup, cleanup-override or session-reset authority. The real native
|
||||
readiness/recovery fixtures and installed provider acceptance remain separate gates.
|
||||
@@ -85,10 +85,12 @@ different observations, both required in the evidence.
|
||||
- Authentication/model discovery does not establish tool, image, steering, or resumption capability parity.
|
||||
- Use [recovery](./recovery.md) for interrupted runs and [execution environments](./execution-environments.md) for local/SSH/sandbox differences.
|
||||
|
||||
Native setup retirement must join owned startup before cleanup can be confirmed.
|
||||
Native setup retirement joins owned startup before cleanup can be confirmed.
|
||||
[Transport ownership controls](../../packages/paperclip-runner/src/live/runnerd-codex-transport.test.ts)
|
||||
hold credential preparation or registration without launching a provider, then
|
||||
verify bounded close, no later launch, and retention of unconfirmed state. A
|
||||
primary setup timeout remains a failure even when teardown also fails. These
|
||||
controls cover the lifecycle boundary; final packaged provider execution is
|
||||
separate evidence.
|
||||
hold preparation or registration without launching a provider, then check bounded
|
||||
close, rejected duplicate requests, no later launch and retention of unconfirmed
|
||||
state. Readiness observes shutdown; resumed startup cannot reactivate ingress
|
||||
after close. These are lifecycle controls, separate from installed provider proof.
|
||||
The real daemon readiness and recovery controls require their existing native
|
||||
fixtures in hosted CI; passing credential-free controls does not qualify them.
|
||||
A primary setup timeout remains a failure even when teardown also fails.
|
||||
@@ -0,0 +1,610 @@
|
||||
# Storybook by feature
|
||||
|
||||
<!-- Generated by node scripts/storybook-catalog.mjs --write. Edit catalog.json. -->
|
||||
|
||||
[Contract and maintenance](README.md) · [Review findings](REVIEW.md)
|
||||
|
||||
224 story files; 2223 indexed stories. Counts describe inventory, not passed tests or complete feature coverage.
|
||||
|
||||
Status describes product alignment; implementation describes the rendered subject; boundary describes simulated data. Sources and limitations are in catalog.json. Related capabilities cross-list one canonical story file.
|
||||
|
||||
## access
|
||||
|
||||
[Feature recipe](../feature-map/access.md)
|
||||
|
||||
| Story suite | Capabilities | Product / implementation / data | Stories / play functions | Review note |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| [Getting started and access/Login, invitations, and access/Private tasks/00 Review guide](../../ui/storybook/stories/access/private-tasks/00-review-guide.stories.tsx) | `access/resources`, `tasks/properties` | unreviewed / guide / static | 1 / 0 | Review guide retained; explanatory content does not establish runtime or production coverage. |
|
||||
| [Getting started and access/Login, invitations, and access/Private tasks/01 Creation](../../ui/storybook/stories/access/private-tasks/01-creation.stories.tsx) | `access/resources`, `tasks/properties` | unreviewed / production / fixtures | 22 / 14 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Getting started and access/Login, invitations, and access/Private tasks/02 Sharing](../../ui/storybook/stories/access/private-tasks/02-sharing.stories.tsx) | `access/resources`, `tasks/properties` | unreviewed / production / fixtures | 26 / 10 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Getting started and access/Login, invitations, and access/Private tasks/03 Task actions](../../ui/storybook/stories/access/private-tasks/03-task-actions.stories.tsx) | `access/resources`, `tasks/properties` | unreviewed / production / fixtures | 11 / 7 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Getting started and access/Login, invitations, and access/Private tasks/04 References and blockers](../../ui/storybook/stories/access/private-tasks/04-references-and-blockers.stories.tsx) | `access/resources`, `tasks/properties` | unreviewed / production / fixtures | 12 / 5 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Getting started and access/Login, invitations, and access/Private tasks/05 Projects](../../ui/storybook/stories/access/private-tasks/05-projects.stories.tsx) | `access/resources`, `tasks/properties`, `projects/lifecycle` | unreviewed / production / fixtures | 19 / 12 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Getting started and access/Login, invitations, and access/Private tasks/06 Full product pages](../../ui/storybook/stories/access/private-tasks/06-full-product-pages.stories.tsx) | `access/resources`, `tasks/properties`, `projects/lifecycle` | unreviewed / production / fixtures | 21 / 12 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Getting started and access/Login, invitations, and access/Private tasks/07 User stories](../../ui/storybook/stories/access/private-tasks/07-user-stories.stories.tsx) | `access/resources`, `tasks/properties` | unreviewed / production / fixtures | 13 / 7 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
|
||||
## activity
|
||||
|
||||
[Feature recipe](../feature-map/activity.md)
|
||||
|
||||
| Story suite | Capabilities | Product / implementation / data | Stories / play functions | Review note |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| [Oversight and operation/Dashboards and audit trails/Tools/Activity (audit feed)](../../ui/storybook/stories/activity/activity-feed.stories.tsx) | `activity/activity`, `gateways-profiles/activity` | unreviewed / production / fixtures | 4 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Oversight and operation/Dashboards and audit trails/Apps/App detail · Activity (lifecycle)](../../ui/storybook/stories/activity/app-activity-lifecycle.stories.tsx) | `activity/activity`, `gateways-profiles/activity` | unreviewed / production / fixtures | 4 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Oversight and operation/Dashboards and audit trails/Product/Data Visualization & Misc](../../ui/storybook/stories/activity/data-viz-misc.stories.tsx) | `activity/overview` | unreviewed / production / fixtures | 21 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Runner display/default/effort and matching-discovery source changes reviewed; existing fixture, proposal and historical scope retained. No new rendering, play, backend or provider qualification is claimed. |
|
||||
| [Oversight and operation/Dashboards and audit trails/Pages/Work Timeline](../../ui/storybook/stories/activity/work-timeline.stories.tsx) | `activity/timeline` | unreviewed / production / fixtures | 4 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
|
||||
## agent-chat
|
||||
|
||||
[Feature recipe](../feature-map/agent-chat.md)
|
||||
|
||||
| Story suite | Capabilities | Product / implementation / data | Stories / play functions | Review note |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| [Tasks and conversations/Agent conversations and project handoff/Design explorations/Agent chat sidebar/Pages](../../ui/storybook/stories/agent-chat/agent-chat-sidebar.stories.tsx) | `agent-chat/discovery`, `agent-chat/history` | current / production / fixtures | 9 / 1 | Wrapper mounts production Layout and AgentChat; conversation selection and sends use in-memory routes. Directory name is not prototype evidence. Runner display/default/effort and matching-discovery source changes reviewed; existing fixture, proposal and historical scope retained. No new rendering, play, backend or provider qualification is claimed. |
|
||||
| [Tasks and conversations/Agent conversations and project handoff/Design explorations/Agent chat](../../ui/storybook/stories/agent-chat/agent-chat.stories.tsx) | `agent-chat/discovery`, `agent-chat/history` | mixed / mixed / fixtures | 17 / 0 | Production conversation/task routes with a local ChatEntryLanding. Default-agent behavior and fallback API responses remain outside current-product parity. Runner display/default/effort and matching-discovery source changes reviewed; existing fixture, proposal and historical scope retained. No new rendering, play, backend or provider qualification is claimed. |
|
||||
| [Tasks and conversations/Agent conversations and project handoff/Design explorations/Agent chat sidebar/Components](../../ui/storybook/stories/agent-chat/agent-conversation-sidebar.stories.tsx) | `agent-chat/discovery`, `agent-chat/history` | current / production / fixtures | 10 / 1 | Support module re-exports production AgentConversationSidebar; search/selection use fixture callbacks. |
|
||||
| [Tasks and conversations/Agent conversations and project handoff/Design explorations/Chat entry](../../ui/storybook/stories/agent-chat/chat-entry.stories.tsx) | `agent-chat/discovery`, `agent-chat/history` | historical / mixed / fixtures | 9 / 3 | Retained first-created-agent sidebar/landing design predates personal primary-agent selection. Shared production conversation components remain useful. Runner display/default/effort and matching-discovery source changes reviewed; existing fixture, proposal and historical scope retained. No new rendering, play, backend or provider qualification is claimed. |
|
||||
|
||||
## agents
|
||||
|
||||
[Feature recipe](../feature-map/agents.md)
|
||||
|
||||
| Story suite | Capabilities | Product / implementation / data | Stories / play functions | Review note |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| [Agents and reusable capabilities/Hiring, configuration, and organization/Pages/Agent Detail](../../ui/storybook/stories/agents/agent-detail.stories.tsx) | `agents/lifecycle`, `agents/instructions` | unreviewed / production / fixtures | 1 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Runner-default recipe text reviewed; rendered subjects and fixture contracts are unchanged by that documentation update. |
|
||||
| [Agents and reusable capabilities/Hiring, configuration, and organization/Agents/Persistent files](../../ui/storybook/stories/agents/agent-files.stories.tsx) | `agents/instructions` | unreviewed / production / fixtures | 9 / 7 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Runner-default recipe text reviewed; rendered subjects and fixture contracts are unchanged by that documentation update. |
|
||||
| [Agents and reusable capabilities/Hiring, configuration, and organization/Product/Agent Management](../../ui/storybook/stories/agents/agent-management.stories.tsx) | `agents/lifecycle`, `agents/instructions` | unreviewed / production / fixtures | 3 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Runner display/default/effort and matching-discovery source changes reviewed; existing fixture, proposal and historical scope retained. No new rendering, play, backend or provider qualification is claimed. |
|
||||
| [Agents and reusable capabilities/Hiring, configuration, and organization/Agents/Personas/Full pages](../../ui/storybook/stories/agents/agent-persona-pages.stories.tsx) | `agents/organization`, `navigation-preferences/profile` | unreviewed / production / fixtures | 6 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Runner display/default/effort and matching-discovery source changes reviewed; existing fixture, proposal and historical scope retained. No new rendering, play, backend or provider qualification is claimed. |
|
||||
| [Agents and reusable capabilities/Hiring, configuration, and organization/Agents/Personas](../../ui/storybook/stories/agents/agent-personas.stories.tsx) | `agents/organization`, `navigation-preferences/profile` | unreviewed / production / fixtures | 19 / 1 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Runner-default recipe text reviewed; rendered subjects and fixture contracts are unchanged by that documentation update. |
|
||||
| [Agents and reusable capabilities/Hiring, configuration, and organization/Agents/Configuration refresh](../../ui/storybook/stories/agents/agent-settings-redesign.stories.tsx) | `agents/lifecycle`, `agents/instructions` | mixed / mixed / fixtures | 20 / 1 | Production configuration panels in a locally composed settings shell; full AgentDetail route parity remains unreviewed. Runner display/default/effort and matching-discovery source changes reviewed; existing fixture, proposal and historical scope retained. No new rendering, play, backend or provider qualification is claimed. |
|
||||
| [Agents and reusable capabilities/Hiring, configuration, and organization/Product/Built-in Agents](../../ui/storybook/stories/agents/built-in-agents.stories.tsx) | `agents/built-in` | unreviewed / production / fixtures | 4 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Runner display/default/effort and matching-discovery source changes reviewed; existing fixture, proposal and historical scope retained. No new rendering, play, backend or provider qualification is claimed. |
|
||||
| [Agents and reusable capabilities/Hiring, configuration, and organization/Codex runner/Creation](../../ui/storybook/stories/agents/codex-runner-creation.stories.tsx) | `agents/hire`, `runs-adapters/availability`, `runs-adapters/validation` | unreviewed / production / fixtures | 8 / 6 | Production NewAgent and AgentBasicsDialog with an isolated fetch boundary. Native/Legacy replies and save mutations are simulated; source inspection does not establish all play cases or provider/default qualification. Existing eight story IDs retained through explicit metadata and feature placement. Native adapter inventory is not duplicated when the shared preview provides it. |
|
||||
| [Agents and reusable capabilities/Hiring, configuration, and organization/Onboarding/External agent invitation/Components](../../ui/storybook/stories/agents/external-agent-invite/components.stories.tsx) | `agents/hire`, `access/invitations` | unreviewed / production / fixtures | 8 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Runner-default recipe text reviewed; rendered subjects and fixture contracts are unchanged by that documentation update. |
|
||||
| [Agents and reusable capabilities/Hiring, configuration, and organization/Onboarding/External agent invitation/Journeys](../../ui/storybook/stories/agents/external-agent-invite/journeys.stories.tsx) | `agents/hire`, `access/invitations` | unreviewed / production / fixtures | 20 / 4 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Runner display/default/effort and matching-discovery source changes reviewed; existing fixture, proposal and historical scope retained. No new rendering, play, backend or provider qualification is claimed. |
|
||||
| [Agents and reusable capabilities/Hiring, configuration, and organization/Onboarding/New agent](../../ui/storybook/stories/agents/new-agent-onboarding.stories.tsx) | `agents/hire` | historical / prototype / fixtures | 37 / 1 | Retained local wizard predates production ui/src/components/new-agent flow; shared fields do not make the wizard current. Runner display/default/effort and matching-discovery source changes reviewed; existing fixture, proposal and historical scope retained. No new rendering, play, backend or provider qualification is claimed. |
|
||||
| [Agents and reusable capabilities/Hiring, configuration, and organization/Primary Agent/00 Review guide](../../ui/storybook/stories/agents/primary-agent/00-review-guide.stories.tsx) | `agents/organization`, `tasks/create`, `agent-chat/discovery` | unreviewed / guide / static | 1 / 0 | Review guide retained; explanatory content does not establish runtime or production coverage. Runner-default recipe text reviewed; rendered subjects and fixture contracts are unchanged by that documentation update. |
|
||||
| [Agents and reusable capabilities/Hiring, configuration, and organization/Primary Agent/01 Components](../../ui/storybook/stories/agents/primary-agent/01-components.stories.tsx) | `agents/organization`, `tasks/create`, `agent-chat/discovery` | unreviewed / production / fixtures | 5 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Runner-default recipe text reviewed; rendered subjects and fixture contracts are unchanged by that documentation update. |
|
||||
| [Agents and reusable capabilities/Hiring, configuration, and organization/Primary Agent/02 Sidebar](../../ui/storybook/stories/agents/primary-agent/02-sidebar.stories.tsx) | `agents/organization`, `tasks/create`, `agent-chat/discovery` | unreviewed / production / fixtures | 12 / 1 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Runner-default recipe text reviewed; rendered subjects and fixture contracts are unchanged by that documentation update. |
|
||||
| [Agents and reusable capabilities/Hiring, configuration, and organization/Primary Agent/03 Agents](../../ui/storybook/stories/agents/primary-agent/03-agents.stories.tsx) | `agents/organization`, `tasks/create`, `agent-chat/discovery` | unreviewed / production / fixtures | 14 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Runner-default recipe text reviewed; rendered subjects and fixture contracts are unchanged by that documentation update. |
|
||||
| [Agents and reusable capabilities/Hiring, configuration, and organization/Primary Agent/04 New task](../../ui/storybook/stories/agents/primary-agent/04-new-task.stories.tsx) | `agents/organization`, `tasks/create`, `agent-chat/discovery` | unreviewed / production / fixtures | 9 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Runner-default recipe text reviewed; rendered subjects and fixture contracts are unchanged by that documentation update. |
|
||||
| [Agents and reusable capabilities/Hiring, configuration, and organization/Primary Agent/05 Chat](../../ui/storybook/stories/agents/primary-agent/05-chat.stories.tsx) | `agents/organization`, `tasks/create`, `agent-chat/discovery` | unreviewed / production / fixtures | 8 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Runner-default recipe text reviewed; rendered subjects and fixture contracts are unchanged by that documentation update. |
|
||||
| [Agents and reusable capabilities/Hiring, configuration, and organization/Primary Agent/06 Journeys](../../ui/storybook/stories/agents/primary-agent/06-journeys.stories.tsx) | `agents/organization`, `tasks/create`, `agent-chat/discovery` | unreviewed / production / fixtures | 11 / 11 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Runner-default recipe text reviewed; rendered subjects and fixture contracts are unchanged by that documentation update. |
|
||||
| [Agents and reusable capabilities/Hiring, configuration, and organization/Primary Agent/07 Confirmation](../../ui/storybook/stories/agents/primary-agent/07-confirmation.stories.tsx) | `agents/organization`, `tasks/create`, `agent-chat/discovery` | unreviewed / production / fixtures | 4 / 1 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Runner-default recipe text reviewed; rendered subjects and fixture contracts are unchanged by that documentation update. |
|
||||
| [Agents and reusable capabilities/Hiring, configuration, and organization/Summaries/SummarySlotCard](../../ui/storybook/stories/agents/summary-slot-card.stories.tsx) | `agents/built-in` | unreviewed / production / fixtures | 15 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Runner-default recipe text reviewed; rendered subjects and fixture contracts are unchanged by that documentation update. |
|
||||
|
||||
## app-permissions
|
||||
|
||||
[Feature recipe](../feature-map/app-permissions.md)
|
||||
|
||||
| Story suite | Capabilities | Product / implementation / data | Stories / play functions | Review note |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| [Apps, channels, and extensions/App access and action permissions/Tools & Access/ActionCard](../../ui/storybook/stories/app-permissions/action-card.stories.tsx) | `app-permissions/test-action`, `app-permissions/action-policy` | unreviewed / production / fixtures | 5 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Apps, channels, and extensions/App access and action permissions/Apps/Action permissions test](../../ui/storybook/stories/app-permissions/action-permissions-test.stories.tsx) | `app-permissions/test-action`, `app-permissions/action-policy` | unreviewed / production / fixtures | 7 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Apps, channels, and extensions/App access and action permissions/Apps/Managed accounts](../../ui/storybook/stories/app-permissions/apps-managed-accounts.stories.tsx) | `app-permissions/account-maintenance` | current / production / fixture-server | 1 / 0 | Production Browse requires owned tests/aggregator-accounts/test-drive.ts on localhost:4310; providers are simulated. Not an offline story. |
|
||||
| [Apps, channels, and extensions/App access and action permissions/Tools/Permitted vs Installed](../../ui/storybook/stories/app-permissions/permitted-vs-installed.stories.tsx) | `app-permissions/agent-access`, `app-permissions/action-policy` | unreviewed / production / fixtures | 12 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
|
||||
## budgets-costs
|
||||
|
||||
[Feature recipe](../feature-map/budgets-costs.md)
|
||||
|
||||
| Story suite | Capabilities | Product / implementation / data | Stories / play functions | Review note |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| [Oversight and operation/Costs and budgets/Product/Budget & Finance](../../ui/storybook/stories/budgets-costs/budget-finance.stories.tsx) | `budgets-costs/cost-reporting`, `budgets-costs/budgets` | unreviewed / production / fixtures | 1 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Oversight and operation/Costs and budgets/Costs/Subscriptions](../../ui/storybook/stories/budgets-costs/costs/subscriptions.stories.tsx) | `budgets-costs/cost-reporting`, `budgets-costs/budgets` | unreviewed / production / fixtures | 7 / 4 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
|
||||
## chat-channels
|
||||
|
||||
[Feature recipe](../feature-map/chat-channels.md)
|
||||
|
||||
| Story suite | Capabilities | Product / implementation / data | Stories / play functions | Review note |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| [Apps, channels, and extensions/External chat and email/Connections/AgentMail setup](../../ui/storybook/stories/chat-channels/agentmail-onboarding.stories.tsx) | `chat-channels/providers`, `connection-setup/channel-setup` | unreviewed / production / fixtures | 19 / 16 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Apps, channels, and extensions/External chat and email/Connections/AgentMail settings](../../ui/storybook/stories/chat-channels/agentmail-settings.stories.tsx) | `chat-channels/lifecycle` | unreviewed / production / fixtures | 8 / 5 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Apps, channels, and extensions/External chat and email/Connections/AgentMail tasks](../../ui/storybook/stories/chat-channels/agentmail.stories.tsx) | `chat-channels/conversation` | unreviewed / prototype / fixtures | 9 / 2 | Local EmailTaskExperience combines production composer/bubbles with simulated email lifecycle; compare against IssueDetail before claiming page parity. |
|
||||
| [Apps, channels, and extensions/External chat and email/Connections/Chat connection actions](../../ui/storybook/stories/chat-channels/chat-connection-actions.stories.tsx) | `chat-channels/providers` | unreviewed / production / fixtures | 1 / 1 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Apps, channels, and extensions/External chat and email/Connections/Chat management](../../ui/storybook/stories/chat-channels/chat-management/chat-management.stories.tsx) | `chat-channels/lifecycle` | unreviewed / production / fixtures | 15 / 6 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Apps, channels, and extensions/External chat and email/Connectors/GitHub bot management](../../ui/storybook/stories/chat-channels/github-bot-management/management.stories.tsx) | `chat-channels/providers`, `chat-channels/conversation` | unreviewed / production / fixtures | 33 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Runner display/default/effort and matching-discovery source changes reviewed; existing fixture, proposal and historical scope retained. No new rendering, play, backend or provider qualification is claimed. |
|
||||
| [Apps, channels, and extensions/External chat and email/Historical/GitHub chat prototypes](../../ui/storybook/stories/chat-channels/github-chat.stories.tsx) | `chat-channels/providers`, `chat-channels/conversation` | historical / prototype / fixtures | 48 / 9 | Historical wizard retained for original setup/access/review design; current GitHub setup is ui/src/pages/apps/chat/GitHubChatSetup.tsx. |
|
||||
| [Apps, channels, and extensions/External chat and email/Connections/GitHub and Code Review Bot](../../ui/storybook/stories/chat-channels/github-connections.stories.tsx) | `chat-channels/providers`, `connection-setup/channel-setup` | unreviewed / production / fixtures | 7 / 7 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Apps, channels, and extensions/External chat and email/Connections/iMessage Photon](../../ui/storybook/stories/chat-channels/imessage-photon.stories.tsx) | `chat-channels/conversation` | unreviewed / production / fixtures | 13 / 7 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Apps, channels, and extensions/External chat and email/Connections/Slack/Automatic setup](../../ui/storybook/stories/chat-channels/slack-automatic-setup.stories.tsx) | `chat-channels/providers`, `connection-setup/channel-setup` | unreviewed / production / fixtures | 18 / 3 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Apps, channels, and extensions/External chat and email/Connections/Slack/Avatar in Settings](../../ui/storybook/stories/chat-channels/slack-avatar-settings.stories.tsx) | `chat-channels/identity` | current / production / fixtures | 1 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Apps, channels, and extensions/External chat and email/Connections/Slack/Communication in Settings](../../ui/storybook/stories/chat-channels/slack-communication-settings.stories.tsx) | `chat-channels/identity` | current / production / fixtures | 4 / 1 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Apps, channels, and extensions/External chat and email/Connections/Slack/Managed setup](../../ui/storybook/stories/chat-channels/slack-managed-setup.stories.tsx) | `chat-channels/providers`, `connection-setup/channel-setup` | unreviewed / production / fixtures | 21 / 7 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Apps, channels, and extensions/External chat and email/Connections/Slack/Task tools](../../ui/storybook/stories/chat-channels/slack-tools.stories.tsx) | `chat-channels/conversation` | unreviewed / production / fixtures | 5 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
|
||||
## companies
|
||||
|
||||
[Feature recipe](../feature-map/companies.md)
|
||||
|
||||
| Story suite | Capabilities | Product / implementation / data | Stories / play functions | Review note |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| [Getting started and access/Companies and portability/Decision models/01 Settings](../../ui/storybook/stories/companies/decision-models/01-settings.stories.tsx) | `companies/identity`, `budgets-costs/cost-reporting` | unreviewed / production / fixtures | 11 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Getting started and access/Companies and portability/Decision models/02 History](../../ui/storybook/stories/companies/decision-models/02-history.stories.tsx) | `companies/identity`, `budgets-costs/cost-reporting` | unreviewed / production / fixtures | 4 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Getting started and access/Companies and portability/Decision models/03 Journey](../../ui/storybook/stories/companies/decision-models/03-journey.stories.tsx) | `companies/identity`, `budgets-costs/cost-reporting` | unreviewed / production / fixtures | 3 / 1 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Getting started and access/Companies and portability/Fast responses/01 Settings](../../ui/storybook/stories/companies/fast-responses/01-settings.stories.tsx) | `companies/identity`, `budgets-costs/cost-reporting` | unreviewed / production / fixtures | 9 / 0 | Production settings view with simulated callbacks. Excluded exported fixture helpers and added the onConnectionChange spy required during rendering; full save/API state parity remains unreviewed. |
|
||||
| [Getting started and access/Companies and portability/Fast responses/02 History](../../ui/storybook/stories/companies/fast-responses/02-history.stories.tsx) | `companies/identity`, `budgets-costs/cost-reporting` | current / production / fixtures | 4 / 0 | Shared production DecisionHistoryTable receives fastResponse=true, matching FastResponseHistory presentation; API loading/error and feature-gate wrapper are outside this story. |
|
||||
|
||||
## connection-setup
|
||||
|
||||
[Feature recipe](../feature-map/connection-setup.md)
|
||||
|
||||
| Story suite | Capabilities | Product / implementation / data | Stories / play functions | Review note |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| [Apps, channels, and extensions/Connection setup/Components/Agent setup prompt](../../ui/storybook/stories/connection-setup/agent-setup-prompt.stories.tsx) | `connection-setup/channel-setup`, `gateways-profiles/client-connect` | unreviewed / production / fixtures | 11 / 3 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Apps, channels, and extensions/Connection setup/Apps/Canonical icon registry](../../ui/storybook/stories/connection-setup/AppLogoRegistry.stories.tsx) | `connection-setup/catalog` | unreviewed / production / fixtures | 2 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Apps, channels, and extensions/Connection setup/Apps/Catalog source selection](../../ui/storybook/stories/connection-setup/apps-catalog-source-selection.stories.tsx) | `connection-setup/catalog` | proposed / prototype / fixtures | 12 / 1 | Local catalog composition uses shipped cards and filters; retains design exploration purpose, not acceptance of the Apps route. |
|
||||
| [Apps, channels, and extensions/Connection setup/Apps/Connect discoverability (PAP-11091)](../../ui/storybook/stories/connection-setup/apps-connect-discoverability.stories.tsx) | `connection-setup/catalog` | unreviewed / production / fixtures | 5 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Apps, channels, and extensions/Connection setup/Apps/Connect name at create time (PAP-11283)](../../ui/storybook/stories/connection-setup/apps-connect-name.stories.tsx) | `connection-setup/identity-and-access` | unreviewed / production / fixtures | 2 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Apps, channels, and extensions/Connection setup/Apps/Connections/Arcade](../../ui/storybook/stories/connection-setup/ArcadeConnection.stories.tsx) | `connection-setup/credentials`, `connection-setup/failure` | unreviewed / production / fixtures | 26 / 1 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Apps, channels, and extensions/Connection setup/Connections/Browser Use Cloud/Settings](../../ui/storybook/stories/connection-setup/browser-use-settings.stories.tsx) | `connection-setup/credentials` | current / production / fixtures | 9 / 2 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Apps, channels, and extensions/Connection setup/Apps/Connections/Composio](../../ui/storybook/stories/connection-setup/ComposioConnection.stories.tsx) | `connection-setup/credentials`, `connection-setup/failure` | unreviewed / production / fixtures | 27 / 1 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Apps, channels, and extensions/Connection setup/Design explorations/Connections/Agent instructions](../../ui/storybook/stories/connection-setup/connection-instructions.stories.tsx) | `connection-setup/credentials`, `agents/instructions` | unreviewed / production / fixtures | 20 / 5 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Runner-default recipe text reviewed; rendered subjects and fixture contracts are unchanged by that documentation update. |
|
||||
| [Apps, channels, and extensions/Connection setup/Apps/Connections/Provider choice](../../ui/storybook/stories/connection-setup/ConnectionProviderChoice.stories.tsx) | `connection-setup/identity-and-access` | unreviewed / production / fixtures | 7 / 4 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Apps, channels, and extensions/Connection setup/Apps/Enterpret connector](../../ui/storybook/stories/connection-setup/enterpret-connect-flow.stories.tsx) | `connection-setup/credentials`, `connection-setup/failure` | unreviewed / production / fixtures | 9 / 1 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Apps, channels, and extensions/Connection setup/Apps/Connections/Executor](../../ui/storybook/stories/connection-setup/ExecutorConnection.stories.tsx) | `connection-setup/credentials`, `connection-setup/failure` | unreviewed / production / fixtures | 26 / 1 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Apps, channels, and extensions/Connection setup/PR reviews/Fireflies and app webhooks](../../ui/storybook/stories/connection-setup/fireflies-pr.stories.tsx) | `connection-setup/credentials`, `routines/triggers` | unreviewed / production / fixtures | 27 / 5 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Apps, channels, and extensions/Connection setup/Connections/In-task connections](../../ui/storybook/stories/connection-setup/in-feed-connections.stories.tsx) | `connection-setup/task-request` | unreviewed / production / fixtures | 78 / 23 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Apps, channels, and extensions/Connection setup/Apps/Managed Cloud OAuth handoff](../../ui/storybook/stories/connection-setup/managed-cloud-oauth-handoff.stories.tsx) | `connection-setup/handoff` | unreviewed / production / fixtures | 4 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Apps, channels, and extensions/Connection setup/Apps/Connections/Memory](../../ui/storybook/stories/connection-setup/memory-connectors.stories.tsx) | `connection-setup/credentials`, `agents/instructions` | unreviewed / production / fixtures | 21 / 7 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Runner-default recipe text reviewed; rendered subjects and fixture contracts are unchanged by that documentation update. |
|
||||
| [Apps, channels, and extensions/Connection setup/Apps/Notion MCP connect flow (PAP-16650)](../../ui/storybook/stories/connection-setup/notion-connect-flow.stories.tsx) | `connection-setup/credentials`, `connection-setup/failure` | unreviewed / production / fixtures | 7 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Apps, channels, and extensions/Connection setup/Reviews/PAP-17835 Personal connection identity](../../ui/storybook/stories/connection-setup/personal-connection-identity.stories.tsx) | `connection-setup/identity-and-access` | unreviewed / production / fixtures | 10 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Apps, channels, and extensions/Connection setup/Apps/Connections/Zapier](../../ui/storybook/stories/connection-setup/ZapierConnection.stories.tsx) | `connection-setup/credentials`, `connection-setup/failure` | unreviewed / production / fixtures | 21 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
|
||||
## developer-labs
|
||||
|
||||
[Feature recipe](../feature-map/developer-labs.md)
|
||||
|
||||
| Story suite | Capabilities | Product / implementation / data | Stories / play functions | Review note |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| [Contributor surfaces/Developer previews and diagnostic labs/UX Labs/Converted Test Pages](../../ui/storybook/stories/developer-labs/ux-labs.stories.tsx) | `developer-labs/interaction-labs` | unreviewed / production / fixtures | 4 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
|
||||
## documents-artifacts
|
||||
|
||||
[Feature recipe](../feature-map/documents-artifacts.md)
|
||||
|
||||
| Story suite | Capabilities | Product / implementation / data | Stories / play functions | Review note |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| [Tasks and conversations/Documents, attachments, and work products/Explorations/Artifact cards/Commit](../../ui/storybook/stories/documents-artifacts/artifact-cards/commit.stories.tsx) | `documents-artifacts/work-products` | unreviewed / production / fixtures | 1 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Tasks and conversations/Documents, attachments, and work products/Explorations/Artifact cards/Data](../../ui/storybook/stories/documents-artifacts/artifact-cards/data.stories.tsx) | `documents-artifacts/work-products` | unreviewed / production / fixtures | 1 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Tasks and conversations/Documents, attachments, and work products/Explorations/Artifact cards/Document](../../ui/storybook/stories/documents-artifacts/artifact-cards/document.stories.tsx) | `documents-artifacts/work-products` | unreviewed / production / fixtures | 1 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Tasks and conversations/Documents, attachments, and work products/Explorations/Artifact cards/File](../../ui/storybook/stories/documents-artifacts/artifact-cards/file.stories.tsx) | `documents-artifacts/work-products` | unreviewed / production / fixtures | 1 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Tasks and conversations/Documents, attachments, and work products/Explorations/Artifact cards/Image](../../ui/storybook/stories/documents-artifacts/artifact-cards/image.stories.tsx) | `documents-artifacts/work-products` | unreviewed / production / fixtures | 1 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Tasks and conversations/Documents, attachments, and work products/Explorations/Artifact cards/Link preview](../../ui/storybook/stories/documents-artifacts/artifact-cards/link-preview.stories.tsx) | `documents-artifacts/work-products` | unreviewed / production / fixtures | 1 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Tasks and conversations/Documents, attachments, and work products/Explorations/Artifact cards/Pull request](../../ui/storybook/stories/documents-artifacts/artifact-cards/pull-request.stories.tsx) | `documents-artifacts/work-products` | unreviewed / production / fixtures | 1 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Tasks and conversations/Documents, attachments, and work products/Explorations/Artifact cards/Video](../../ui/storybook/stories/documents-artifacts/artifact-cards/video.stories.tsx) | `documents-artifacts/work-products` | unreviewed / production / fixtures | 1 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Tasks and conversations/Documents, attachments, and work products/Artifact upload/01 Artifacts pane](../../ui/storybook/stories/documents-artifacts/artifact-upload/01-components.stories.tsx) | `documents-artifacts/attachments` | unreviewed / production / fixtures | 10 / 3 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Tasks and conversations/Documents, attachments, and work products/Artifact upload/02 Paused task journey](../../ui/storybook/stories/documents-artifacts/artifact-upload/02-task-journey.stories.tsx) | `documents-artifacts/attachments` | unreviewed / production / fixtures | 6 / 1 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Tasks and conversations/Documents, attachments, and work products/Pages/Artifacts](../../ui/storybook/stories/documents-artifacts/artifacts.stories.tsx) | `documents-artifacts/library`, `documents-artifacts/work-products` | unreviewed / production / fixtures | 6 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Tasks and conversations/Documents, attachments, and work products/Components/CSV File Tab](../../ui/storybook/stories/documents-artifacts/csv-preview.stories.tsx) | `documents-artifacts/attachments` | unreviewed / production / fixtures | 4 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Tasks and conversations/Documents, attachments, and work products/Product/Documents/Annotations](../../ui/storybook/stories/documents-artifacts/document-annotations.stories.tsx) | `documents-artifacts/annotations` | unreviewed / production / fixtures | 9 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Tasks and conversations/Documents, attachments, and work products/Product/Document Comments](../../ui/storybook/stories/documents-artifacts/document-comments.stories.tsx) | `documents-artifacts/annotations` | unreviewed / production / fixtures | 1 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Tasks and conversations/Documents, attachments, and work products/Components/File Viewer Sheet](../../ui/storybook/stories/documents-artifacts/file-viewer.stories.tsx) | `documents-artifacts/attachments` | unreviewed / production / fixtures | 13 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Tasks and conversations/Documents, attachments, and work products/HTML artifacts/01 Preview](../../ui/storybook/stories/documents-artifacts/html-artifacts/01-preview.stories.tsx) | `documents-artifacts/attachments`, `workspaces/files-git` | unreviewed / production / fixtures | 5 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Tasks and conversations/Documents, attachments, and work products/HTML artifacts/02 Workspace](../../ui/storybook/stories/documents-artifacts/html-artifacts/02-workspace.stories.tsx) | `documents-artifacts/attachments`, `workspaces/files-git` | unreviewed / production / fixtures | 3 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Tasks and conversations/Documents, attachments, and work products/HTML artifacts/03 Task journey](../../ui/storybook/stories/documents-artifacts/html-artifacts/03-task-journey.stories.tsx) | `documents-artifacts/attachments`, `workspaces/files-git` | unreviewed / production / fixtures | 3 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Tasks and conversations/Documents, attachments, and work products/Task Chat/Rich Work Product Cards](../../ui/storybook/stories/documents-artifacts/rich-work-product-cards.stories.tsx) | `documents-artifacts/work-products` | unreviewed / production / fixtures | 9 / 3 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Tasks and conversations/Documents, attachments, and work products/Tasks/Artifact Gallery](../../ui/storybook/stories/documents-artifacts/task-artifact-gallery.stories.tsx) | `documents-artifacts/library`, `documents-artifacts/work-products` | unreviewed / production / fixtures | 8 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Tasks and conversations/Documents, attachments, and work products/Tasks/Text file tabs](../../ui/storybook/stories/documents-artifacts/task-text-files.stories.tsx) | `documents-artifacts/attachments` | unreviewed / production / fixtures | 7 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
|
||||
## foundations-and-guides
|
||||
|
||||
| Story suite | Capabilities | Product / implementation / data | Stories / play functions | Review note |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| [Foundations/Components/Forms & Editors](../../ui/storybook/stories/foundations/forms-editors.stories.tsx) | Shared foundations or contributor guide; no single business capability owns this suite. | unreviewed / production / fixtures | 3 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Foundations/Primitive Matrix](../../ui/storybook/stories/foundations/foundations.stories.tsx) | Shared foundations or contributor guide; no single business capability owns this suite. | unreviewed / production / fixtures | 1 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Runner display/default/effort and matching-discovery source changes reviewed; existing fixture, proposal and historical scope retained. No new rendering, play, backend or provider qualification is claimed. |
|
||||
| [Foundations/Primitive Coverage](../../ui/storybook/stories/foundations/primitives-coverage.stories.tsx) | Shared foundations or contributor guide; no single business capability owns this suite. | unreviewed / production / fixtures | 11 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Foundations/Status Language](../../ui/storybook/stories/foundations/status-language.stories.tsx) | Shared foundations or contributor guide; no single business capability owns this suite. | unreviewed / production / fixtures | 1 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Overview/Storybook Guide](../../ui/storybook/stories/overview/overview.stories.tsx) | Shared foundations or contributor guide; no single business capability owns this suite. | unreviewed / guide / static | 1 / 0 | Generated inventory counts replace stale hand-maintained Planned/Covered claims; no execution coverage is implied. |
|
||||
|
||||
## gateways-profiles
|
||||
|
||||
[Feature recipe](../feature-map/gateways-profiles.md)
|
||||
|
||||
| Story suite | Capabilities | Product / implementation / data | Stories / play functions | Review note |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| [Apps, channels, and extensions/Tool gateways and access profiles/Tools/Access profiles](../../ui/storybook/stories/gateways-profiles/access-profiles.stories.tsx) | `gateways-profiles/profiles` | unreviewed / production / fixtures | 18 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Apps, channels, and extensions/Tool gateways and access profiles/Tools/Gateways (PAP-11182)](../../ui/storybook/stories/gateways-profiles/gateways-tab.stories.tsx) | `gateways-profiles/gateway` | unreviewed / production / fixtures | 3 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Apps, channels, and extensions/Tool gateways and access profiles/Assistant connections/Manage connections](../../ui/storybook/stories/gateways-profiles/public-mcp-connections.stories.tsx) | `gateways-profiles/client-connect` | current / production / fixtures | 9 / 4 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Apps, channels, and extensions/Tool gateways and access profiles/Assistant connections/Consent](../../ui/storybook/stories/gateways-profiles/public-mcp-consent.stories.tsx) | `gateways-profiles/client-connect` | current / production / fixtures | 25 / 14 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Apps, channels, and extensions/Tool gateways and access profiles/Assistant connections/Device authorization](../../ui/storybook/stories/gateways-profiles/public-mcp-device.stories.tsx) | `gateways-profiles/client-connect` | current / production / fixtures | 7 / 3 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Apps, channels, and extensions/Tool gateways and access profiles/Assistant connections/Start here](../../ui/storybook/stories/gateways-profiles/public-mcp-journey.stories.tsx) | `gateways-profiles/client-connect` | unreviewed / production / fixtures | 14 / 4 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Apps, channels, and extensions/Tool gateways and access profiles/Assistant connections/Experimental setting](../../ui/storybook/stories/gateways-profiles/public-mcp-setting.stories.tsx) | `gateways-profiles/client-connect` | unreviewed / production / fixtures | 8 / 5 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
|
||||
## inbox-search
|
||||
|
||||
[Feature recipe](../feature-map/inbox-search.md)
|
||||
|
||||
| Story suite | Capabilities | Product / implementation / data | Stories / play functions | Review note |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| [Tasks and conversations/Inbox, decisions, and search/Product/Inbox/Blocked tab](../../ui/storybook/stories/inbox-search/blocked-inbox.stories.tsx) | `inbox-search/triage`, `recovery/diagnosis` | unreviewed / production / fixtures | 5 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Tasks and conversations/Inbox, decisions, and search/Decisions/DecisionCard](../../ui/storybook/stories/inbox-search/decision-card.stories.tsx) | `inbox-search/decisions` | unreviewed / production / fixtures | 9 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Tasks and conversations/Inbox, decisions, and search/Pages/Decisions Desk](../../ui/storybook/stories/inbox-search/decisions-desk.stories.tsx) | `inbox-search/decisions` | unreviewed / production / fixtures | 6 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Tasks and conversations/Inbox, decisions, and search/Product/Search & Command K](../../ui/storybook/stories/inbox-search/search.stories.tsx) | `inbox-search/search` | unreviewed / prototype / fixtures | 1 / 0 | Local search-page composition mounts production result components; does not establish parity with the production Search route. |
|
||||
| [Tasks and conversations/Inbox, decisions, and search/Pages/Decisions](../../ui/storybook/stories/inbox-search/what-needs-me.stories.tsx) | `inbox-search/decisions` | unreviewed / production / fixtures | 13 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
|
||||
## instance-operations
|
||||
|
||||
[Feature recipe](../feature-map/instance-operations.md)
|
||||
|
||||
| Story suite | Capabilities | Product / implementation / data | Stories / play functions | Review note |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| [Oversight and operation/Instance operations/Product/Dev Ops Surfaces](../../ui/storybook/stories/instance-operations/dev-ops-surfaces.stories.tsx) | `instance-operations/server-service` | unreviewed / production / fixtures | 1 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
|
||||
## navigation-preferences
|
||||
|
||||
[Feature recipe](../feature-map/navigation-preferences.md)
|
||||
|
||||
| Story suite | Capabilities | Product / implementation / data | Stories / play functions | Review note |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| [Oversight and operation/Navigation, profile, and announcements/Announcements/AnnouncementCard](../../ui/storybook/stories/navigation-preferences/announcement-card.stories.tsx) | `navigation-preferences/announcements` | unreviewed / production / fixtures | 11 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Oversight and operation/Navigation, profile, and announcements/Prototypes/Task detail/Mobile tabs](../../ui/storybook/stories/navigation-preferences/mobile-task-tabs.stories.tsx) | `navigation-preferences/navigation` | proposed / prototype / fixtures | 4 / 2 | Local composition retained for its original design purpose; full production parity remains unreviewed. |
|
||||
| [Oversight and operation/Navigation, profile, and announcements/Product/Navigation & Layout](../../ui/storybook/stories/navigation-preferences/navigation-layout.stories.tsx) | `navigation-preferences/navigation` | unreviewed / production / fixtures | 2 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
|
||||
## onboarding
|
||||
|
||||
[Feature recipe](../feature-map/onboarding.md)
|
||||
|
||||
| Story suite | Capabilities | Product / implementation / data | Stories / play functions | Review note |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| [Getting started and access/Onboarding and first work/Onboarding/Agent arc](../../ui/storybook/stories/onboarding/onboarding-agent-arc.stories.tsx) | `onboarding/adapter`, `onboarding/first-task` | unreviewed / production / fixtures | 27 / 11 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Runner display/default/effort and matching-discovery source changes reviewed; existing fixture, proposal and historical scope retained. No new rendering, play, backend or provider qualification is claimed. |
|
||||
| [Getting started and access/Onboarding and first work/Onboarding/Character](../../ui/storybook/stories/onboarding/onboarding-character.stories.tsx) | `onboarding/company` | unreviewed / production / fixtures | 3 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Runner-default recipe text reviewed; rendered subjects and fixture contracts are unchanged by that documentation update. |
|
||||
| [Getting started and access/Onboarding and first work/Onboarding/Connect a model](../../ui/storybook/stories/onboarding/onboarding-connect-model.stories.tsx) | `onboarding/adapter`, `onboarding/first-task` | historical / prototype / fixtures | 5 / 0 | Older tile/mode preview; onboarding-agent-arc mounts the production wizard. Runner-default recipe text reviewed; rendered subjects and fixture contracts are unchanged by that documentation update. |
|
||||
|
||||
## projects
|
||||
|
||||
[Feature recipe](../feature-map/projects.md)
|
||||
|
||||
| Story suite | Capabilities | Product / implementation / data | Stories / play functions | Review note |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| [Projects and execution/Projects and repositories/Proposals/Project repos/New project](../../ui/storybook/stories/projects/new-project-repos.stories.tsx) | `projects/repositories` | proposed / mixed / fixtures | 18 / 5 | RepositoryEditor is shipped, but NewProjectPrototype composes its own creation flow. Preserve proposal until route parity is checked. |
|
||||
| [Projects and execution/Projects and repositories/Workspaces/Project runtime provision command](../../ui/storybook/stories/projects/project-execution-workspace-strategy.stories.tsx) | `projects/defaults`, `workspaces/provisioning` | unreviewed / production / fixtures | 2 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Projects and execution/Projects and repositories/Proposals/Project repos/Configuration](../../ui/storybook/stories/projects/project-repos-configuration.stories.tsx) | `projects/repositories` | proposed / mixed / fixtures | 12 / 0 | Production properties plus local configuration page composition; repository interaction is simulated. |
|
||||
| [Projects and execution/Projects and repositories/Product/Projects Goals Workspaces](../../ui/storybook/stories/projects/projects-goals-workspaces.stories.tsx) | `projects/lifecycle`, `goals/hierarchy`, `workspaces/binding` | unreviewed / production / fixtures | 1 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
|
||||
## questions-and-approvals
|
||||
|
||||
[Feature recipe](../feature-map/questions-and-approvals.md)
|
||||
|
||||
| Story suite | Capabilities | Product / implementation / data | Stories / play functions | Review note |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| [Tasks and conversations/Questions and approvals/Chat & Comments/Agent Chat Unanswered Questions](../../ui/storybook/stories/questions-and-approvals/agent-chat-unanswered-questions.stories.tsx) | `questions-and-approvals/dismiss-and-reopen`, `agent-chat/history` | unreviewed / production / fixtures | 9 / 6 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Tasks and conversations/Questions and approvals/Composer/Question flow](../../ui/storybook/stories/questions-and-approvals/composer-question-flow.stories.tsx) | `questions-and-approvals/questions` | unreviewed / production / fixtures | 3 / 1 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Tasks and conversations/Questions and approvals/Chat & Comments/Connection Reviews](../../ui/storybook/stories/questions-and-approvals/connection-reviews.stories.tsx) | `questions-and-approvals/tool-review`, `connection-setup/task-request` | unreviewed / production / fixtures | 25 / 8 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Tasks and conversations/Questions and approvals/Issue Detail/Plan Decompositions](../../ui/storybook/stories/questions-and-approvals/issue-plan-decompositions.stories.tsx) | `questions-and-approvals/plan-review`, `task-coordination/delegation` | unreviewed / production / fixtures | 2 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Tasks and conversations/Questions and approvals/Chat & Comments/Issue Thread Interactions](../../ui/storybook/stories/questions-and-approvals/issue-thread-interactions.stories.tsx) | `questions-and-approvals/questions`, `questions-and-approvals/confirmations`, `questions-and-approvals/audience` | unreviewed / production / fixtures | 53 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Tasks and conversations/Questions and approvals/Chat & Comments/Rich ACP interactions](../../ui/storybook/stories/questions-and-approvals/rich-acp-interactions.stories.tsx) | `questions-and-approvals/questions`, `questions-and-approvals/confirmations`, `questions-and-approvals/audience` | unreviewed / production / fixtures | 12 / 8 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Tasks and conversations/Questions and approvals/Composer/Interaction above composer](../../ui/storybook/stories/questions-and-approvals/task-chat-interaction-above-composer.stories.tsx) | `questions-and-approvals/questions`, `questions-and-approvals/confirmations`, `questions-and-approvals/audience` | current / production / fixtures | 15 / 5 | Corrected local dismissal/send harness: questions leave no pending badge; confirmations retain reopen. Production cards/composer; persisted dismissal is covered separately by unanswered-question fixtures. |
|
||||
| [Tasks and conversations/Questions and approvals/Chat & Comments/Task Chat Unanswered Questions](../../ui/storybook/stories/questions-and-approvals/task-chat-unanswered-questions.stories.tsx) | `questions-and-approvals/dismiss-and-reopen`, `tasks/discussion` | unreviewed / production / fixtures | 6 / 1 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
|
||||
## recovery
|
||||
|
||||
[Feature recipe](../feature-map/recovery.md)
|
||||
|
||||
| Story suite | Capabilities | Product / implementation / data | Stories / play functions | Review note |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| [Projects and execution/Recovery/App/Connection recovery](../../ui/storybook/stories/recovery/cloud-access-recovery.stories.tsx) | `recovery/reconnect` | unreviewed / production / fixtures | 3 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Projects and execution/Recovery/Task chat/Recovery notice](../../ui/storybook/stories/recovery/execution-blocker-notice.stories.tsx) | `recovery/diagnosis` | unreviewed / production / fixtures | 1 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Projects and execution/Recovery/Tasks/Execution recovery](../../ui/storybook/stories/recovery/execution-recovery.stories.tsx) | `recovery/diagnosis` | unreviewed / production / fixtures | 18 / 2 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Projects and execution/Recovery/Product/Issue/Blocked notice](../../ui/storybook/stories/recovery/issue-blocked-notice.stories.tsx) | `recovery/diagnosis`, `task-coordination/dependencies` | unreviewed / production / fixtures | 7 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Projects and execution/Recovery/Tasks/Model capacity retry](../../ui/storybook/stories/recovery/model-capacity-retry.stories.tsx) | `recovery/bounded-continuation` | unreviewed / production / fixtures | 1 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Projects and execution/Recovery/Design previews/Recovery notice](../../ui/storybook/stories/recovery/recovery-notice-preview.stories.tsx) | `recovery/diagnosis`, `recovery/source-action` | current / production / fixtures | 9 / 3 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Projects and execution/Recovery/Product/Issue Scheduled retry surfaces](../../ui/storybook/stories/recovery/scheduled-retry.stories.tsx) | `recovery/bounded-continuation` | unreviewed / production / fixtures | 1 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Projects and execution/Recovery/Paperclip/Source Issue Recovery](../../ui/storybook/stories/recovery/source-issue-recovery.stories.tsx) | `recovery/diagnosis`, `recovery/source-action` | unreviewed / production / fixtures | 5 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Projects and execution/Recovery/Paperclip/Source-resolved Fold](../../ui/storybook/stories/recovery/source-resolved-fold.stories.tsx) | `recovery/diagnosis`, `recovery/source-action` | unreviewed / production / fixtures | 5 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Projects and execution/Recovery/Workspace branch recovery/01 Repair card](../../ui/storybook/stories/recovery/workspace-branch-recovery/01-component.stories.tsx) | `recovery/workspace` | unreviewed / production / fixtures | 8 / 4 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Projects and execution/Recovery/Workspace branch recovery/02 Task journey](../../ui/storybook/stories/recovery/workspace-branch-recovery/02-task-journey.stories.tsx) | `recovery/workspace` | unreviewed / production / fixtures | 2 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
|
||||
## routines
|
||||
|
||||
[Feature recipe](../feature-map/routines.md)
|
||||
|
||||
| Story suite | Capabilities | Product / implementation / data | Stories / play functions | Review note |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| [Automation and structured work/Routines, schedules, and triggers/Product/Routines · Detail (variation C)](../../ui/storybook/stories/routines/routine-detail-c.stories.tsx) | `routines/definition`, `routines/management` | unreviewed / production / fixtures | 8 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Automation and structured work/Routines, schedules, and triggers/Product/Routines · Secrets tab](../../ui/storybook/stories/routines/routine-secrets.stories.tsx) | `routines/definition`, `secrets/runtime-delivery` | unreviewed / production / fixtures | 4 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Automation and structured work/Routines, schedules, and triggers/Product/Routines/Webhooks UX preview](../../ui/storybook/stories/routines/routine-webhooks-ux.stories.tsx) | `routines/triggers` | unreviewed / production / fixtures | 21 / 5 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Automation and structured work/Routines, schedules, and triggers/Product/Routines/Webhooks](../../ui/storybook/stories/routines/routine-webhooks.stories.tsx) | `routines/triggers` | unreviewed / production / fixtures | 16 / 14 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Automation and structured work/Routines, schedules, and triggers/Product/Routines · List (grouped cards)](../../ui/storybook/stories/routines/routines-list-groups.stories.tsx) | `routines/definition`, `routines/management` | unreviewed / production / fixtures | 1 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
|
||||
## runs-adapters
|
||||
|
||||
[Feature recipe](../feature-map/runs-adapters.md)
|
||||
|
||||
| Story suite | Capabilities | Product / implementation / data | Stories / play functions | Review note |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| [Agents and reusable capabilities/Runs, harnesses, and model accounts/Onboarding/Saved connections](../../ui/storybook/stories/runs-adapters/agent-connection-reuse.stories.tsx) | `runs-adapters/validation`, `agents/hire` | current / production / fixtures | 7 / 6 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Runner-default recipe text reviewed; rendered subjects and fixture contracts are unchanged by that documentation update. |
|
||||
| [Agents and reusable capabilities/Runs, harnesses, and model accounts/AI Connections/Usage limits](../../ui/storybook/stories/runs-adapters/ai-connection-usage.stories.tsx) | `runs-adapters/validation` | unreviewed / production / fixtures | 8 / 8 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Runner-default recipe text reviewed; rendered subjects and fixture contracts are unchanged by that documentation update. |
|
||||
| [Agents and reusable capabilities/Runs, harnesses, and model accounts/AI Connections/Review](../../ui/storybook/stories/runs-adapters/ai-connections.stories.tsx) | `runs-adapters/validation` | unreviewed / production / fixtures | 46 / 15 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Runner-default recipe text reviewed; rendered subjects and fixture contracts are unchanged by that documentation update. |
|
||||
| [Agents and reusable capabilities/Runs, harnesses, and model accounts/Connectors/Pool host](../../ui/storybook/stories/runs-adapters/connection-pool-connector.stories.tsx) | `runs-adapters/validation`, `connection-setup/catalog` | unreviewed / production / fixtures | 5 / 3 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Runner-default recipe text reviewed; rendered subjects and fixture contracts are unchanged by that documentation update. |
|
||||
| [Agents and reusable capabilities/Runs, harnesses, and model accounts/AI Connections/Connection pools](../../ui/storybook/stories/runs-adapters/connection-pools.stories.tsx) | `runs-adapters/validation` | unreviewed / production / fixtures | 16 / 2 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Runner display/default/effort and matching-discovery source changes reviewed; existing fixture, proposal and historical scope retained. No new rendering, play, backend or provider qualification is claimed. |
|
||||
| [Agents and reusable capabilities/Runs, harnesses, and model accounts/Assistant connections/Dot Runner](../../ui/storybook/stories/runs-adapters/dot-runner-connection.stories.tsx) | `runs-adapters/validation`, `gateways-profiles/client-connect` | unreviewed / production / fixtures | 4 / 2 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Runner-default recipe text reviewed; rendered subjects and fixture contracts are unchanged by that documentation update. |
|
||||
| [Agents and reusable capabilities/Runs, harnesses, and model accounts/AI Connections/Provider routing/07 Subscription sign-in](../../ui/storybook/stories/runs-adapters/local-ai-subscription.stories.tsx) | `runs-adapters/validation` | unreviewed / production / fixtures | 4 / 3 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Runner-default recipe text reviewed; rendered subjects and fixture contracts are unchanged by that documentation update. |
|
||||
| [Agents and reusable capabilities/Runs, harnesses, and model accounts/AI Connections/Provider routing/00 Overview](../../ui/storybook/stories/runs-adapters/provider-routing-00-overview.stories.tsx) | `runs-adapters/validation` | unreviewed / guide / static | 1 / 0 | Review guide retained; explanatory content does not establish runtime or production coverage. Runner-default recipe text reviewed; rendered subjects and fixture contracts are unchanged by that documentation update. |
|
||||
| [Agents and reusable capabilities/Runs, harnesses, and model accounts/AI Connections/Provider routing/01 Onboarding](../../ui/storybook/stories/runs-adapters/provider-routing-01-onboarding.stories.tsx) | `runs-adapters/validation` | mixed / mixed / fixtures | 6 / 3 | Design-review route uses simulated account/provider state; audit the rendered subject before treating it as the live connection flow. Runner display/default/effort and matching-discovery source changes reviewed; existing fixture, proposal and historical scope retained. No new rendering, play, backend or provider qualification is claimed. |
|
||||
| [Agents and reusable capabilities/Runs, harnesses, and model accounts/AI Connections/Provider routing/02 Connect](../../ui/storybook/stories/runs-adapters/provider-routing-02-connect.stories.tsx) | `runs-adapters/validation` | mixed / mixed / fixtures | 14 / 3 | Design-review route uses simulated account/provider state; audit the rendered subject before treating it as the live connection flow. Runner-default recipe text reviewed; rendered subjects and fixture contracts are unchanged by that documentation update. |
|
||||
| [Agents and reusable capabilities/Runs, harnesses, and model accounts/AI Connections/Provider routing/03 Agent](../../ui/storybook/stories/runs-adapters/provider-routing-03-agent.stories.tsx) | `runs-adapters/validation` | mixed / mixed / fixtures | 34 / 7 | Design-review route uses simulated account/provider state; audit the rendered subject before treating it as the live connection flow. Runner display/default/effort and matching-discovery source changes reviewed; existing fixture, proposal and historical scope retained. No new rendering, play, backend or provider qualification is claimed. |
|
||||
| [Agents and reusable capabilities/Runs, harnesses, and model accounts/AI Connections/Provider routing/04 Manage](../../ui/storybook/stories/runs-adapters/provider-routing-04-manage.stories.tsx) | `runs-adapters/validation` | mixed / mixed / fixtures | 11 / 5 | Design-review route uses simulated account/provider state; audit the rendered subject before treating it as the live connection flow. Runner-default recipe text reviewed; rendered subjects and fixture contracts are unchanged by that documentation update. |
|
||||
| [Agents and reusable capabilities/Runs, harnesses, and model accounts/AI Connections/Provider routing/05 Recovery](../../ui/storybook/stories/runs-adapters/provider-routing-05-recovery.stories.tsx) | `runs-adapters/validation` | mixed / mixed / fixtures | 7 / 2 | Design-review route uses simulated account/provider state; audit the rendered subject before treating it as the live connection flow. Runner-default recipe text reviewed; rendered subjects and fixture contracts are unchanged by that documentation update. |
|
||||
| [Agents and reusable capabilities/Runs, harnesses, and model accounts/AI Connections/Provider routing/06 Production components](../../ui/storybook/stories/runs-adapters/provider-routing-06-production.stories.tsx) | `runs-adapters/validation` | unreviewed / production / fixtures | 5 / 5 | Design-review route uses simulated account/provider state; audit the rendered subject before treating it as the live connection flow. Runner display/default/effort and matching-discovery source changes reviewed; existing fixture, proposal and historical scope retained. No new rendering, play, backend or provider qualification is claimed. |
|
||||
| [Agents and reusable capabilities/Runs, harnesses, and model accounts/Composer/Session goals](../../ui/storybook/stories/runs-adapters/session-goals.stories.tsx) | `runs-adapters/session` | unreviewed / production / fixtures | 3 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Runner-default recipe text reviewed; rendered subjects and fixture contracts are unchanged by that documentation update. |
|
||||
|
||||
## secrets
|
||||
|
||||
[Feature recipe](../feature-map/secrets.md)
|
||||
|
||||
| Story suite | Capabilities | Product / implementation / data | Stories / play functions | Review note |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| [Apps, channels, and extensions/Secrets and proposals/Task credential access/01 Notice](../../ui/storybook/stories/secrets/credential-access/01-notice.stories.tsx) | `secrets/runtime-delivery`, `recovery/diagnosis` | unreviewed / production / fixtures | 5 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Apps, channels, and extensions/Secrets and proposals/Task credential access/02 Task page](../../ui/storybook/stories/secrets/credential-access/02-task-page.stories.tsx) | `secrets/runtime-delivery`, `recovery/diagnosis` | unreviewed / production / fixtures | 6 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Runner display/default/effort and matching-discovery source changes reviewed; existing fixture, proposal and historical scope retained. No new rendering, play, backend or provider qualification is claimed. |
|
||||
| [Apps, channels, and extensions/Secrets and proposals/Product/Environment Variables Editor](../../ui/storybook/stories/secrets/environment-variables-editor.stories.tsx) | `secrets/runtime-delivery` | unreviewed / production / fixtures | 11 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Apps, channels, and extensions/Secrets and proposals/Product/Secrets](../../ui/storybook/stories/secrets/secrets.stories.tsx) | `secrets/company-secrets`, `secrets/vault-import` | unreviewed / production / fixtures | 4 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Apps, channels, and extensions/Secrets and proposals/Product/User secrets](../../ui/storybook/stories/secrets/user-secrets.stories.tsx) | `secrets/user-secrets` | unreviewed / production / fixtures | 4 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
|
||||
## skills
|
||||
|
||||
[Feature recipe](../feature-map/skills.md)
|
||||
|
||||
| Story suite | Capabilities | Product / implementation / data | Stories / play functions | Review note |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| [Agents and reusable capabilities/Skills and Skill Studio/Agent Skills/AgentSkillRow](../../ui/storybook/stories/skills/agent-skill-row.stories.tsx) | `skills/policy` | unreviewed / production / fixtures | 5 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Agents and reusable capabilities/Skills and Skill Studio/QA / Agent skills edge states](../../ui/storybook/stories/skills/agent-skills-edge.stories.tsx) | `skills/policy` | unreviewed / production / fixtures | 2 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Agents and reusable capabilities/Skills and Skill Studio/Skill Studio/AgentsUsingSkill](../../ui/storybook/stories/skills/agents-using-skill.stories.tsx) | `skills/policy` | unreviewed / production / fixtures | 5 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Agents and reusable capabilities/Skills and Skill Studio/Skill Studio/EditACopy](../../ui/storybook/stories/skills/fork-skill-flow.stories.tsx) | `skills/authoring` | unreviewed / production / fixtures | 6 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Agents and reusable capabilities/Skills and Skill Studio/Product/Skill Studio · FrontmatterPanel](../../ui/storybook/stories/skills/frontmatter-panel.stories.tsx) | `skills/authoring` | unreviewed / production / fixtures | 13 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Agents and reusable capabilities/Skills and Skill Studio/Skills/GitHub sync journey](../../ui/storybook/stories/skills/github-skills-journey.stories.tsx) | `skills/versions`, `skills/discovery` | unreviewed / production / fixtures | 14 / 3 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Agents and reusable capabilities/Skills and Skill Studio/Skills/GitHub synced skills](../../ui/storybook/stories/skills/github-synced-skills.stories.tsx) | `skills/versions`, `skills/discovery` | unreviewed / production / fixtures | 19 / 6 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Agents and reusable capabilities/Skills and Skill Studio/Skills/Repository packages](../../ui/storybook/stories/skills/repository-skill-packages/repository-skill-packages.stories.tsx) | `skills/versions`, `skills/discovery` | unreviewed / production / fixtures | 9 / 3 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Agents and reusable capabilities/Skills and Skill Studio/Skills/Import progress](../../ui/storybook/stories/skills/skill-import-progress.stories.tsx) | `skills/versions`, `skills/discovery` | unreviewed / production / fixtures | 6 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Agents and reusable capabilities/Skills and Skill Studio/Product/Skills/Policy surfaces](../../ui/storybook/stories/skills/skill-policy-surfaces.stories.tsx) | `skills/policy` | unreviewed / production / fixtures | 2 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Agents and reusable capabilities/Skills and Skill Studio/Skills Store/Skill detail](../../ui/storybook/stories/skills/skills-store-detail.stories.tsx) | `skills/discovery` | unreviewed / production / fixtures | 4 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Agents and reusable capabilities/Skills and Skill Studio/Skills Store/Discovery grid](../../ui/storybook/stories/skills/skills-store-discovery.stories.tsx) | `skills/discovery` | unreviewed / production / fixtures | 3 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
|
||||
## steering
|
||||
|
||||
[Feature recipe](../feature-map/steering.md)
|
||||
|
||||
| Story suite | Capabilities | Product / implementation / data | Stories / play functions | Review note |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| [Tasks and conversations/Steering and queued messages/Composer/Steering and queued messages](../../ui/storybook/stories/steering/composer-queued-messages.stories.tsx) | `steering/queue`, `steering/edit-order-discard`, `steering/steer`, `steering/interrupt` | current / production / fixtures | 6 / 1 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Tasks and conversations/Steering and queued messages/Surfaces/Interrupt Handoff](../../ui/storybook/stories/steering/interrupt-handoff.stories.tsx) | `steering/interrupt` | unreviewed / production / fixtures | 5 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Tasks and conversations/Steering and queued messages/Composer/Paused task takeover](../../ui/storybook/stories/steering/paused-composer.stories.tsx) | `steering/stop-and-pause` | historical / prototype / fixtures | 8 / 0 | Retained local paused takeover; task-execution-controls exercises the shipped pause/stop controls. |
|
||||
| [Tasks and conversations/Steering and queued messages/Composer/Execution controls](../../ui/storybook/stories/steering/task-execution-controls.stories.tsx) | `steering/stop-and-pause` | current / production / fixtures | 22 / 6 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
|
||||
## task-coordination
|
||||
|
||||
[Feature recipe](../feature-map/task-coordination.md)
|
||||
|
||||
| Story suite | Capabilities | Product / implementation / data | Stories / play functions | Review note |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| [Tasks and conversations/Delegation, dependencies, and signoff/Product/Decisions/Stalled review actions](../../ui/storybook/stories/task-coordination/issue-review-panel.stories.tsx) | `task-coordination/signoff` | unreviewed / production / fixtures | 2 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Tasks and conversations/Delegation, dependencies, and signoff/Product/Issue Monitor surfaces](../../ui/storybook/stories/task-coordination/monitor-surfaces.stories.tsx) | `task-coordination/tree-control` | unreviewed / production / fixtures | 1 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Tasks and conversations/Delegation, dependencies, and signoff/UX Labs/Tasks Created From a Task](../../ui/storybook/stories/task-coordination/originating-tasks.stories.tsx) | `task-coordination/delegation`, `projects/task-context` | unreviewed / production / fixtures | 14 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Tasks and conversations/Delegation, dependencies, and signoff/UX Labs/Sub-issues Workflow Checklist](../../ui/storybook/stories/task-coordination/sub-issues-workflow.stories.tsx) | `task-coordination/dependencies`, `task-coordination/delegation` | unreviewed / production / fixtures | 1 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Tasks and conversations/Delegation, dependencies, and signoff/Product/Task Watchdog surfaces](../../ui/storybook/stories/task-coordination/task-watchdog-surfaces.stories.tsx) | `task-coordination/tree-control` | unreviewed / production / fixtures | 1 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
|
||||
## tasks
|
||||
|
||||
[Feature recipe](../feature-map/tasks.md)
|
||||
|
||||
| Story suite | Capabilities | Product / implementation / data | Stories / play functions | Review note |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| [Tasks and conversations/Task creation and lifecycle/Paperclip/Assigned Backlog Safeguards](../../ui/storybook/stories/tasks/assigned-backlog-safeguards.stories.tsx) | `tasks/completion`, `recovery/diagnosis` | unreviewed / production / fixtures | 5 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Tasks and conversations/Task creation and lifecycle/Product/Chat & Comments](../../ui/storybook/stories/tasks/chat-comments.stories.tsx) | `tasks/discussion`, `runs-adapters/runs` | unreviewed / production / fixtures | 6 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Runner-default recipe text reviewed; rendered subjects and fixture contracts are unchanged by that documentation update. |
|
||||
| [Tasks and conversations/Task creation and lifecycle/Tasks/Completed activity preview](../../ui/storybook/stories/tasks/completed-activity.stories.tsx) | `tasks/discussion`, `runs-adapters/runs` | current / production / fixtures | 8 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Runner-default recipe text reviewed; rendered subjects and fixture contracts are unchanged by that documentation update. |
|
||||
| [Tasks and conversations/Task creation and lifecycle/Composer/Add menu](../../ui/storybook/stories/tasks/composer-add-menu.stories.tsx) | `tasks/create` | unreviewed / production / fixtures | 14 / 14 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Tasks and conversations/Task creation and lifecycle/Composer/Mobile assignee picker](../../ui/storybook/stories/tasks/composer-mobile-assignee.stories.tsx) | `tasks/create`, `runs-adapters/availability` | unreviewed / production / fixtures | 3 / 1 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Runner display/default/effort and matching-discovery source changes reviewed; existing fixture, proposal and historical scope retained. No new rendering, play, backend or provider qualification is claimed. |
|
||||
| [Tasks and conversations/Task creation and lifecycle/Composer/Model and effort picker](../../ui/storybook/stories/tasks/composer-model-picker.stories.tsx) | `tasks/create`, `runs-adapters/availability` | mixed / mixed / fixtures | 57 / 30 | Most examples use a local picker implementation; live picker examples use ComposerRunSettingsLiveStory. Do not count the whole suite as production parity. Runner display/default/effort and matching-discovery source changes reviewed; existing fixture, proposal and historical scope retained. No new rendering, play, backend or provider qualification is claimed. |
|
||||
| [Tasks and conversations/Task creation and lifecycle/Product/Control Plane Surfaces](../../ui/storybook/stories/tasks/control-plane-surfaces.stories.tsx) | `tasks/organize`, `questions-and-approvals/governance`, `budgets-costs/budgets`, `activity/activity` | unreviewed / production / fixtures | 1 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Tasks and conversations/Task creation and lifecycle/Product/Dialogs & Modals](../../ui/storybook/stories/tasks/dialogs-modals.stories.tsx) | `tasks/create`, `agents/hire`, `projects/lifecycle`, `goals/hierarchy` | unreviewed / production / fixtures | 17 / 2 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Runner-default recipe text reviewed; rendered subjects and fixture contracts are unchanged by that documentation update. |
|
||||
| [Tasks and conversations/Task creation and lifecycle/External Objects](../../ui/storybook/stories/tasks/external-objects.stories.tsx) | `tasks/properties` | unreviewed / production / fixtures | 8 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Tasks and conversations/Task creation and lifecycle/Product/Issue Management](../../ui/storybook/stories/tasks/issue-management.stories.tsx) | `tasks/properties`, `tasks/organize` | unreviewed / production / fixtures | 8 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Tasks and conversations/Task creation and lifecycle/Components/Entity pickers/Mobile](../../ui/storybook/stories/tasks/mobile-entity-pickers.stories.tsx) | `tasks/create` | unreviewed / production / fixtures | 3 / 1 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Tasks and conversations/Task creation and lifecycle/Composer/New task](../../ui/storybook/stories/tasks/new-task-composer.stories.tsx) | `tasks/create` | current / production / fixtures | 40 / 25 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Tasks and conversations/Task creation and lifecycle/Tasks/Runner activity preview](../../ui/storybook/stories/tasks/runner-activity.stories.tsx) | `tasks/discussion`, `runs-adapters/runs` | current / production / fixtures | 14 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Runner-default recipe text reviewed; rendered subjects and fixture contracts are unchanged by that documentation update. |
|
||||
| [Tasks and conversations/Task creation and lifecycle/Paperclip/Successful Run Handoff](../../ui/storybook/stories/tasks/successful-run-handoff.stories.tsx) | `tasks/completion`, `recovery/diagnosis` | unreviewed / production / fixtures | 7 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Tasks and conversations/Task creation and lifecycle/Tasks/Task chat review fixes](../../ui/storybook/stories/tasks/task-chat-chain-of-thought.stories.tsx) | `tasks/discussion`, `runs-adapters/runs` | unreviewed / production / fixtures | 4 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Runner-default recipe text reviewed; rendered subjects and fixture contracts are unchanged by that documentation update. |
|
||||
| [Tasks and conversations/Task creation and lifecycle/Task chat/Mobile streaming follow](../../ui/storybook/stories/tasks/task-chat-mobile-follow.stories.tsx) | `tasks/discussion` | unreviewed / production / fixtures | 1 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Tasks and conversations/Task creation and lifecycle/Product/Task chat/System notice](../../ui/storybook/stories/tasks/task-chat-system-notice.stories.tsx) | `tasks/discussion` | unreviewed / production / fixtures | 2 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Tasks and conversations/Task creation and lifecycle/Tasks/Context notices](../../ui/storybook/stories/tasks/task-context-notices.stories.tsx) | `tasks/discussion` | unreviewed / production / fixtures | 1 / 1 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Tasks and conversations/Task creation and lifecycle/Chat & Comments/Human identity/00 Review guide](../../ui/storybook/stories/tasks/thread-human-identity/00-evidence.stories.tsx) | `tasks/discussion` | unreviewed / guide / static | 1 / 0 | Review guide retained; explanatory content does not establish runtime or production coverage. |
|
||||
| [Tasks and conversations/Task creation and lifecycle/Chat & Comments/Human identity/01 Components](../../ui/storybook/stories/tasks/thread-human-identity/01-components.stories.tsx) | `tasks/discussion` | unreviewed / production / fixtures | 5 / 2 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Tasks and conversations/Task creation and lifecycle/Chat & Comments/Human identity/02 Mixed-author journey](../../ui/storybook/stories/tasks/thread-human-identity/02-task-journey.stories.tsx) | `tasks/discussion` | unreviewed / production / fixtures | 5 / 2 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
|
||||
## teams
|
||||
|
||||
[Feature recipe](../feature-map/teams.md)
|
||||
|
||||
| Story suite | Capabilities | Product / implementation / data | Stories / play functions | Review note |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| [Agents and reusable capabilities/Team packages and installation/Surfaces/Team Catalog](../../ui/storybook/stories/teams/team-catalog.stories.tsx) | `teams/preview`, `teams/discovery` | unreviewed / production / fixtures | 12 / 0 | Renders shipped TeamCatalog source, but feature map documents current CLI/API access and no top-level catalog route. |
|
||||
|
||||
## workspaces
|
||||
|
||||
[Feature recipe](../feature-map/workspaces.md)
|
||||
|
||||
| Story suite | Capabilities | Product / implementation / data | Stories / play functions | Review note |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| [Projects and execution/Workspaces, services, and files/Tasks/Browser Use/Task activity](../../ui/storybook/stories/workspaces/browser-use-activity.stories.tsx) | `workspaces/services`, `tasks/discussion` | unreviewed / production / fixtures | 7 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Projects and execution/Workspaces, services, and files/Tasks/Browser Use/Footer](../../ui/storybook/stories/workspaces/browser-use-footer.stories.tsx) | `workspaces/services`, `tasks/discussion` | unreviewed / production / fixtures | 21 / 2 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Projects and execution/Workspaces, services, and files/Tasks/Browser Use/Panel](../../ui/storybook/stories/workspaces/browser-use.stories.tsx) | `workspaces/services`, `tasks/discussion` | unreviewed / production / fixtures | 28 / 3 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Projects and execution/Workspaces, services, and files/Components/SearchableSelect/Workspace picker](../../ui/storybook/stories/workspaces/searchable-select.stories.tsx) | `workspaces/binding` | unreviewed / production / fixtures | 9 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Projects and execution/Workspaces, services, and files/Issue/Workspace File Browser](../../ui/storybook/stories/workspaces/workspace-file-browser.stories.tsx) | `workspaces/files-git` | unreviewed / production / fixtures | 6 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Projects and execution/Workspaces, services, and files/Workspaces/Runtime provisioning status](../../ui/storybook/stories/workspaces/workspace-runtime-provisioning.stories.tsx) | `workspaces/provisioning` | unreviewed / production / fixtures | 6 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
| [Projects and execution/Workspaces, services, and files/Workspaces/Service control bar](../../ui/storybook/stories/workspaces/workspace-service-control-bar.stories.tsx) | `workspaces/services` | unreviewed / production / fixtures | 16 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
|
||||
|
||||
## Capability lookup
|
||||
|
||||
References are navigation links, not proof of parity. An absent link means no cataloged story, not a missing product feature. CLI/operator capabilities may need no Storybook example.
|
||||
|
||||
- `access/login`: No cataloged story.
|
||||
- `access/invitations`: [Agents and reusable capabilities/Hiring, configuration, and organization/Onboarding/External agent invitation/Components](../../ui/storybook/stories/agents/external-agent-invite/components.stories.tsx) (unreviewed), [Agents and reusable capabilities/Hiring, configuration, and organization/Onboarding/External agent invitation/Journeys](../../ui/storybook/stories/agents/external-agent-invite/journeys.stories.tsx) (unreviewed)
|
||||
- `access/membership`: No cataloged story.
|
||||
- `access/resources`: [Getting started and access/Login, invitations, and access/Private tasks/00 Review guide](../../ui/storybook/stories/access/private-tasks/00-review-guide.stories.tsx) (unreviewed), [Getting started and access/Login, invitations, and access/Private tasks/01 Creation](../../ui/storybook/stories/access/private-tasks/01-creation.stories.tsx) (unreviewed), [Getting started and access/Login, invitations, and access/Private tasks/02 Sharing](../../ui/storybook/stories/access/private-tasks/02-sharing.stories.tsx) (unreviewed), [Getting started and access/Login, invitations, and access/Private tasks/03 Task actions](../../ui/storybook/stories/access/private-tasks/03-task-actions.stories.tsx) (unreviewed), [Getting started and access/Login, invitations, and access/Private tasks/04 References and blockers](../../ui/storybook/stories/access/private-tasks/04-references-and-blockers.stories.tsx) (unreviewed), [Getting started and access/Login, invitations, and access/Private tasks/05 Projects](../../ui/storybook/stories/access/private-tasks/05-projects.stories.tsx) (unreviewed), [Getting started and access/Login, invitations, and access/Private tasks/06 Full product pages](../../ui/storybook/stories/access/private-tasks/06-full-product-pages.stories.tsx) (unreviewed), [Getting started and access/Login, invitations, and access/Private tasks/07 User stories](../../ui/storybook/stories/access/private-tasks/07-user-stories.stories.tsx) (unreviewed)
|
||||
- `access/cli-auth`: No cataloged story.
|
||||
- `activity/overview`: [Oversight and operation/Dashboards and audit trails/Product/Data Visualization & Misc](../../ui/storybook/stories/activity/data-viz-misc.stories.tsx) (unreviewed)
|
||||
- `activity/activity`: [Oversight and operation/Dashboards and audit trails/Tools/Activity (audit feed)](../../ui/storybook/stories/activity/activity-feed.stories.tsx) (unreviewed), [Oversight and operation/Dashboards and audit trails/Apps/App detail · Activity (lifecycle)](../../ui/storybook/stories/activity/app-activity-lifecycle.stories.tsx) (unreviewed), [Tasks and conversations/Task creation and lifecycle/Product/Control Plane Surfaces](../../ui/storybook/stories/tasks/control-plane-surfaces.stories.tsx) (unreviewed)
|
||||
- `activity/runs`: No cataloged story.
|
||||
- `activity/routine-audit`: No cataloged story.
|
||||
- `activity/timeline`: [Oversight and operation/Dashboards and audit trails/Pages/Work Timeline](../../ui/storybook/stories/activity/work-timeline.stories.tsx) (unreviewed)
|
||||
- `agent-chat/discovery`: [Tasks and conversations/Agent conversations and project handoff/Design explorations/Agent chat sidebar/Pages](../../ui/storybook/stories/agent-chat/agent-chat-sidebar.stories.tsx) (current), [Tasks and conversations/Agent conversations and project handoff/Design explorations/Agent chat](../../ui/storybook/stories/agent-chat/agent-chat.stories.tsx) (mixed), [Tasks and conversations/Agent conversations and project handoff/Design explorations/Agent chat sidebar/Components](../../ui/storybook/stories/agent-chat/agent-conversation-sidebar.stories.tsx) (current), [Tasks and conversations/Agent conversations and project handoff/Design explorations/Chat entry](../../ui/storybook/stories/agent-chat/chat-entry.stories.tsx) (historical), [Agents and reusable capabilities/Hiring, configuration, and organization/Primary Agent/00 Review guide](../../ui/storybook/stories/agents/primary-agent/00-review-guide.stories.tsx) (unreviewed), [Agents and reusable capabilities/Hiring, configuration, and organization/Primary Agent/01 Components](../../ui/storybook/stories/agents/primary-agent/01-components.stories.tsx) (unreviewed), [Agents and reusable capabilities/Hiring, configuration, and organization/Primary Agent/02 Sidebar](../../ui/storybook/stories/agents/primary-agent/02-sidebar.stories.tsx) (unreviewed), [Agents and reusable capabilities/Hiring, configuration, and organization/Primary Agent/03 Agents](../../ui/storybook/stories/agents/primary-agent/03-agents.stories.tsx) (unreviewed), [Agents and reusable capabilities/Hiring, configuration, and organization/Primary Agent/04 New task](../../ui/storybook/stories/agents/primary-agent/04-new-task.stories.tsx) (unreviewed), [Agents and reusable capabilities/Hiring, configuration, and organization/Primary Agent/05 Chat](../../ui/storybook/stories/agents/primary-agent/05-chat.stories.tsx) (unreviewed), [Agents and reusable capabilities/Hiring, configuration, and organization/Primary Agent/06 Journeys](../../ui/storybook/stories/agents/primary-agent/06-journeys.stories.tsx) (unreviewed), [Agents and reusable capabilities/Hiring, configuration, and organization/Primary Agent/07 Confirmation](../../ui/storybook/stories/agents/primary-agent/07-confirmation.stories.tsx) (unreviewed)
|
||||
- `agent-chat/history`: [Tasks and conversations/Agent conversations and project handoff/Design explorations/Agent chat sidebar/Pages](../../ui/storybook/stories/agent-chat/agent-chat-sidebar.stories.tsx) (current), [Tasks and conversations/Questions and approvals/Chat & Comments/Agent Chat Unanswered Questions](../../ui/storybook/stories/questions-and-approvals/agent-chat-unanswered-questions.stories.tsx) (unreviewed), [Tasks and conversations/Agent conversations and project handoff/Design explorations/Agent chat](../../ui/storybook/stories/agent-chat/agent-chat.stories.tsx) (mixed), [Tasks and conversations/Agent conversations and project handoff/Design explorations/Agent chat sidebar/Components](../../ui/storybook/stories/agent-chat/agent-conversation-sidebar.stories.tsx) (current), [Tasks and conversations/Agent conversations and project handoff/Design explorations/Chat entry](../../ui/storybook/stories/agent-chat/chat-entry.stories.tsx) (historical)
|
||||
- `agent-chat/handoff`: No cataloged story.
|
||||
- `agent-chat/conference-room`: No cataloged story.
|
||||
- `agents/hire`: [Agents and reusable capabilities/Runs, harnesses, and model accounts/Onboarding/Saved connections](../../ui/storybook/stories/runs-adapters/agent-connection-reuse.stories.tsx) (current), [Tasks and conversations/Task creation and lifecycle/Product/Dialogs & Modals](../../ui/storybook/stories/tasks/dialogs-modals.stories.tsx) (unreviewed), [Agents and reusable capabilities/Hiring, configuration, and organization/Onboarding/External agent invitation/Components](../../ui/storybook/stories/agents/external-agent-invite/components.stories.tsx) (unreviewed), [Agents and reusable capabilities/Hiring, configuration, and organization/Onboarding/External agent invitation/Journeys](../../ui/storybook/stories/agents/external-agent-invite/journeys.stories.tsx) (unreviewed), [Agents and reusable capabilities/Hiring, configuration, and organization/Onboarding/New agent](../../ui/storybook/stories/agents/new-agent-onboarding.stories.tsx) (historical), [Agents and reusable capabilities/Hiring, configuration, and organization/Codex runner/Creation](../../ui/storybook/stories/agents/codex-runner-creation.stories.tsx) (unreviewed)
|
||||
- `agents/instructions`: [Agents and reusable capabilities/Hiring, configuration, and organization/Pages/Agent Detail](../../ui/storybook/stories/agents/agent-detail.stories.tsx) (unreviewed), [Agents and reusable capabilities/Hiring, configuration, and organization/Agents/Persistent files](../../ui/storybook/stories/agents/agent-files.stories.tsx) (unreviewed), [Agents and reusable capabilities/Hiring, configuration, and organization/Product/Agent Management](../../ui/storybook/stories/agents/agent-management.stories.tsx) (unreviewed), [Agents and reusable capabilities/Hiring, configuration, and organization/Agents/Configuration refresh](../../ui/storybook/stories/agents/agent-settings-redesign.stories.tsx) (mixed), [Apps, channels, and extensions/Connection setup/Design explorations/Connections/Agent instructions](../../ui/storybook/stories/connection-setup/connection-instructions.stories.tsx) (unreviewed), [Apps, channels, and extensions/Connection setup/Apps/Connections/Memory](../../ui/storybook/stories/connection-setup/memory-connectors.stories.tsx) (unreviewed)
|
||||
- `agents/organization`: [Agents and reusable capabilities/Hiring, configuration, and organization/Agents/Personas/Full pages](../../ui/storybook/stories/agents/agent-persona-pages.stories.tsx) (unreviewed), [Agents and reusable capabilities/Hiring, configuration, and organization/Agents/Personas](../../ui/storybook/stories/agents/agent-personas.stories.tsx) (unreviewed), [Agents and reusable capabilities/Hiring, configuration, and organization/Primary Agent/00 Review guide](../../ui/storybook/stories/agents/primary-agent/00-review-guide.stories.tsx) (unreviewed), [Agents and reusable capabilities/Hiring, configuration, and organization/Primary Agent/01 Components](../../ui/storybook/stories/agents/primary-agent/01-components.stories.tsx) (unreviewed), [Agents and reusable capabilities/Hiring, configuration, and organization/Primary Agent/02 Sidebar](../../ui/storybook/stories/agents/primary-agent/02-sidebar.stories.tsx) (unreviewed), [Agents and reusable capabilities/Hiring, configuration, and organization/Primary Agent/03 Agents](../../ui/storybook/stories/agents/primary-agent/03-agents.stories.tsx) (unreviewed), [Agents and reusable capabilities/Hiring, configuration, and organization/Primary Agent/04 New task](../../ui/storybook/stories/agents/primary-agent/04-new-task.stories.tsx) (unreviewed), [Agents and reusable capabilities/Hiring, configuration, and organization/Primary Agent/05 Chat](../../ui/storybook/stories/agents/primary-agent/05-chat.stories.tsx) (unreviewed), [Agents and reusable capabilities/Hiring, configuration, and organization/Primary Agent/06 Journeys](../../ui/storybook/stories/agents/primary-agent/06-journeys.stories.tsx) (unreviewed), [Agents and reusable capabilities/Hiring, configuration, and organization/Primary Agent/07 Confirmation](../../ui/storybook/stories/agents/primary-agent/07-confirmation.stories.tsx) (unreviewed)
|
||||
- `agents/lifecycle`: [Agents and reusable capabilities/Hiring, configuration, and organization/Pages/Agent Detail](../../ui/storybook/stories/agents/agent-detail.stories.tsx) (unreviewed), [Agents and reusable capabilities/Hiring, configuration, and organization/Product/Agent Management](../../ui/storybook/stories/agents/agent-management.stories.tsx) (unreviewed), [Agents and reusable capabilities/Hiring, configuration, and organization/Agents/Configuration refresh](../../ui/storybook/stories/agents/agent-settings-redesign.stories.tsx) (mixed)
|
||||
- `agents/built-in`: [Agents and reusable capabilities/Hiring, configuration, and organization/Product/Built-in Agents](../../ui/storybook/stories/agents/built-in-agents.stories.tsx) (unreviewed), [Agents and reusable capabilities/Hiring, configuration, and organization/Summaries/SummarySlotCard](../../ui/storybook/stories/agents/summary-slot-card.stories.tsx) (unreviewed)
|
||||
- `app-permissions/agent-access`: [Apps, channels, and extensions/App access and action permissions/Tools/Permitted vs Installed](../../ui/storybook/stories/app-permissions/permitted-vs-installed.stories.tsx) (unreviewed)
|
||||
- `app-permissions/action-policy`: [Apps, channels, and extensions/App access and action permissions/Tools & Access/ActionCard](../../ui/storybook/stories/app-permissions/action-card.stories.tsx) (unreviewed), [Apps, channels, and extensions/App access and action permissions/Apps/Action permissions test](../../ui/storybook/stories/app-permissions/action-permissions-test.stories.tsx) (unreviewed), [Apps, channels, and extensions/App access and action permissions/Tools/Permitted vs Installed](../../ui/storybook/stories/app-permissions/permitted-vs-installed.stories.tsx) (unreviewed)
|
||||
- `app-permissions/catalog-change`: No cataloged story.
|
||||
- `app-permissions/test-action`: [Apps, channels, and extensions/App access and action permissions/Tools & Access/ActionCard](../../ui/storybook/stories/app-permissions/action-card.stories.tsx) (unreviewed), [Apps, channels, and extensions/App access and action permissions/Apps/Action permissions test](../../ui/storybook/stories/app-permissions/action-permissions-test.stories.tsx) (unreviewed)
|
||||
- `app-permissions/account-maintenance`: [Apps, channels, and extensions/App access and action permissions/Apps/Managed accounts](../../ui/storybook/stories/app-permissions/apps-managed-accounts.stories.tsx) (current)
|
||||
- `budgets-costs/cost-reporting`: [Oversight and operation/Costs and budgets/Product/Budget & Finance](../../ui/storybook/stories/budgets-costs/budget-finance.stories.tsx) (unreviewed), [Oversight and operation/Costs and budgets/Costs/Subscriptions](../../ui/storybook/stories/budgets-costs/costs/subscriptions.stories.tsx) (unreviewed), [Getting started and access/Companies and portability/Decision models/01 Settings](../../ui/storybook/stories/companies/decision-models/01-settings.stories.tsx) (unreviewed), [Getting started and access/Companies and portability/Decision models/02 History](../../ui/storybook/stories/companies/decision-models/02-history.stories.tsx) (unreviewed), [Getting started and access/Companies and portability/Decision models/03 Journey](../../ui/storybook/stories/companies/decision-models/03-journey.stories.tsx) (unreviewed), [Getting started and access/Companies and portability/Fast responses/01 Settings](../../ui/storybook/stories/companies/fast-responses/01-settings.stories.tsx) (unreviewed), [Getting started and access/Companies and portability/Fast responses/02 History](../../ui/storybook/stories/companies/fast-responses/02-history.stories.tsx) (current)
|
||||
- `budgets-costs/budgets`: [Oversight and operation/Costs and budgets/Product/Budget & Finance](../../ui/storybook/stories/budgets-costs/budget-finance.stories.tsx) (unreviewed), [Tasks and conversations/Task creation and lifecycle/Product/Control Plane Surfaces](../../ui/storybook/stories/tasks/control-plane-surfaces.stories.tsx) (unreviewed), [Oversight and operation/Costs and budgets/Costs/Subscriptions](../../ui/storybook/stories/budgets-costs/costs/subscriptions.stories.tsx) (unreviewed)
|
||||
- `budgets-costs/incidents`: No cataloged story.
|
||||
- `budgets-costs/resolution`: No cataloged story.
|
||||
- `cases/browse`: No cataloged story.
|
||||
- `cases/structure`: No cataloged story.
|
||||
- `cases/history`: No cataloged story.
|
||||
- `cases/task-links`: No cataloged story.
|
||||
- `chat-channels/providers`: [Apps, channels, and extensions/External chat and email/Connections/AgentMail setup](../../ui/storybook/stories/chat-channels/agentmail-onboarding.stories.tsx) (unreviewed), [Apps, channels, and extensions/External chat and email/Connections/Chat connection actions](../../ui/storybook/stories/chat-channels/chat-connection-actions.stories.tsx) (unreviewed), [Apps, channels, and extensions/External chat and email/Connectors/GitHub bot management](../../ui/storybook/stories/chat-channels/github-bot-management/management.stories.tsx) (unreviewed), [Apps, channels, and extensions/External chat and email/Historical/GitHub chat prototypes](../../ui/storybook/stories/chat-channels/github-chat.stories.tsx) (historical), [Apps, channels, and extensions/External chat and email/Connections/GitHub and Code Review Bot](../../ui/storybook/stories/chat-channels/github-connections.stories.tsx) (unreviewed), [Apps, channels, and extensions/External chat and email/Connections/Slack/Automatic setup](../../ui/storybook/stories/chat-channels/slack-automatic-setup.stories.tsx) (unreviewed), [Apps, channels, and extensions/External chat and email/Connections/Slack/Managed setup](../../ui/storybook/stories/chat-channels/slack-managed-setup.stories.tsx) (unreviewed)
|
||||
- `chat-channels/identity`: [Apps, channels, and extensions/External chat and email/Connections/Slack/Avatar in Settings](../../ui/storybook/stories/chat-channels/slack-avatar-settings.stories.tsx) (current), [Apps, channels, and extensions/External chat and email/Connections/Slack/Communication in Settings](../../ui/storybook/stories/chat-channels/slack-communication-settings.stories.tsx) (current)
|
||||
- `chat-channels/conversation`: [Apps, channels, and extensions/External chat and email/Connections/AgentMail tasks](../../ui/storybook/stories/chat-channels/agentmail.stories.tsx) (unreviewed), [Apps, channels, and extensions/External chat and email/Connectors/GitHub bot management](../../ui/storybook/stories/chat-channels/github-bot-management/management.stories.tsx) (unreviewed), [Apps, channels, and extensions/External chat and email/Historical/GitHub chat prototypes](../../ui/storybook/stories/chat-channels/github-chat.stories.tsx) (historical), [Apps, channels, and extensions/External chat and email/Connections/iMessage Photon](../../ui/storybook/stories/chat-channels/imessage-photon.stories.tsx) (unreviewed), [Apps, channels, and extensions/External chat and email/Connections/Slack/Task tools](../../ui/storybook/stories/chat-channels/slack-tools.stories.tsx) (unreviewed)
|
||||
- `chat-channels/attachments`: No cataloged story.
|
||||
- `chat-channels/lifecycle`: [Apps, channels, and extensions/External chat and email/Connections/AgentMail settings](../../ui/storybook/stories/chat-channels/agentmail-settings.stories.tsx) (unreviewed), [Apps, channels, and extensions/External chat and email/Connections/Chat management](../../ui/storybook/stories/chat-channels/chat-management/chat-management.stories.tsx) (unreviewed)
|
||||
- `cli-operations/context`: No cataloged story.
|
||||
- `cli-operations/domain-commands`: No cataloged story.
|
||||
- `cli-operations/outputs`: No cataloged story.
|
||||
- `cli-operations/run-controls`: No cataloged story.
|
||||
- `cli-operations/worktree-instances`: No cataloged story.
|
||||
- `companies/selection`: No cataloged story.
|
||||
- `companies/identity`: [Getting started and access/Companies and portability/Decision models/01 Settings](../../ui/storybook/stories/companies/decision-models/01-settings.stories.tsx) (unreviewed), [Getting started and access/Companies and portability/Decision models/02 History](../../ui/storybook/stories/companies/decision-models/02-history.stories.tsx) (unreviewed), [Getting started and access/Companies and portability/Decision models/03 Journey](../../ui/storybook/stories/companies/decision-models/03-journey.stories.tsx) (unreviewed), [Getting started and access/Companies and portability/Fast responses/01 Settings](../../ui/storybook/stories/companies/fast-responses/01-settings.stories.tsx) (unreviewed), [Getting started and access/Companies and portability/Fast responses/02 History](../../ui/storybook/stories/companies/fast-responses/02-history.stories.tsx) (current)
|
||||
- `companies/archive`: No cataloged story.
|
||||
- `companies/portability`: No cataloged story.
|
||||
- `connection-setup/catalog`: [Apps, channels, and extensions/Connection setup/Apps/Canonical icon registry](../../ui/storybook/stories/connection-setup/AppLogoRegistry.stories.tsx) (unreviewed), [Apps, channels, and extensions/Connection setup/Apps/Catalog source selection](../../ui/storybook/stories/connection-setup/apps-catalog-source-selection.stories.tsx) (proposed), [Apps, channels, and extensions/Connection setup/Apps/Connect discoverability (PAP-11091)](../../ui/storybook/stories/connection-setup/apps-connect-discoverability.stories.tsx) (unreviewed), [Agents and reusable capabilities/Runs, harnesses, and model accounts/Connectors/Pool host](../../ui/storybook/stories/runs-adapters/connection-pool-connector.stories.tsx) (unreviewed)
|
||||
- `connection-setup/identity-and-access`: [Apps, channels, and extensions/Connection setup/Apps/Connections/Provider choice](../../ui/storybook/stories/connection-setup/ConnectionProviderChoice.stories.tsx) (unreviewed), [Apps, channels, and extensions/Connection setup/Apps/Connect name at create time (PAP-11283)](../../ui/storybook/stories/connection-setup/apps-connect-name.stories.tsx) (unreviewed), [Apps, channels, and extensions/Connection setup/Reviews/PAP-17835 Personal connection identity](../../ui/storybook/stories/connection-setup/personal-connection-identity.stories.tsx) (unreviewed)
|
||||
- `connection-setup/credentials`: [Apps, channels, and extensions/Connection setup/Apps/Connections/Arcade](../../ui/storybook/stories/connection-setup/ArcadeConnection.stories.tsx) (unreviewed), [Apps, channels, and extensions/Connection setup/Apps/Connections/Composio](../../ui/storybook/stories/connection-setup/ComposioConnection.stories.tsx) (unreviewed), [Apps, channels, and extensions/Connection setup/Apps/Connections/Executor](../../ui/storybook/stories/connection-setup/ExecutorConnection.stories.tsx) (unreviewed), [Apps, channels, and extensions/Connection setup/Apps/Connections/Zapier](../../ui/storybook/stories/connection-setup/ZapierConnection.stories.tsx) (unreviewed), [Apps, channels, and extensions/Connection setup/Connections/Browser Use Cloud/Settings](../../ui/storybook/stories/connection-setup/browser-use-settings.stories.tsx) (current), [Apps, channels, and extensions/Connection setup/Design explorations/Connections/Agent instructions](../../ui/storybook/stories/connection-setup/connection-instructions.stories.tsx) (unreviewed), [Apps, channels, and extensions/Connection setup/Apps/Enterpret connector](../../ui/storybook/stories/connection-setup/enterpret-connect-flow.stories.tsx) (unreviewed), [Apps, channels, and extensions/Connection setup/PR reviews/Fireflies and app webhooks](../../ui/storybook/stories/connection-setup/fireflies-pr.stories.tsx) (unreviewed), [Apps, channels, and extensions/Connection setup/Apps/Connections/Memory](../../ui/storybook/stories/connection-setup/memory-connectors.stories.tsx) (unreviewed), [Apps, channels, and extensions/Connection setup/Apps/Notion MCP connect flow (PAP-16650)](../../ui/storybook/stories/connection-setup/notion-connect-flow.stories.tsx) (unreviewed)
|
||||
- `connection-setup/handoff`: [Apps, channels, and extensions/Connection setup/Apps/Managed Cloud OAuth handoff](../../ui/storybook/stories/connection-setup/managed-cloud-oauth-handoff.stories.tsx) (unreviewed)
|
||||
- `connection-setup/draft-recovery`: No cataloged story.
|
||||
- `connection-setup/task-request`: [Tasks and conversations/Questions and approvals/Chat & Comments/Connection Reviews](../../ui/storybook/stories/questions-and-approvals/connection-reviews.stories.tsx) (unreviewed), [Apps, channels, and extensions/Connection setup/Connections/In-task connections](../../ui/storybook/stories/connection-setup/in-feed-connections.stories.tsx) (unreviewed)
|
||||
- `connection-setup/channel-setup`: [Apps, channels, and extensions/Connection setup/Components/Agent setup prompt](../../ui/storybook/stories/connection-setup/agent-setup-prompt.stories.tsx) (unreviewed), [Apps, channels, and extensions/External chat and email/Connections/AgentMail setup](../../ui/storybook/stories/chat-channels/agentmail-onboarding.stories.tsx) (unreviewed), [Apps, channels, and extensions/External chat and email/Connections/GitHub and Code Review Bot](../../ui/storybook/stories/chat-channels/github-connections.stories.tsx) (unreviewed), [Apps, channels, and extensions/External chat and email/Connections/Slack/Automatic setup](../../ui/storybook/stories/chat-channels/slack-automatic-setup.stories.tsx) (unreviewed), [Apps, channels, and extensions/External chat and email/Connections/Slack/Managed setup](../../ui/storybook/stories/chat-channels/slack-managed-setup.stories.tsx) (unreviewed)
|
||||
- `connection-setup/failure`: [Apps, channels, and extensions/Connection setup/Apps/Connections/Arcade](../../ui/storybook/stories/connection-setup/ArcadeConnection.stories.tsx) (unreviewed), [Apps, channels, and extensions/Connection setup/Apps/Connections/Composio](../../ui/storybook/stories/connection-setup/ComposioConnection.stories.tsx) (unreviewed), [Apps, channels, and extensions/Connection setup/Apps/Connections/Executor](../../ui/storybook/stories/connection-setup/ExecutorConnection.stories.tsx) (unreviewed), [Apps, channels, and extensions/Connection setup/Apps/Connections/Zapier](../../ui/storybook/stories/connection-setup/ZapierConnection.stories.tsx) (unreviewed), [Apps, channels, and extensions/Connection setup/Apps/Enterpret connector](../../ui/storybook/stories/connection-setup/enterpret-connect-flow.stories.tsx) (unreviewed), [Apps, channels, and extensions/Connection setup/Apps/Notion MCP connect flow (PAP-16650)](../../ui/storybook/stories/connection-setup/notion-connect-flow.stories.tsx) (unreviewed)
|
||||
- `developer-labs/design`: No cataloged story.
|
||||
- `developer-labs/interaction-labs`: [Contributor surfaces/Developer previews and diagnostic labs/UX Labs/Converted Test Pages](../../ui/storybook/stories/developer-labs/ux-labs.stories.tsx) (unreviewed)
|
||||
- `developer-labs/performance`: No cataloged story.
|
||||
- `developer-labs/route-gates`: No cataloged story.
|
||||
- `documents-artifacts/documents`: No cataloged story.
|
||||
- `documents-artifacts/annotations`: [Tasks and conversations/Documents, attachments, and work products/Product/Documents/Annotations](../../ui/storybook/stories/documents-artifacts/document-annotations.stories.tsx) (unreviewed), [Tasks and conversations/Documents, attachments, and work products/Product/Document Comments](../../ui/storybook/stories/documents-artifacts/document-comments.stories.tsx) (unreviewed)
|
||||
- `documents-artifacts/attachments`: [Tasks and conversations/Documents, attachments, and work products/Artifact upload/01 Artifacts pane](../../ui/storybook/stories/documents-artifacts/artifact-upload/01-components.stories.tsx) (unreviewed), [Tasks and conversations/Documents, attachments, and work products/Artifact upload/02 Paused task journey](../../ui/storybook/stories/documents-artifacts/artifact-upload/02-task-journey.stories.tsx) (unreviewed), [Tasks and conversations/Documents, attachments, and work products/Components/CSV File Tab](../../ui/storybook/stories/documents-artifacts/csv-preview.stories.tsx) (unreviewed), [Tasks and conversations/Documents, attachments, and work products/Components/File Viewer Sheet](../../ui/storybook/stories/documents-artifacts/file-viewer.stories.tsx) (unreviewed), [Tasks and conversations/Documents, attachments, and work products/HTML artifacts/01 Preview](../../ui/storybook/stories/documents-artifacts/html-artifacts/01-preview.stories.tsx) (unreviewed), [Tasks and conversations/Documents, attachments, and work products/HTML artifacts/02 Workspace](../../ui/storybook/stories/documents-artifacts/html-artifacts/02-workspace.stories.tsx) (unreviewed), [Tasks and conversations/Documents, attachments, and work products/HTML artifacts/03 Task journey](../../ui/storybook/stories/documents-artifacts/html-artifacts/03-task-journey.stories.tsx) (unreviewed), [Tasks and conversations/Documents, attachments, and work products/Tasks/Text file tabs](../../ui/storybook/stories/documents-artifacts/task-text-files.stories.tsx) (unreviewed)
|
||||
- `documents-artifacts/work-products`: [Tasks and conversations/Documents, attachments, and work products/Explorations/Artifact cards/Commit](../../ui/storybook/stories/documents-artifacts/artifact-cards/commit.stories.tsx) (unreviewed), [Tasks and conversations/Documents, attachments, and work products/Explorations/Artifact cards/Data](../../ui/storybook/stories/documents-artifacts/artifact-cards/data.stories.tsx) (unreviewed), [Tasks and conversations/Documents, attachments, and work products/Explorations/Artifact cards/Document](../../ui/storybook/stories/documents-artifacts/artifact-cards/document.stories.tsx) (unreviewed), [Tasks and conversations/Documents, attachments, and work products/Explorations/Artifact cards/File](../../ui/storybook/stories/documents-artifacts/artifact-cards/file.stories.tsx) (unreviewed), [Tasks and conversations/Documents, attachments, and work products/Explorations/Artifact cards/Image](../../ui/storybook/stories/documents-artifacts/artifact-cards/image.stories.tsx) (unreviewed), [Tasks and conversations/Documents, attachments, and work products/Explorations/Artifact cards/Link preview](../../ui/storybook/stories/documents-artifacts/artifact-cards/link-preview.stories.tsx) (unreviewed), [Tasks and conversations/Documents, attachments, and work products/Explorations/Artifact cards/Pull request](../../ui/storybook/stories/documents-artifacts/artifact-cards/pull-request.stories.tsx) (unreviewed), [Tasks and conversations/Documents, attachments, and work products/Explorations/Artifact cards/Video](../../ui/storybook/stories/documents-artifacts/artifact-cards/video.stories.tsx) (unreviewed), [Tasks and conversations/Documents, attachments, and work products/Pages/Artifacts](../../ui/storybook/stories/documents-artifacts/artifacts.stories.tsx) (unreviewed), [Tasks and conversations/Documents, attachments, and work products/Task Chat/Rich Work Product Cards](../../ui/storybook/stories/documents-artifacts/rich-work-product-cards.stories.tsx) (unreviewed), [Tasks and conversations/Documents, attachments, and work products/Tasks/Artifact Gallery](../../ui/storybook/stories/documents-artifacts/task-artifact-gallery.stories.tsx) (unreviewed)
|
||||
- `documents-artifacts/library`: [Tasks and conversations/Documents, attachments, and work products/Pages/Artifacts](../../ui/storybook/stories/documents-artifacts/artifacts.stories.tsx) (unreviewed), [Tasks and conversations/Documents, attachments, and work products/Tasks/Artifact Gallery](../../ui/storybook/stories/documents-artifacts/task-artifact-gallery.stories.tsx) (unreviewed)
|
||||
- `execution-environments/configuration`: No cataloged story.
|
||||
- `execution-environments/capabilities`: No cataloged story.
|
||||
- `execution-environments/probe`: No cataloged story.
|
||||
- `execution-environments/assignment`: No cataloged story.
|
||||
- `execution-environments/images`: No cataloged story.
|
||||
- `gateways-profiles/gateway`: [Apps, channels, and extensions/Tool gateways and access profiles/Tools/Gateways (PAP-11182)](../../ui/storybook/stories/gateways-profiles/gateways-tab.stories.tsx) (unreviewed)
|
||||
- `gateways-profiles/profiles`: [Apps, channels, and extensions/Tool gateways and access profiles/Tools/Access profiles](../../ui/storybook/stories/gateways-profiles/access-profiles.stories.tsx) (unreviewed)
|
||||
- `gateways-profiles/client-connect`: [Apps, channels, and extensions/Connection setup/Components/Agent setup prompt](../../ui/storybook/stories/connection-setup/agent-setup-prompt.stories.tsx) (unreviewed), [Agents and reusable capabilities/Runs, harnesses, and model accounts/Assistant connections/Dot Runner](../../ui/storybook/stories/runs-adapters/dot-runner-connection.stories.tsx) (unreviewed), [Apps, channels, and extensions/Tool gateways and access profiles/Assistant connections/Manage connections](../../ui/storybook/stories/gateways-profiles/public-mcp-connections.stories.tsx) (current), [Apps, channels, and extensions/Tool gateways and access profiles/Assistant connections/Consent](../../ui/storybook/stories/gateways-profiles/public-mcp-consent.stories.tsx) (current), [Apps, channels, and extensions/Tool gateways and access profiles/Assistant connections/Device authorization](../../ui/storybook/stories/gateways-profiles/public-mcp-device.stories.tsx) (current), [Apps, channels, and extensions/Tool gateways and access profiles/Assistant connections/Start here](../../ui/storybook/stories/gateways-profiles/public-mcp-journey.stories.tsx) (unreviewed), [Apps, channels, and extensions/Tool gateways and access profiles/Assistant connections/Experimental setting](../../ui/storybook/stories/gateways-profiles/public-mcp-setting.stories.tsx) (unreviewed)
|
||||
- `gateways-profiles/tokens`: No cataloged story.
|
||||
- `gateways-profiles/activity`: [Oversight and operation/Dashboards and audit trails/Tools/Activity (audit feed)](../../ui/storybook/stories/activity/activity-feed.stories.tsx) (unreviewed), [Oversight and operation/Dashboards and audit trails/Apps/App detail · Activity (lifecycle)](../../ui/storybook/stories/activity/app-activity-lifecycle.stories.tsx) (unreviewed)
|
||||
- `goals/hierarchy`: [Tasks and conversations/Task creation and lifecycle/Product/Dialogs & Modals](../../ui/storybook/stories/tasks/dialogs-modals.stories.tsx) (unreviewed), [Projects and execution/Projects and repositories/Product/Projects Goals Workspaces](../../ui/storybook/stories/projects/projects-goals-workspaces.stories.tsx) (unreviewed)
|
||||
- `goals/alignment`: No cataloged story.
|
||||
- `goals/navigation`: No cataloged story.
|
||||
- `inbox-search/views`: No cataloged story.
|
||||
- `inbox-search/triage`: [Tasks and conversations/Inbox, decisions, and search/Product/Inbox/Blocked tab](../../ui/storybook/stories/inbox-search/blocked-inbox.stories.tsx) (unreviewed)
|
||||
- `inbox-search/decisions`: [Tasks and conversations/Inbox, decisions, and search/Decisions/DecisionCard](../../ui/storybook/stories/inbox-search/decision-card.stories.tsx) (unreviewed), [Tasks and conversations/Inbox, decisions, and search/Pages/Decisions Desk](../../ui/storybook/stories/inbox-search/decisions-desk.stories.tsx) (unreviewed), [Tasks and conversations/Inbox, decisions, and search/Pages/Decisions](../../ui/storybook/stories/inbox-search/what-needs-me.stories.tsx) (unreviewed)
|
||||
- `inbox-search/search`: [Tasks and conversations/Inbox, decisions, and search/Product/Search & Command K](../../ui/storybook/stories/inbox-search/search.stories.tsx) (unreviewed)
|
||||
- `instance-operations/install-update`: No cataloged story.
|
||||
- `instance-operations/server-service`: [Oversight and operation/Instance operations/Product/Dev Ops Surfaces](../../ui/storybook/stories/instance-operations/dev-ops-surfaces.stories.tsx) (unreviewed)
|
||||
- `instance-operations/configuration`: No cataloged story.
|
||||
- `instance-operations/diagnostics`: No cataloged story.
|
||||
- `instance-operations/backup`: No cataloged story.
|
||||
- `navigation-preferences/navigation`: [Oversight and operation/Navigation, profile, and announcements/Prototypes/Task detail/Mobile tabs](../../ui/storybook/stories/navigation-preferences/mobile-task-tabs.stories.tsx) (proposed), [Oversight and operation/Navigation, profile, and announcements/Product/Navigation & Layout](../../ui/storybook/stories/navigation-preferences/navigation-layout.stories.tsx) (unreviewed)
|
||||
- `navigation-preferences/preferences`: No cataloged story.
|
||||
- `navigation-preferences/profile`: [Agents and reusable capabilities/Hiring, configuration, and organization/Agents/Personas/Full pages](../../ui/storybook/stories/agents/agent-persona-pages.stories.tsx) (unreviewed), [Agents and reusable capabilities/Hiring, configuration, and organization/Agents/Personas](../../ui/storybook/stories/agents/agent-personas.stories.tsx) (unreviewed)
|
||||
- `navigation-preferences/announcements`: [Oversight and operation/Navigation, profile, and announcements/Announcements/AnnouncementCard](../../ui/storybook/stories/navigation-preferences/announcement-card.stories.tsx) (unreviewed)
|
||||
- `navigation-preferences/fallbacks`: No cataloged story.
|
||||
- `onboarding/instance`: No cataloged story.
|
||||
- `onboarding/company`: [Getting started and access/Onboarding and first work/Onboarding/Character](../../ui/storybook/stories/onboarding/onboarding-character.stories.tsx) (unreviewed)
|
||||
- `onboarding/adapter`: [Getting started and access/Onboarding and first work/Onboarding/Agent arc](../../ui/storybook/stories/onboarding/onboarding-agent-arc.stories.tsx) (unreviewed), [Getting started and access/Onboarding and first work/Onboarding/Connect a model](../../ui/storybook/stories/onboarding/onboarding-connect-model.stories.tsx) (historical)
|
||||
- `onboarding/first-task`: [Getting started and access/Onboarding and first work/Onboarding/Agent arc](../../ui/storybook/stories/onboarding/onboarding-agent-arc.stories.tsx) (unreviewed), [Getting started and access/Onboarding and first work/Onboarding/Connect a model](../../ui/storybook/stories/onboarding/onboarding-connect-model.stories.tsx) (historical)
|
||||
- `onboarding/resume`: No cataloged story.
|
||||
- `pipelines/stages`: No cataloged story.
|
||||
- `pipelines/items`: No cataloged story.
|
||||
- `pipelines/review`: No cataloged story.
|
||||
- `pipelines/learnings`: No cataloged story.
|
||||
- `plugins/installation`: No cataloged story.
|
||||
- `plugins/configuration`: No cataloged story.
|
||||
- `plugins/contributions`: No cataloged story.
|
||||
- `plugins/lifecycle`: No cataloged story.
|
||||
- `projects/lifecycle`: [Tasks and conversations/Task creation and lifecycle/Product/Dialogs & Modals](../../ui/storybook/stories/tasks/dialogs-modals.stories.tsx) (unreviewed), [Getting started and access/Login, invitations, and access/Private tasks/05 Projects](../../ui/storybook/stories/access/private-tasks/05-projects.stories.tsx) (unreviewed), [Getting started and access/Login, invitations, and access/Private tasks/06 Full product pages](../../ui/storybook/stories/access/private-tasks/06-full-product-pages.stories.tsx) (unreviewed), [Projects and execution/Projects and repositories/Product/Projects Goals Workspaces](../../ui/storybook/stories/projects/projects-goals-workspaces.stories.tsx) (unreviewed)
|
||||
- `projects/task-context`: [Tasks and conversations/Delegation, dependencies, and signoff/UX Labs/Tasks Created From a Task](../../ui/storybook/stories/task-coordination/originating-tasks.stories.tsx) (unreviewed)
|
||||
- `projects/repositories`: [Projects and execution/Projects and repositories/Proposals/Project repos/New project](../../ui/storybook/stories/projects/new-project-repos.stories.tsx) (proposed), [Projects and execution/Projects and repositories/Proposals/Project repos/Configuration](../../ui/storybook/stories/projects/project-repos-configuration.stories.tsx) (proposed)
|
||||
- `projects/defaults`: [Projects and execution/Projects and repositories/Workspaces/Project runtime provision command](../../ui/storybook/stories/projects/project-execution-workspace-strategy.stories.tsx) (unreviewed)
|
||||
- `questions-and-approvals/questions`: [Tasks and conversations/Questions and approvals/Composer/Question flow](../../ui/storybook/stories/questions-and-approvals/composer-question-flow.stories.tsx) (unreviewed), [Tasks and conversations/Questions and approvals/Chat & Comments/Issue Thread Interactions](../../ui/storybook/stories/questions-and-approvals/issue-thread-interactions.stories.tsx) (unreviewed), [Tasks and conversations/Questions and approvals/Chat & Comments/Rich ACP interactions](../../ui/storybook/stories/questions-and-approvals/rich-acp-interactions.stories.tsx) (unreviewed), [Tasks and conversations/Questions and approvals/Composer/Interaction above composer](../../ui/storybook/stories/questions-and-approvals/task-chat-interaction-above-composer.stories.tsx) (current)
|
||||
- `questions-and-approvals/dismiss-and-reopen`: [Tasks and conversations/Questions and approvals/Chat & Comments/Agent Chat Unanswered Questions](../../ui/storybook/stories/questions-and-approvals/agent-chat-unanswered-questions.stories.tsx) (unreviewed), [Tasks and conversations/Questions and approvals/Chat & Comments/Task Chat Unanswered Questions](../../ui/storybook/stories/questions-and-approvals/task-chat-unanswered-questions.stories.tsx) (unreviewed)
|
||||
- `questions-and-approvals/confirmations`: [Tasks and conversations/Questions and approvals/Chat & Comments/Issue Thread Interactions](../../ui/storybook/stories/questions-and-approvals/issue-thread-interactions.stories.tsx) (unreviewed), [Tasks and conversations/Questions and approvals/Chat & Comments/Rich ACP interactions](../../ui/storybook/stories/questions-and-approvals/rich-acp-interactions.stories.tsx) (unreviewed), [Tasks and conversations/Questions and approvals/Composer/Interaction above composer](../../ui/storybook/stories/questions-and-approvals/task-chat-interaction-above-composer.stories.tsx) (current)
|
||||
- `questions-and-approvals/plan-review`: [Tasks and conversations/Questions and approvals/Issue Detail/Plan Decompositions](../../ui/storybook/stories/questions-and-approvals/issue-plan-decompositions.stories.tsx) (unreviewed)
|
||||
- `questions-and-approvals/audience`: [Tasks and conversations/Questions and approvals/Chat & Comments/Issue Thread Interactions](../../ui/storybook/stories/questions-and-approvals/issue-thread-interactions.stories.tsx) (unreviewed), [Tasks and conversations/Questions and approvals/Chat & Comments/Rich ACP interactions](../../ui/storybook/stories/questions-and-approvals/rich-acp-interactions.stories.tsx) (unreviewed), [Tasks and conversations/Questions and approvals/Composer/Interaction above composer](../../ui/storybook/stories/questions-and-approvals/task-chat-interaction-above-composer.stories.tsx) (current)
|
||||
- `questions-and-approvals/governance`: [Tasks and conversations/Task creation and lifecycle/Product/Control Plane Surfaces](../../ui/storybook/stories/tasks/control-plane-surfaces.stories.tsx) (unreviewed)
|
||||
- `questions-and-approvals/tool-review`: [Tasks and conversations/Questions and approvals/Chat & Comments/Connection Reviews](../../ui/storybook/stories/questions-and-approvals/connection-reviews.stories.tsx) (unreviewed)
|
||||
- `questions-and-approvals/continuation`: No cataloged story.
|
||||
- `recovery/diagnosis`: [Tasks and conversations/Task creation and lifecycle/Paperclip/Assigned Backlog Safeguards](../../ui/storybook/stories/tasks/assigned-backlog-safeguards.stories.tsx) (unreviewed), [Tasks and conversations/Inbox, decisions, and search/Product/Inbox/Blocked tab](../../ui/storybook/stories/inbox-search/blocked-inbox.stories.tsx) (unreviewed), [Apps, channels, and extensions/Secrets and proposals/Task credential access/01 Notice](../../ui/storybook/stories/secrets/credential-access/01-notice.stories.tsx) (unreviewed), [Apps, channels, and extensions/Secrets and proposals/Task credential access/02 Task page](../../ui/storybook/stories/secrets/credential-access/02-task-page.stories.tsx) (unreviewed), [Projects and execution/Recovery/Task chat/Recovery notice](../../ui/storybook/stories/recovery/execution-blocker-notice.stories.tsx) (unreviewed), [Projects and execution/Recovery/Tasks/Execution recovery](../../ui/storybook/stories/recovery/execution-recovery.stories.tsx) (unreviewed), [Projects and execution/Recovery/Product/Issue/Blocked notice](../../ui/storybook/stories/recovery/issue-blocked-notice.stories.tsx) (unreviewed), [Projects and execution/Recovery/Design previews/Recovery notice](../../ui/storybook/stories/recovery/recovery-notice-preview.stories.tsx) (current), [Projects and execution/Recovery/Paperclip/Source Issue Recovery](../../ui/storybook/stories/recovery/source-issue-recovery.stories.tsx) (unreviewed), [Projects and execution/Recovery/Paperclip/Source-resolved Fold](../../ui/storybook/stories/recovery/source-resolved-fold.stories.tsx) (unreviewed), [Tasks and conversations/Task creation and lifecycle/Paperclip/Successful Run Handoff](../../ui/storybook/stories/tasks/successful-run-handoff.stories.tsx) (unreviewed)
|
||||
- `recovery/bounded-continuation`: [Projects and execution/Recovery/Tasks/Model capacity retry](../../ui/storybook/stories/recovery/model-capacity-retry.stories.tsx) (unreviewed), [Projects and execution/Recovery/Product/Issue Scheduled retry surfaces](../../ui/storybook/stories/recovery/scheduled-retry.stories.tsx) (unreviewed)
|
||||
- `recovery/source-action`: [Projects and execution/Recovery/Design previews/Recovery notice](../../ui/storybook/stories/recovery/recovery-notice-preview.stories.tsx) (current), [Projects and execution/Recovery/Paperclip/Source Issue Recovery](../../ui/storybook/stories/recovery/source-issue-recovery.stories.tsx) (unreviewed), [Projects and execution/Recovery/Paperclip/Source-resolved Fold](../../ui/storybook/stories/recovery/source-resolved-fold.stories.tsx) (unreviewed)
|
||||
- `recovery/exhaustion`: No cataloged story.
|
||||
- `recovery/workspace`: [Projects and execution/Recovery/Workspace branch recovery/01 Repair card](../../ui/storybook/stories/recovery/workspace-branch-recovery/01-component.stories.tsx) (unreviewed), [Projects and execution/Recovery/Workspace branch recovery/02 Task journey](../../ui/storybook/stories/recovery/workspace-branch-recovery/02-task-journey.stories.tsx) (unreviewed)
|
||||
- `recovery/unknown-outcome`: No cataloged story.
|
||||
- `recovery/user-continuation`: No cataloged story.
|
||||
- `recovery/reconnect`: [Projects and execution/Recovery/App/Connection recovery](../../ui/storybook/stories/recovery/cloud-access-recovery.stories.tsx) (unreviewed)
|
||||
- `routines/definition`: [Automation and structured work/Routines, schedules, and triggers/Product/Routines · Detail (variation C)](../../ui/storybook/stories/routines/routine-detail-c.stories.tsx) (unreviewed), [Automation and structured work/Routines, schedules, and triggers/Product/Routines · Secrets tab](../../ui/storybook/stories/routines/routine-secrets.stories.tsx) (unreviewed), [Automation and structured work/Routines, schedules, and triggers/Product/Routines · List (grouped cards)](../../ui/storybook/stories/routines/routines-list-groups.stories.tsx) (unreviewed)
|
||||
- `routines/triggers`: [Apps, channels, and extensions/Connection setup/PR reviews/Fireflies and app webhooks](../../ui/storybook/stories/connection-setup/fireflies-pr.stories.tsx) (unreviewed), [Automation and structured work/Routines, schedules, and triggers/Product/Routines/Webhooks UX preview](../../ui/storybook/stories/routines/routine-webhooks-ux.stories.tsx) (unreviewed), [Automation and structured work/Routines, schedules, and triggers/Product/Routines/Webhooks](../../ui/storybook/stories/routines/routine-webhooks.stories.tsx) (unreviewed)
|
||||
- `routines/run-now`: No cataloged story.
|
||||
- `routines/history`: No cataloged story.
|
||||
- `routines/management`: [Automation and structured work/Routines, schedules, and triggers/Product/Routines · Detail (variation C)](../../ui/storybook/stories/routines/routine-detail-c.stories.tsx) (unreviewed), [Automation and structured work/Routines, schedules, and triggers/Product/Routines · List (grouped cards)](../../ui/storybook/stories/routines/routines-list-groups.stories.tsx) (unreviewed)
|
||||
- `runs-adapters/availability`: [Tasks and conversations/Task creation and lifecycle/Composer/Mobile assignee picker](../../ui/storybook/stories/tasks/composer-mobile-assignee.stories.tsx) (unreviewed), [Tasks and conversations/Task creation and lifecycle/Composer/Model and effort picker](../../ui/storybook/stories/tasks/composer-model-picker.stories.tsx) (mixed), [Agents and reusable capabilities/Hiring, configuration, and organization/Codex runner/Creation](../../ui/storybook/stories/agents/codex-runner-creation.stories.tsx) (unreviewed)
|
||||
- `runs-adapters/validation`: [Agents and reusable capabilities/Runs, harnesses, and model accounts/Onboarding/Saved connections](../../ui/storybook/stories/runs-adapters/agent-connection-reuse.stories.tsx) (current), [Agents and reusable capabilities/Runs, harnesses, and model accounts/AI Connections/Usage limits](../../ui/storybook/stories/runs-adapters/ai-connection-usage.stories.tsx) (unreviewed), [Agents and reusable capabilities/Runs, harnesses, and model accounts/AI Connections/Review](../../ui/storybook/stories/runs-adapters/ai-connections.stories.tsx) (unreviewed), [Agents and reusable capabilities/Runs, harnesses, and model accounts/Connectors/Pool host](../../ui/storybook/stories/runs-adapters/connection-pool-connector.stories.tsx) (unreviewed), [Agents and reusable capabilities/Runs, harnesses, and model accounts/AI Connections/Connection pools](../../ui/storybook/stories/runs-adapters/connection-pools.stories.tsx) (unreviewed), [Agents and reusable capabilities/Runs, harnesses, and model accounts/Assistant connections/Dot Runner](../../ui/storybook/stories/runs-adapters/dot-runner-connection.stories.tsx) (unreviewed), [Agents and reusable capabilities/Runs, harnesses, and model accounts/AI Connections/Provider routing/07 Subscription sign-in](../../ui/storybook/stories/runs-adapters/local-ai-subscription.stories.tsx) (unreviewed), [Agents and reusable capabilities/Runs, harnesses, and model accounts/AI Connections/Provider routing/00 Overview](../../ui/storybook/stories/runs-adapters/provider-routing-00-overview.stories.tsx) (unreviewed), [Agents and reusable capabilities/Runs, harnesses, and model accounts/AI Connections/Provider routing/01 Onboarding](../../ui/storybook/stories/runs-adapters/provider-routing-01-onboarding.stories.tsx) (mixed), [Agents and reusable capabilities/Runs, harnesses, and model accounts/AI Connections/Provider routing/02 Connect](../../ui/storybook/stories/runs-adapters/provider-routing-02-connect.stories.tsx) (mixed), [Agents and reusable capabilities/Runs, harnesses, and model accounts/AI Connections/Provider routing/03 Agent](../../ui/storybook/stories/runs-adapters/provider-routing-03-agent.stories.tsx) (mixed), [Agents and reusable capabilities/Runs, harnesses, and model accounts/AI Connections/Provider routing/04 Manage](../../ui/storybook/stories/runs-adapters/provider-routing-04-manage.stories.tsx) (mixed), [Agents and reusable capabilities/Runs, harnesses, and model accounts/AI Connections/Provider routing/05 Recovery](../../ui/storybook/stories/runs-adapters/provider-routing-05-recovery.stories.tsx) (mixed), [Agents and reusable capabilities/Runs, harnesses, and model accounts/AI Connections/Provider routing/06 Production components](../../ui/storybook/stories/runs-adapters/provider-routing-06-production.stories.tsx) (unreviewed), [Agents and reusable capabilities/Hiring, configuration, and organization/Codex runner/Creation](../../ui/storybook/stories/agents/codex-runner-creation.stories.tsx) (unreviewed)
|
||||
- `runs-adapters/runs`: [Tasks and conversations/Task creation and lifecycle/Product/Chat & Comments](../../ui/storybook/stories/tasks/chat-comments.stories.tsx) (unreviewed), [Tasks and conversations/Task creation and lifecycle/Tasks/Completed activity preview](../../ui/storybook/stories/tasks/completed-activity.stories.tsx) (current), [Tasks and conversations/Task creation and lifecycle/Tasks/Runner activity preview](../../ui/storybook/stories/tasks/runner-activity.stories.tsx) (current), [Tasks and conversations/Task creation and lifecycle/Tasks/Task chat review fixes](../../ui/storybook/stories/tasks/task-chat-chain-of-thought.stories.tsx) (unreviewed)
|
||||
- `runs-adapters/session`: [Agents and reusable capabilities/Runs, harnesses, and model accounts/Composer/Session goals](../../ui/storybook/stories/runs-adapters/session-goals.stories.tsx) (unreviewed)
|
||||
- `secrets/company-secrets`: [Apps, channels, and extensions/Secrets and proposals/Product/Secrets](../../ui/storybook/stories/secrets/secrets.stories.tsx) (unreviewed)
|
||||
- `secrets/user-secrets`: [Apps, channels, and extensions/Secrets and proposals/Product/User secrets](../../ui/storybook/stories/secrets/user-secrets.stories.tsx) (unreviewed)
|
||||
- `secrets/proposals`: No cataloged story.
|
||||
- `secrets/vault-import`: [Apps, channels, and extensions/Secrets and proposals/Product/Secrets](../../ui/storybook/stories/secrets/secrets.stories.tsx) (unreviewed)
|
||||
- `secrets/runtime-delivery`: [Apps, channels, and extensions/Secrets and proposals/Task credential access/01 Notice](../../ui/storybook/stories/secrets/credential-access/01-notice.stories.tsx) (unreviewed), [Apps, channels, and extensions/Secrets and proposals/Task credential access/02 Task page](../../ui/storybook/stories/secrets/credential-access/02-task-page.stories.tsx) (unreviewed), [Apps, channels, and extensions/Secrets and proposals/Product/Environment Variables Editor](../../ui/storybook/stories/secrets/environment-variables-editor.stories.tsx) (unreviewed), [Automation and structured work/Routines, schedules, and triggers/Product/Routines · Secrets tab](../../ui/storybook/stories/routines/routine-secrets.stories.tsx) (unreviewed)
|
||||
- `skills/discovery`: [Agents and reusable capabilities/Skills and Skill Studio/Skills/GitHub sync journey](../../ui/storybook/stories/skills/github-skills-journey.stories.tsx) (unreviewed), [Agents and reusable capabilities/Skills and Skill Studio/Skills/GitHub synced skills](../../ui/storybook/stories/skills/github-synced-skills.stories.tsx) (unreviewed), [Agents and reusable capabilities/Skills and Skill Studio/Skills/Repository packages](../../ui/storybook/stories/skills/repository-skill-packages/repository-skill-packages.stories.tsx) (unreviewed), [Agents and reusable capabilities/Skills and Skill Studio/Skills/Import progress](../../ui/storybook/stories/skills/skill-import-progress.stories.tsx) (unreviewed), [Agents and reusable capabilities/Skills and Skill Studio/Skills Store/Skill detail](../../ui/storybook/stories/skills/skills-store-detail.stories.tsx) (unreviewed), [Agents and reusable capabilities/Skills and Skill Studio/Skills Store/Discovery grid](../../ui/storybook/stories/skills/skills-store-discovery.stories.tsx) (unreviewed)
|
||||
- `skills/authoring`: [Agents and reusable capabilities/Skills and Skill Studio/Skill Studio/EditACopy](../../ui/storybook/stories/skills/fork-skill-flow.stories.tsx) (unreviewed), [Agents and reusable capabilities/Skills and Skill Studio/Product/Skill Studio · FrontmatterPanel](../../ui/storybook/stories/skills/frontmatter-panel.stories.tsx) (unreviewed)
|
||||
- `skills/versions`: [Agents and reusable capabilities/Skills and Skill Studio/Skills/GitHub sync journey](../../ui/storybook/stories/skills/github-skills-journey.stories.tsx) (unreviewed), [Agents and reusable capabilities/Skills and Skill Studio/Skills/GitHub synced skills](../../ui/storybook/stories/skills/github-synced-skills.stories.tsx) (unreviewed), [Agents and reusable capabilities/Skills and Skill Studio/Skills/Repository packages](../../ui/storybook/stories/skills/repository-skill-packages/repository-skill-packages.stories.tsx) (unreviewed), [Agents and reusable capabilities/Skills and Skill Studio/Skills/Import progress](../../ui/storybook/stories/skills/skill-import-progress.stories.tsx) (unreviewed)
|
||||
- `skills/policy`: [Agents and reusable capabilities/Skills and Skill Studio/Agent Skills/AgentSkillRow](../../ui/storybook/stories/skills/agent-skill-row.stories.tsx) (unreviewed), [Agents and reusable capabilities/Skills and Skill Studio/QA / Agent skills edge states](../../ui/storybook/stories/skills/agent-skills-edge.stories.tsx) (unreviewed), [Agents and reusable capabilities/Skills and Skill Studio/Skill Studio/AgentsUsingSkill](../../ui/storybook/stories/skills/agents-using-skill.stories.tsx) (unreviewed), [Agents and reusable capabilities/Skills and Skill Studio/Product/Skills/Policy surfaces](../../ui/storybook/stories/skills/skill-policy-surfaces.stories.tsx) (unreviewed)
|
||||
- `skills/testing`: No cataloged story.
|
||||
- `status-cards/authoring`: No cataloged story.
|
||||
- `status-cards/generation`: No cataloged story.
|
||||
- `status-cards/inspection`: No cataloged story.
|
||||
- `status-cards/archive`: No cataloged story.
|
||||
- `steering/queue`: [Tasks and conversations/Steering and queued messages/Composer/Steering and queued messages](../../ui/storybook/stories/steering/composer-queued-messages.stories.tsx) (current)
|
||||
- `steering/edit-order-discard`: [Tasks and conversations/Steering and queued messages/Composer/Steering and queued messages](../../ui/storybook/stories/steering/composer-queued-messages.stories.tsx) (current)
|
||||
- `steering/steer`: [Tasks and conversations/Steering and queued messages/Composer/Steering and queued messages](../../ui/storybook/stories/steering/composer-queued-messages.stories.tsx) (current)
|
||||
- `steering/interrupt`: [Tasks and conversations/Steering and queued messages/Composer/Steering and queued messages](../../ui/storybook/stories/steering/composer-queued-messages.stories.tsx) (current), [Tasks and conversations/Steering and queued messages/Surfaces/Interrupt Handoff](../../ui/storybook/stories/steering/interrupt-handoff.stories.tsx) (unreviewed)
|
||||
- `steering/stop-and-pause`: [Tasks and conversations/Steering and queued messages/Composer/Paused task takeover](../../ui/storybook/stories/steering/paused-composer.stories.tsx) (historical), [Tasks and conversations/Steering and queued messages/Composer/Execution controls](../../ui/storybook/stories/steering/task-execution-controls.stories.tsx) (current)
|
||||
- `steering/drafts`: No cataloged story.
|
||||
- `steering/approval-queue`: No cataloged story.
|
||||
- `task-coordination/delegation`: [Tasks and conversations/Questions and approvals/Issue Detail/Plan Decompositions](../../ui/storybook/stories/questions-and-approvals/issue-plan-decompositions.stories.tsx) (unreviewed), [Tasks and conversations/Delegation, dependencies, and signoff/UX Labs/Tasks Created From a Task](../../ui/storybook/stories/task-coordination/originating-tasks.stories.tsx) (unreviewed), [Tasks and conversations/Delegation, dependencies, and signoff/UX Labs/Sub-issues Workflow Checklist](../../ui/storybook/stories/task-coordination/sub-issues-workflow.stories.tsx) (unreviewed)
|
||||
- `task-coordination/dependencies`: [Projects and execution/Recovery/Product/Issue/Blocked notice](../../ui/storybook/stories/recovery/issue-blocked-notice.stories.tsx) (unreviewed), [Tasks and conversations/Delegation, dependencies, and signoff/UX Labs/Sub-issues Workflow Checklist](../../ui/storybook/stories/task-coordination/sub-issues-workflow.stories.tsx) (unreviewed)
|
||||
- `task-coordination/signoff`: [Tasks and conversations/Delegation, dependencies, and signoff/Product/Decisions/Stalled review actions](../../ui/storybook/stories/task-coordination/issue-review-panel.stories.tsx) (unreviewed)
|
||||
- `task-coordination/tree-control`: [Tasks and conversations/Delegation, dependencies, and signoff/Product/Issue Monitor surfaces](../../ui/storybook/stories/task-coordination/monitor-surfaces.stories.tsx) (unreviewed), [Tasks and conversations/Delegation, dependencies, and signoff/Product/Task Watchdog surfaces](../../ui/storybook/stories/task-coordination/task-watchdog-surfaces.stories.tsx) (unreviewed)
|
||||
- `tasks/create`: [Tasks and conversations/Task creation and lifecycle/Composer/Add menu](../../ui/storybook/stories/tasks/composer-add-menu.stories.tsx) (unreviewed), [Tasks and conversations/Task creation and lifecycle/Composer/Mobile assignee picker](../../ui/storybook/stories/tasks/composer-mobile-assignee.stories.tsx) (unreviewed), [Tasks and conversations/Task creation and lifecycle/Composer/Model and effort picker](../../ui/storybook/stories/tasks/composer-model-picker.stories.tsx) (mixed), [Tasks and conversations/Task creation and lifecycle/Product/Dialogs & Modals](../../ui/storybook/stories/tasks/dialogs-modals.stories.tsx) (unreviewed), [Tasks and conversations/Task creation and lifecycle/Components/Entity pickers/Mobile](../../ui/storybook/stories/tasks/mobile-entity-pickers.stories.tsx) (unreviewed), [Tasks and conversations/Task creation and lifecycle/Composer/New task](../../ui/storybook/stories/tasks/new-task-composer.stories.tsx) (current), [Agents and reusable capabilities/Hiring, configuration, and organization/Primary Agent/00 Review guide](../../ui/storybook/stories/agents/primary-agent/00-review-guide.stories.tsx) (unreviewed), [Agents and reusable capabilities/Hiring, configuration, and organization/Primary Agent/01 Components](../../ui/storybook/stories/agents/primary-agent/01-components.stories.tsx) (unreviewed), [Agents and reusable capabilities/Hiring, configuration, and organization/Primary Agent/02 Sidebar](../../ui/storybook/stories/agents/primary-agent/02-sidebar.stories.tsx) (unreviewed), [Agents and reusable capabilities/Hiring, configuration, and organization/Primary Agent/03 Agents](../../ui/storybook/stories/agents/primary-agent/03-agents.stories.tsx) (unreviewed), [Agents and reusable capabilities/Hiring, configuration, and organization/Primary Agent/04 New task](../../ui/storybook/stories/agents/primary-agent/04-new-task.stories.tsx) (unreviewed), [Agents and reusable capabilities/Hiring, configuration, and organization/Primary Agent/05 Chat](../../ui/storybook/stories/agents/primary-agent/05-chat.stories.tsx) (unreviewed), [Agents and reusable capabilities/Hiring, configuration, and organization/Primary Agent/06 Journeys](../../ui/storybook/stories/agents/primary-agent/06-journeys.stories.tsx) (unreviewed), [Agents and reusable capabilities/Hiring, configuration, and organization/Primary Agent/07 Confirmation](../../ui/storybook/stories/agents/primary-agent/07-confirmation.stories.tsx) (unreviewed)
|
||||
- `tasks/organize`: [Tasks and conversations/Task creation and lifecycle/Product/Control Plane Surfaces](../../ui/storybook/stories/tasks/control-plane-surfaces.stories.tsx) (unreviewed), [Tasks and conversations/Task creation and lifecycle/Product/Issue Management](../../ui/storybook/stories/tasks/issue-management.stories.tsx) (unreviewed)
|
||||
- `tasks/properties`: [Tasks and conversations/Task creation and lifecycle/External Objects](../../ui/storybook/stories/tasks/external-objects.stories.tsx) (unreviewed), [Tasks and conversations/Task creation and lifecycle/Product/Issue Management](../../ui/storybook/stories/tasks/issue-management.stories.tsx) (unreviewed), [Getting started and access/Login, invitations, and access/Private tasks/00 Review guide](../../ui/storybook/stories/access/private-tasks/00-review-guide.stories.tsx) (unreviewed), [Getting started and access/Login, invitations, and access/Private tasks/01 Creation](../../ui/storybook/stories/access/private-tasks/01-creation.stories.tsx) (unreviewed), [Getting started and access/Login, invitations, and access/Private tasks/02 Sharing](../../ui/storybook/stories/access/private-tasks/02-sharing.stories.tsx) (unreviewed), [Getting started and access/Login, invitations, and access/Private tasks/03 Task actions](../../ui/storybook/stories/access/private-tasks/03-task-actions.stories.tsx) (unreviewed), [Getting started and access/Login, invitations, and access/Private tasks/04 References and blockers](../../ui/storybook/stories/access/private-tasks/04-references-and-blockers.stories.tsx) (unreviewed), [Getting started and access/Login, invitations, and access/Private tasks/05 Projects](../../ui/storybook/stories/access/private-tasks/05-projects.stories.tsx) (unreviewed), [Getting started and access/Login, invitations, and access/Private tasks/06 Full product pages](../../ui/storybook/stories/access/private-tasks/06-full-product-pages.stories.tsx) (unreviewed), [Getting started and access/Login, invitations, and access/Private tasks/07 User stories](../../ui/storybook/stories/access/private-tasks/07-user-stories.stories.tsx) (unreviewed)
|
||||
- `tasks/discussion`: [Projects and execution/Workspaces, services, and files/Tasks/Browser Use/Task activity](../../ui/storybook/stories/workspaces/browser-use-activity.stories.tsx) (unreviewed), [Projects and execution/Workspaces, services, and files/Tasks/Browser Use/Footer](../../ui/storybook/stories/workspaces/browser-use-footer.stories.tsx) (unreviewed), [Projects and execution/Workspaces, services, and files/Tasks/Browser Use/Panel](../../ui/storybook/stories/workspaces/browser-use.stories.tsx) (unreviewed), [Tasks and conversations/Task creation and lifecycle/Product/Chat & Comments](../../ui/storybook/stories/tasks/chat-comments.stories.tsx) (unreviewed), [Tasks and conversations/Task creation and lifecycle/Tasks/Completed activity preview](../../ui/storybook/stories/tasks/completed-activity.stories.tsx) (current), [Tasks and conversations/Task creation and lifecycle/Tasks/Runner activity preview](../../ui/storybook/stories/tasks/runner-activity.stories.tsx) (current), [Tasks and conversations/Task creation and lifecycle/Tasks/Task chat review fixes](../../ui/storybook/stories/tasks/task-chat-chain-of-thought.stories.tsx) (unreviewed), [Tasks and conversations/Task creation and lifecycle/Task chat/Mobile streaming follow](../../ui/storybook/stories/tasks/task-chat-mobile-follow.stories.tsx) (unreviewed), [Tasks and conversations/Task creation and lifecycle/Product/Task chat/System notice](../../ui/storybook/stories/tasks/task-chat-system-notice.stories.tsx) (unreviewed), [Tasks and conversations/Questions and approvals/Chat & Comments/Task Chat Unanswered Questions](../../ui/storybook/stories/questions-and-approvals/task-chat-unanswered-questions.stories.tsx) (unreviewed), [Tasks and conversations/Task creation and lifecycle/Tasks/Context notices](../../ui/storybook/stories/tasks/task-context-notices.stories.tsx) (unreviewed), [Tasks and conversations/Task creation and lifecycle/Chat & Comments/Human identity/00 Review guide](../../ui/storybook/stories/tasks/thread-human-identity/00-evidence.stories.tsx) (unreviewed), [Tasks and conversations/Task creation and lifecycle/Chat & Comments/Human identity/01 Components](../../ui/storybook/stories/tasks/thread-human-identity/01-components.stories.tsx) (unreviewed), [Tasks and conversations/Task creation and lifecycle/Chat & Comments/Human identity/02 Mixed-author journey](../../ui/storybook/stories/tasks/thread-human-identity/02-task-journey.stories.tsx) (unreviewed)
|
||||
- `tasks/completion`: [Tasks and conversations/Task creation and lifecycle/Paperclip/Assigned Backlog Safeguards](../../ui/storybook/stories/tasks/assigned-backlog-safeguards.stories.tsx) (unreviewed), [Tasks and conversations/Task creation and lifecycle/Paperclip/Successful Run Handoff](../../ui/storybook/stories/tasks/successful-run-handoff.stories.tsx) (unreviewed)
|
||||
- `teams/discovery`: [Agents and reusable capabilities/Team packages and installation/Surfaces/Team Catalog](../../ui/storybook/stories/teams/team-catalog.stories.tsx) (unreviewed)
|
||||
- `teams/preview`: [Agents and reusable capabilities/Team packages and installation/Surfaces/Team Catalog](../../ui/storybook/stories/teams/team-catalog.stories.tsx) (unreviewed)
|
||||
- `teams/install`: No cataloged story.
|
||||
- `teams/configuration`: No cataloged story.
|
||||
- `workspaces/binding`: [Projects and execution/Projects and repositories/Product/Projects Goals Workspaces](../../ui/storybook/stories/projects/projects-goals-workspaces.stories.tsx) (unreviewed), [Projects and execution/Workspaces, services, and files/Components/SearchableSelect/Workspace picker](../../ui/storybook/stories/workspaces/searchable-select.stories.tsx) (unreviewed)
|
||||
- `workspaces/provisioning`: [Projects and execution/Projects and repositories/Workspaces/Project runtime provision command](../../ui/storybook/stories/projects/project-execution-workspace-strategy.stories.tsx) (unreviewed), [Projects and execution/Workspaces, services, and files/Workspaces/Runtime provisioning status](../../ui/storybook/stories/workspaces/workspace-runtime-provisioning.stories.tsx) (unreviewed)
|
||||
- `workspaces/services`: [Projects and execution/Workspaces, services, and files/Tasks/Browser Use/Task activity](../../ui/storybook/stories/workspaces/browser-use-activity.stories.tsx) (unreviewed), [Projects and execution/Workspaces, services, and files/Tasks/Browser Use/Footer](../../ui/storybook/stories/workspaces/browser-use-footer.stories.tsx) (unreviewed), [Projects and execution/Workspaces, services, and files/Tasks/Browser Use/Panel](../../ui/storybook/stories/workspaces/browser-use.stories.tsx) (unreviewed), [Projects and execution/Workspaces, services, and files/Workspaces/Service control bar](../../ui/storybook/stories/workspaces/workspace-service-control-bar.stories.tsx) (unreviewed)
|
||||
- `workspaces/files-git`: [Tasks and conversations/Documents, attachments, and work products/HTML artifacts/01 Preview](../../ui/storybook/stories/documents-artifacts/html-artifacts/01-preview.stories.tsx) (unreviewed), [Tasks and conversations/Documents, attachments, and work products/HTML artifacts/02 Workspace](../../ui/storybook/stories/documents-artifacts/html-artifacts/02-workspace.stories.tsx) (unreviewed), [Tasks and conversations/Documents, attachments, and work products/HTML artifacts/03 Task journey](../../ui/storybook/stories/documents-artifacts/html-artifacts/03-task-journey.stories.tsx) (unreviewed), [Projects and execution/Workspaces, services, and files/Issue/Workspace File Browser](../../ui/storybook/stories/workspaces/workspace-file-browser.stories.tsx) (unreviewed)
|
||||
- `workspaces/closure`: No cataloged story.
|
||||
@@ -0,0 +1,91 @@
|
||||
# Storybook map
|
||||
|
||||
The [feature index](INDEX.md) connects the existing Storybook to stable capability
|
||||
IDs from the upstream [feature map](../feature-map/README.md). Story files remain
|
||||
in `ui/storybook/`. The feature map and its maintenance skill do not depend on
|
||||
this catalog. New capabilities or upstream recipe drift are reported to their
|
||||
maintainer; this catalog does not redefine product behavior or permissions.
|
||||
|
||||
## Ownership
|
||||
|
||||
- [maintain-storybook](../../.agents/skills/maintain-storybook/SKILL.md) owns story
|
||||
alignment, fixture corrections, classification, and this catalog.
|
||||
- [review-storybook](../../.agents/skills/review-storybook/SKILL.md) consumes it to
|
||||
select and exercise examples, then reports evidence and drift.
|
||||
- [DESIGN.md](../../DESIGN.md) governs design. Production verification remains
|
||||
with the existing product recipes and verification tools.
|
||||
|
||||
## Catalog contract
|
||||
|
||||
`catalog.json` is the maintained inventory, with one entry per story file.
|
||||
`INDEX.md` is generated from it and the installed Storybook CSF parser. Titles,
|
||||
export filtering, story counts and IDs come from source, not filename guesses.
|
||||
All examples are retained, including historical suites. A file's first capability
|
||||
is its primary home; remaining capabilities are lookup references, not copies.
|
||||
Foundations/guides and proposals without a mapped capability need an exception
|
||||
reason. An absent story is not automatically a feature gap.
|
||||
|
||||
## Folders and sidebar
|
||||
|
||||
Story files live in `ui/storybook/stories/<primary-feature-area>/`, using the first
|
||||
capability's area slug. Related capabilities still point to the same story. Shared
|
||||
fixture modules and assets live in `ui/storybook/support/`; production wrappers in
|
||||
`ui/storybook/prototypes/` retain their existing ownership.
|
||||
|
||||
The sidebar follows the section and feature names in the feature-map README, then
|
||||
retains each suite's useful scenario grouping. Foundations contains shared component
|
||||
examples with no primary product capability. Overview contains the catalog guide.
|
||||
Areas with no stories do not get empty sidebar folders.
|
||||
|
||||
Each suite declares an explicit CSF `id`, recorded as `stableId` in the catalog.
|
||||
The `sidebarTitle` can change without changing that identity. Existing story and
|
||||
autodocs URLs and screenshot names remain valid. Do not derive a replacement ID
|
||||
from a newly organized title. The checker rejects missing or changed explicit IDs,
|
||||
incorrect feature folders, and sidebar titles outside their primary feature home.
|
||||
|
||||
Each entry records purpose, capability references, review sources/date/fingerprint,
|
||||
notes, and three independent dimensions:
|
||||
|
||||
| Dimension | Values |
|
||||
| --- | --- |
|
||||
| Product status | `current`: source comparison supports the named subject; `proposed`: intentional alternate design; `historical`: superseded reference retained for its original purpose; `mixed`: suite combines these; `unreviewed`: parity has not been established |
|
||||
| Implementation | `production`: shipped subject with fixture/layout wrappers; `prototype`: locally implemented subject, even if it uses shared primitives; `mixed`: both subjects; `guide`: explanatory documentation |
|
||||
| Data boundary | `fixtures`: props, callbacks, caches or API responses are simulated; `fixture-server`: requires a separately owned local API/database fixture with simulated upstream providers; `static`: explanatory/static examples |
|
||||
|
||||
`current` is a source-review classification, not a promise that every state renders
|
||||
or that APIs, gates, persistence and providers passed live tests. `unreviewed`
|
||||
entries still have an inventory/classification review; notes explain the deeper
|
||||
parity work outstanding. Mixed files retain a note naming the important split.
|
||||
Production components can legitimately use mock data. A prototype directory name
|
||||
does not establish implementation: some wrappers there re-export shipped UI.
|
||||
|
||||
Review sources include the story, relevant local support and production subjects.
|
||||
Include directly rendered subjects regardless of import spelling, and primitives
|
||||
when the suite demonstrates them. Do not truncate the list to a fixed source count.
|
||||
Fingerprints make changes to those declared dependencies visible. They do not
|
||||
follow every transitive production import or infer semantic correctness. Shared
|
||||
preview/config/fixture changes have a separate fingerprint and require reviewing
|
||||
affected callers. Update fingerprints only after reviewing the change, and add
|
||||
missing dependencies when discovered. Date/revision context for the initial pass
|
||||
and executed evidence belong in [REVIEW.md](REVIEW.md).
|
||||
|
||||
## Maintenance commands
|
||||
|
||||
After installing workspace dependencies:
|
||||
|
||||
```sh
|
||||
node scripts/storybook-catalog.mjs
|
||||
node scripts/storybook-catalog.mjs --write
|
||||
node --test scripts/storybook-catalog.test.mjs
|
||||
```
|
||||
|
||||
The checker rejects unaccounted or deleted stories, invalid capability references,
|
||||
duplicate entries/IDs, missing review sources, changed fingerprints and a stale
|
||||
generated index. `--write` only regenerates the index; it never refreshes review
|
||||
fingerprints or blesses drift. Preserve IDs when changing titles or moving files;
|
||||
compare the existing built Storybook index before and after such changes.
|
||||
|
||||
Use the existing [Storybook workflow](../../doc/DEVELOPING.md#storybook) for builds,
|
||||
browser review, and visual tests. Report source inspection, rendering, `play`
|
||||
assertions, visual comparisons and production acceptance separately. Do not publish
|
||||
new visual baselines just to make catalog maintenance pass.
|
||||
@@ -0,0 +1,235 @@
|
||||
# Initial Storybook review — 2026-10-12
|
||||
|
||||
Source baseline: `f0e12c851971a21f7aadf0835b016bfaf2067314`.
|
||||
Scope: complete source inventory and initial classification, with targeted source
|
||||
comparison and browser checks. This is not full state parity, full interaction
|
||||
coverage, or production acceptance for all stories.
|
||||
|
||||
## Inventory and organization
|
||||
|
||||
There is one Storybook, already centralized under `ui/storybook/`. At the initial
|
||||
baseline, its 223 story files were split between 176 files at the stories root and
|
||||
47 nested files, with 51 top-level sidebar groups. The initial pass recorded
|
||||
product meaning, provenance, and review status before changing that organization.
|
||||
The follow-up organization pass below moves the files and sidebar together.
|
||||
|
||||
The downstream catalog gives each file a primary feature home and related stable
|
||||
capability IDs. The generated index includes all 171 upstream capabilities across
|
||||
35 feature areas. The existing Overview story now provides searchable feature
|
||||
navigation into the actual built story index. The initial pass kept sidebar titles
|
||||
stable; the follow-up uses explicit IDs to preserve links while changing titles.
|
||||
|
||||
| Dimension | Initial classification |
|
||||
| --- | --- |
|
||||
| Product alignment | 18 current subjects, 4 proposals, 5 historical suites, 8 mixed suites, 188 requiring deeper parity review |
|
||||
| Rendered subject | 199 production, 8 prototype, 11 mixed, 5 guides |
|
||||
| Data boundary | 217 fixture-based, 1 separate fixture server, 5 static guides |
|
||||
|
||||
A production subject can have unreviewed state parity. A current subject can still
|
||||
have untested loading, error, permission, or backend behavior. The catalog's notes
|
||||
and source references qualify these labels; none are a substitute for live proof.
|
||||
|
||||
## Corrections and retained history
|
||||
|
||||
- **Question dismissal and send:** the local interaction-above-composer harness
|
||||
kept a pending badge for dismissed questions and left the question open after
|
||||
a normal send. Production `TaskChatThread` excludes questions from
|
||||
`pendingReminderInputs` and dismisses the selected question on send. The harness
|
||||
now matches those rules. Added question-dismiss and confirmation-reopen cases;
|
||||
corrected the send case's stale editor selector. The story still does not prove
|
||||
persisted dismissal or history behavior.
|
||||
- **Fast response helpers:** `empty`, `configured`, and `result` were exported
|
||||
fixtures which Storybook also treated as stories. Their IDs collided with
|
||||
`Empty`, `Configured`, and `Result`. Explicit exclusion removes the false exports
|
||||
while retaining the real stories and imported fixtures. Browser review also
|
||||
exposed a missing explicit `onConnectionChange` spy after the production view
|
||||
began calling it on mount; added that fixture callback.
|
||||
- **Chat provenance:** the main chat wrapper combines production conversation/task
|
||||
pages with a local landing view. The separate first-created-agent chat-entry
|
||||
design is historical; current primary-agent examples are its replacement.
|
||||
- **Historical designs retained:** GitHub chat, old agent onboarding, connect-model
|
||||
tiles, paused composer, and chat-entry remain available with their purpose and
|
||||
replacement references. Local proposals are labeled rather than deleted.
|
||||
- **Prototype folder is not a verdict:** some files re-export production subjects;
|
||||
others implement local compositions. Static import traversal found no obvious
|
||||
orphan prototype TS/TSX module. This is not proof that each is still useful.
|
||||
- **Contributor guidance:** replaced the old Overview's manual coverage claims,
|
||||
corrected the interaction-authoring skill's obsolete story path, and updated the
|
||||
prototype README's catalog source count to match the shipped five sources.
|
||||
|
||||
## Verification
|
||||
|
||||
Environment: local macOS, headless Chromium, built Storybook 10.6.0, fixture data.
|
||||
The following nine `play` cases reached Storybook's `storyFinished: success` event
|
||||
without a browser page exception. These results establish story interactions only.
|
||||
|
||||
| Story ID | Subject / assertion |
|
||||
| --- | --- |
|
||||
| `composer-interaction-above-composer--ask-user-questions-and-send` | Production card/composer in local harness; normal send dismisses question without a pending badge |
|
||||
| `composer-interaction-above-composer--dismiss-question-without-pending-badge` | Production card/composer; dismiss leaves editable composer without pending badge |
|
||||
| `composer-interaction-above-composer--dismiss-confirmation-keeps-pending-badge` | Production card/composer; dismiss retains confirmation badge and reopening works |
|
||||
| `composer-new-task--empty` | Production composer wrapper; existing play assertions |
|
||||
| `design-explorations-agent-chat-sidebar-components--add-and-reopen-chat` | Production sidebar; fixture add/reopen assertions |
|
||||
| `assistant-connections-consent--choose-organization` | Production consent surface; fixture organization selection assertions |
|
||||
| `historical-github-chat-prototypes--standard-trust-warning` | Historical prototype; existing trust-warning assertions, not current GitHub acceptance |
|
||||
| `design-previews-recovery-notice--retry-request-failed` | Production recovery notice; fixture failure-state assertions |
|
||||
| `composer-mobile-assignee-picker--picker-open` | Production picker; existing open-picker assertions |
|
||||
|
||||
Additional manual browser checks passed: search the guide for `steering/queue`,
|
||||
follow its link to the real queued-message story, dismiss a question, then reload
|
||||
and verify that the fixture resets to an open question. Screenshots were inspected
|
||||
locally and are not committed as new regression baselines.
|
||||
|
||||
Built-index comparison: all 2,208 prior story IDs remain; only the two new dismissal
|
||||
regressions were added (2,210 total). Catalog validation, eight checker tests,
|
||||
feature-map validation, both skill validators, token gates, workspace typecheck,
|
||||
and workspace build passed. Fast-response settings `empty`, `configured`, and
|
||||
`result` subsequently reached `storyFinished: success` with no page exceptions
|
||||
following the explicit callback fix. These three cases are render checks, not save
|
||||
or backend-persistence tests. The UI Vitest project passed all 8,176 tests in 728
|
||||
files. The full `pnpm test:run` reported a 15-second timeout in the unchanged backend
|
||||
case `connects advertised Workspace gmail.read without mutating definitions in
|
||||
authenticated`. That exact case passed standalone without changing expectations
|
||||
or timeouts. The remaining broad run was stopped after this failure; a complete
|
||||
repository suite pass is not established.
|
||||
|
||||
Automated review found missing directly rendered subjects in the initial dependency
|
||||
list. Expanded the catalog across affected suites, including relative production
|
||||
imports, all direct non-primitive subjects in local wrappers, and the primitives
|
||||
that foundation stories demonstrate. Explanatory review guides keep their separate
|
||||
guide classification. This records change detection, not new parity
|
||||
acceptance. Added a real CSF parser regression for nested discovery, include/exclude
|
||||
rules, stable explicit IDs, and play counts.
|
||||
|
||||
## Feature organization follow-up — 2026-10-12
|
||||
|
||||
Source baseline: `75bd3c977cd828d5222823850339e8204f40eb47` (parent PR #16046).
|
||||
The 223 story files now live under their primary feature area in
|
||||
`ui/storybook/stories/`. The sidebar uses the feature map's section and feature
|
||||
labels in map order. Existing scenario groups remain below those labels.
|
||||
Overview and shared Foundations have dedicated homes. Areas without stories do
|
||||
not get empty sidebar groups; their gaps remain visible in the catalog.
|
||||
|
||||
Nineteen supporting files moved to `ui/storybook/support/`, including fixtures,
|
||||
shared wrappers, guide READMEs, an HTML fixture, and a fixture test. Relative imports,
|
||||
active documentation, catalog sources, and replacement references follow the
|
||||
moves. Classification and review scope did not change. The upstream feature map
|
||||
and its skill are unchanged.
|
||||
|
||||
Every suite now has an explicit meta ID, recorded as `stableId` in the catalog.
|
||||
The built index comparison preserved all 2,210 story IDs, the single docs ID, and
|
||||
all entry names. No story was removed or added. Catalog validation checks primary
|
||||
feature folders, sidebar prefixes, and stable meta IDs; its nine tests passed.
|
||||
|
||||
The Storybook build, UI typecheck, all 8,176 UI tests in 728 files, token gates,
|
||||
map checks, and the maintenance-skill validator passed. A source comparison found
|
||||
only changed meta titles/IDs, rebased relative references, and updated fixture
|
||||
path text in story files. The full repository suite was not repeated for these
|
||||
file moves; the prior full-suite limitation above still applies.
|
||||
|
||||
Local Chromium verified the sidebar's section/feature order, rendered the guide,
|
||||
searched `steering/queue`, and followed its link to the unchanged
|
||||
`composer-steering-and-queued-messages--queued-while-running` ID. The nine play
|
||||
cases listed above and `fast-responses-01-settings--empty` all reached
|
||||
`storyFinished: success` without page exceptions after relocation. This is a
|
||||
sample of interactions and rendering, not execution of all 2,210 stories.
|
||||
The fixture-server story and visual-baseline comparisons remain untested for the
|
||||
reasons below. The owned static preview was left running for the requested test-drive.
|
||||
|
||||
## Remaining review work
|
||||
|
||||
1. Review the 188 unreviewed suites by capability and production subject. Compare
|
||||
state transitions, payloads, errors, gates, and reset behavior before promoting
|
||||
a suite. Start with large route wrappers and shared fixture dependencies.
|
||||
2. Shared preview mocks can fall through to the original fetch; some route harnesses
|
||||
return empty arrays for unmatched APIs. These can hide missing fixtures. Audit
|
||||
affected endpoints per feature rather than treating a render as API coverage.
|
||||
3. The managed-accounts test-drive story needs the separately owned local API/DB
|
||||
fixture at port 4310. Its upstream providers are simulated. It was not exercised
|
||||
against an unknown existing service.
|
||||
4. Screenshot comparison is blocked: the existing baseline manifest has zero
|
||||
snapshots and no published archive. Keep using the established Linux baseline
|
||||
workflow when its baseline is published. Do not bless new baselines as part of
|
||||
catalog cleanup.
|
||||
5. Review fingerprints track declared sources, including feature recipes and local
|
||||
support; they do not traverse every production dependency. Add missing subjects
|
||||
when a review discovers them. Shared setup changes require reviewing callers.
|
||||
|
||||
Use `maintain-storybook` to correct drift and `review-storybook` to record separate
|
||||
source, render, interaction, visual-comparison, and production evidence. Both skills
|
||||
consume the feature map; neither changes its upstream contract.
|
||||
|
||||
## Native runtime prerequisite: scoped source follow-up — 2026-10-12
|
||||
|
||||
Source comparison: `9f59ff36cf2da963709ca19c06ab005510c835a6` against actual
|
||||
master `4b958ecbd8f33a7782fe6f29ec825f80f636b833`. The reviewed
|
||||
`maps/feature-map/runs-adapters.md` bytes have SHA-256
|
||||
`ca5aff86608b7d7987dc0ee814475a7b93e3faff1f99ce090429d2e932b7e16b`.
|
||||
|
||||
Twenty-one entries in Runs, harnesses, and model accounts and Task creation and
|
||||
lifecycle had review-fingerprint drift. Their 117 unique declared sources have
|
||||
one changed input: the runs-adapters feature recipe. All 21 story files and the
|
||||
other 116 declared production subjects, wrappers and fixture sources are
|
||||
unchanged from master. The recipe adds bounded native startup and registration
|
||||
retirement controls. It separates those provider-free controls from real daemon
|
||||
and installed-provider proof; it changes no represented UI or fixture contract.
|
||||
|
||||
Refreshed only those 21 reviewed fingerprints. No story, fixture, export, stable
|
||||
ID, title, capability reference or product/implementation/data classification
|
||||
changed. Existing mixed and unreviewed parity limitations remain. No fixture
|
||||
correction is needed for this scoped prerequisite; this does not establish full
|
||||
state parity for the affected suites.
|
||||
|
||||
This follow-up is source inspection only. Rendering, play assertions, visual
|
||||
comparisons and production provider acceptance were not run. Native lifecycle
|
||||
behavior remains with the linked transport controls and their existing hosted
|
||||
CI fixtures. Catalog validation proves structure and reviewed dependency hashes,
|
||||
not that these live journeys passed.
|
||||
|
||||
## Claude/Codex runner composition review — 2026-10-12
|
||||
|
||||
Source context: canonical runtime prerequisite
|
||||
`64c074fb8da1b31f8d6042c4fef48dc4164e759b` composed with the Codex dependency
|
||||
and local Claude behavior at `304e95c320`; the catalog/story changes in this
|
||||
checkpoint are recorded below. This pass covers changed source dependencies and
|
||||
six selected fixture interactions. It is not a full state-parity or Linux visual
|
||||
baseline comparison, and it does not qualify providers or default rollout.
|
||||
|
||||
The shared preview now supplies the production general-settings response and
|
||||
resolved runner-discovery fields required by the matching-discovery guard. Its
|
||||
Claude/Codex defaults explicitly simulate Legacy; the independent Codex creation
|
||||
fixture simulates a qualified native default. The native inventory is not added
|
||||
twice. Opus 5.5 joins the model metadata without replacing explicit older models.
|
||||
All fixtures remain isolated from live providers and production accounts.
|
||||
|
||||
Fifty-three affected entries retain their previous current/mixed/historical/
|
||||
unreviewed classifications. Source changes include shared runner controls,
|
||||
branding, model/effort/default discovery, and the associated recipes. Existing
|
||||
Select subjects remain intact while the shared module adds the runner popover;
|
||||
no new primitive coverage is inferred. Direct runner-control dependencies are
|
||||
listed where the rendered form/wizard uses them. The new Codex creation file is
|
||||
placed in the agents feature home with explicit stable ID
|
||||
`agents-codex-runner-creation`; all eight existing IDs survive. All 2,223 current
|
||||
story IDs match the pre-move inventory, and 224 files are accounted for.
|
||||
|
||||
An owned Storybook 10.6.0 dev server on port 6195 and the in-app browser executed
|
||||
these existing play functions. Each displayed `Story status: Pass`, with its
|
||||
observable result checked and no captured browser errors:
|
||||
|
||||
| Story ID | Observed result |
|
||||
| --- | --- |
|
||||
| `agents-codex-runner-creation--automatic-codex` | Runner shows Paperclip Runner as default with the old experimental gate disabled |
|
||||
| `agents-codex-runner-creation--explicit-legacy` | Runner remains Legacy |
|
||||
| `agents-codex-runner-creation--setup-failure` | Missing-runner failure is visible and the override stays available |
|
||||
| `agents-codex-runner-creation--task-ready` | Native fixture setup reaches the task-ready confirmation |
|
||||
| `onboarding-agent-arc--claude-api-key` | Claude API-key entry is visible; no live key is entered |
|
||||
| `onboarding-agent-arc--connect-with-saved-claude-subscription` | Simulated saved connection advances to Review |
|
||||
|
||||
Revisiting Automatic Codex after Task Ready restores the Connect step, its native
|
||||
default and a disabled Confirmation step; no hired-agent state leaks. This is
|
||||
fixture reset evidence, not persisted backend/session evidence. Other plays,
|
||||
full themes/mobile variants in this Storybook, accessibility grading and Linux
|
||||
baseline comparisons are not run. The retained product preview's separate mobile
|
||||
inspection remains bound to its earlier compiled source, and final installed
|
||||
product/provider checks remain pending. The catalog checker and nine checker
|
||||
controls pass; UI noemit, maps and token gates pass on the composed source.
|
||||
File diff suppressed because it is too large.
Load diff
@@ -5,7 +5,6 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("node:child_process", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("node:child_process")>();
|
||||
// Execute the authored remote command locally, through both subprocess paths.
|
||||
return {
|
||||
...actual,
|
||||
execFile: vi.fn((file: string, args: string[], options: object, callback: Parameters<typeof actual.execFile>[3]) => {
|
||||
|
||||
@@ -68,8 +68,6 @@ export function createSshCommandManagedRuntimeRunner(input: {
|
||||
const envPrefix = envEntries.length > 0
|
||||
? `env ${envEntries.map(([key, value]) => shellQuote(`${key}=${value}`)).join(" ")} `
|
||||
: "";
|
||||
// Keep sh -c's command name and positional arguments intact. Flattening
|
||||
// its script loses $0/$1/... and breaks native runner launch/identity.
|
||||
const commandScript = `exec ${envPrefix}${[shellQuote(command), ...args.map((arg) => shellQuote(arg))].join(" ")}`;
|
||||
const remoteCommand = `cd ${shellQuote(cwd)} && ${commandScript}`;
|
||||
|
||||
|
||||
@@ -725,7 +725,6 @@ export interface CLIAdapterModule {
|
||||
|
||||
export interface CreateConfigValues {
|
||||
runner?: "auto" | "paperclip" | "legacy";
|
||||
/** Transient form resolution; never an execution setting or API override. */
|
||||
resolvedRunner?: "paperclip" | "legacy";
|
||||
adapterType: string;
|
||||
codexPermissionMode?: "never" | "on-request" | "untrusted";
|
||||
|
||||
@@ -45,8 +45,6 @@ export function buildClaudeLocalConfig(v: CreateConfigValues): Record<string, un
|
||||
if (Object.keys(env).length > 0) ac.env = env;
|
||||
const nativeSelected = v.runner === "paperclip"
|
||||
|| ((v.runner === undefined || v.runner === "auto") && v.resolvedRunner === "paperclip");
|
||||
// Only a resolved native form omits the Legacy default. Explicit limits
|
||||
// still reach the server's compatibility check.
|
||||
if (!nativeSelected || v.maxTurnsPerRun !== 1000) ac.maxTurnsPerRun = v.maxTurnsPerRun;
|
||||
if (nativeSelected && v.adapterSchemaValues?.acpxPermissionMode !== undefined) {
|
||||
ac.acpxPermissionMode = v.adapterSchemaValues.acpxPermissionMode;
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
CREATE TABLE IF NOT EXISTS "chat_slack_manager_grants" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"company_id" uuid NOT NULL,
|
||||
"user_id" text NOT NULL,
|
||||
"slack_user_id" text NOT NULL,
|
||||
"workspace_id" text NOT NULL,
|
||||
"workspace_name" text NOT NULL,
|
||||
"manager_app_id" text NOT NULL,
|
||||
"status" text DEFAULT 'active' NOT NULL,
|
||||
"access_secret_id" uuid,
|
||||
"refresh_secret_id" uuid,
|
||||
"expires_at" timestamp with time zone,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
CONSTRAINT "chat_slack_manager_grants_status_check" CHECK ("chat_slack_manager_grants"."status" in ('active', 'revoked', 'reauthorize'))
|
||||
);
|
||||
|
||||
--> statement-breakpoint
|
||||
|
||||
ALTER TABLE "chat_slack_registrations" ADD COLUMN IF NOT EXISTS "manager_grant_id" uuid;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "chat_slack_manager_grants" ADD CONSTRAINT "chat_slack_manager_grants_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||
--> statement-breakpoint
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "chat_slack_manager_grants_company_id_idx" ON "chat_slack_manager_grants" USING btree ("company_id","id");
|
||||
--> statement-breakpoint
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "chat_slack_manager_grants_owner_idx" ON "chat_slack_manager_grants" USING btree ("company_id","user_id","manager_app_id","workspace_id");
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "chat_slack_registrations" ADD CONSTRAINT "chat_slack_registrations_company_id_manager_grant_id_chat_slack_manager_grants_company_id_id_fk" FOREIGN KEY ("company_id","manager_grant_id") REFERENCES "public"."chat_slack_manager_grants"("company_id","id") ON DELETE no action ON UPDATE no action;
|
||||
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "chat_slack_manager_grants" ADD COLUMN IF NOT EXISTS "revision" integer DEFAULT 0 NOT NULL;
|
||||
--> statement-breakpoint
|
||||
|
||||
ALTER TABLE "chat_slack_manager_grants" ADD COLUMN IF NOT EXISTS "rate_limits" jsonb DEFAULT '{}'::jsonb NOT NULL;
|
||||
+817
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"id": "0a93bc99-bf0c-4ea2-ab27-6043b6746cd5",
|
||||
"prevId": "76861d12-d9a9-4f5f-bcbc-0fb06c0addbc",
|
||||
"id": "18c156f2-8344-4282-98b7-f702f0035c81",
|
||||
"prevId": "39d82f2c-8fd5-4606-84df-f51880dbd6a7",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
@@ -416,6 +416,12 @@
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"fast_response_request_id": {
|
||||
"name": "fast_response_request_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"agent_id": {
|
||||
"name": "agent_id",
|
||||
"type": "uuid",
|
||||
@@ -462,6 +468,27 @@
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"budget_reservations_fast_response_idx": {
|
||||
"name": "budget_reservations_fast_response_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "company_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "fast_response_request_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"budget_reservations_decision_idx": {
|
||||
"name": "budget_reservations_decision_idx",
|
||||
"columns": [
|
||||
@@ -580,7 +607,7 @@
|
||||
"checkConstraints": {
|
||||
"budget_reservations_source_check": {
|
||||
"name": "budget_reservations_source_check",
|
||||
"value": "(\"budget_reservations\".\"run_id\" is not null)::int + (\"budget_reservations\".\"decision_invocation_id\" is not null)::int = 1"
|
||||
"value": "(\"budget_reservations\".\"run_id\" is not null)::int + (\"budget_reservations\".\"decision_invocation_id\" is not null)::int + (\"budget_reservations\".\"fast_response_request_id\" is not null)::int = 1"
|
||||
},
|
||||
"budget_reservations_state_check": {
|
||||
"name": "budget_reservations_state_check",
|
||||
@@ -3737,6 +3764,28 @@
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"agent_wakeup_requests_question_response_history_idx": {
|
||||
"name": "agent_wakeup_requests_question_response_history_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "company_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "idempotency_key",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": "\"agent_wakeup_requests\".\"idempotency_key\" LIKE 'question-response:%'",
|
||||
"concurrently": true,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"agent_wakeup_requests_connection_intent_delivery_idempotency_uq": {
|
||||
"name": "agent_wakeup_requests_connection_intent_delivery_idempotency_uq",
|
||||
"columns": [
|
||||
@@ -12384,6 +12433,189 @@
|
||||
},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.chat_slack_manager_grants": {
|
||||
"name": "chat_slack_manager_grants",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"company_id": {
|
||||
"name": "company_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"slack_user_id": {
|
||||
"name": "slack_user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"workspace_name": {
|
||||
"name": "workspace_name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"manager_app_id": {
|
||||
"name": "manager_app_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'active'"
|
||||
},
|
||||
"access_secret_id": {
|
||||
"name": "access_secret_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"refresh_secret_id": {
|
||||
"name": "refresh_secret_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"revision": {
|
||||
"name": "revision",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 0
|
||||
},
|
||||
"rate_limits": {
|
||||
"name": "rate_limits",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'{}'::jsonb"
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"chat_slack_manager_grants_company_id_idx": {
|
||||
"name": "chat_slack_manager_grants_company_id_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "company_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"chat_slack_manager_grants_owner_idx": {
|
||||
"name": "chat_slack_manager_grants_owner_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "company_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "user_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "manager_app_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "workspace_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"chat_slack_manager_grants_company_id_companies_id_fk": {
|
||||
"name": "chat_slack_manager_grants_company_id_companies_id_fk",
|
||||
"tableFrom": "chat_slack_manager_grants",
|
||||
"tableTo": "companies",
|
||||
"columnsFrom": [
|
||||
"company_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {
|
||||
"chat_slack_manager_grants_status_check": {
|
||||
"name": "chat_slack_manager_grants_status_check",
|
||||
"value": "\"chat_slack_manager_grants\".\"status\" in ('active', 'revoked', 'reauthorize')"
|
||||
}
|
||||
},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.chat_slack_registrations": {
|
||||
"name": "chat_slack_registrations",
|
||||
"schema": "",
|
||||
@@ -12400,6 +12632,12 @@
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"manager_grant_id": {
|
||||
"name": "manager_grant_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"request_id": {
|
||||
"name": "request_id",
|
||||
"type": "uuid",
|
||||
@@ -12490,6 +12728,21 @@
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"chat_slack_registrations_company_id_manager_grant_id_chat_slack_manager_grants_company_id_id_fk": {
|
||||
"name": "chat_slack_registrations_company_id_manager_grant_id_chat_slack_manager_grants_company_id_id_fk",
|
||||
"tableFrom": "chat_slack_registrations",
|
||||
"tableTo": "chat_slack_manager_grants",
|
||||
"columnsFrom": [
|
||||
"company_id",
|
||||
"manager_grant_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"company_id",
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"chat_slack_registrations_company_id_endpoint_id_chat_endpoints_company_id_id_fk": {
|
||||
"name": "chat_slack_registrations_company_id_endpoint_id_chat_endpoints_company_id_id_fk",
|
||||
"tableFrom": "chat_slack_registrations",
|
||||
@@ -16725,6 +16978,19 @@
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"package_mode": {
|
||||
"name": "package_mode",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'skills'"
|
||||
},
|
||||
"repository_package": {
|
||||
"name": "repository_package",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"excluded_folders": {
|
||||
"name": "excluded_folders",
|
||||
"type": "jsonb",
|
||||
@@ -17052,6 +17318,87 @@
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.company_skill_repository_snapshots": {
|
||||
"name": "company_skill_repository_snapshots",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"company_id": {
|
||||
"name": "company_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"content_hash": {
|
||||
"name": "content_hash",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"file_inventory": {
|
||||
"name": "file_inventory",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"company_skill_repository_snapshots_content_idx": {
|
||||
"name": "company_skill_repository_snapshots_content_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "company_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "content_hash",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"company_skill_repository_snapshots_company_id_companies_id_fk": {
|
||||
"name": "company_skill_repository_snapshots_company_id_companies_id_fk",
|
||||
"tableFrom": "company_skill_repository_snapshots",
|
||||
"tableTo": "companies",
|
||||
"columnsFrom": [
|
||||
"company_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.company_skill_stars": {
|
||||
"name": "company_skill_stars",
|
||||
"schema": "",
|
||||
@@ -17906,6 +18253,18 @@
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"repository_snapshot_id": {
|
||||
"name": "repository_snapshot_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"repository_skill_path": {
|
||||
"name": "repository_skill_path",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"revision_number": {
|
||||
"name": "revision_number",
|
||||
"type": "integer",
|
||||
@@ -18062,6 +18421,19 @@
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"company_skill_versions_repository_snapshot_id_company_skill_repository_snapshots_id_fk": {
|
||||
"name": "company_skill_versions_repository_snapshot_id_company_skill_repository_snapshots_id_fk",
|
||||
"tableFrom": "company_skill_versions",
|
||||
"tableTo": "company_skill_repository_snapshots",
|
||||
"columnsFrom": [
|
||||
"repository_snapshot_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "restrict",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"company_skill_versions_author_agent_id_agents_id_fk": {
|
||||
"name": "company_skill_versions_author_agent_id_agents_id_fk",
|
||||
"tableFrom": "company_skill_versions",
|
||||
@@ -19576,6 +19948,34 @@
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"cost_events_data_quality_idx": {
|
||||
"name": "cost_events_data_quality_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "company_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "occurred_at",
|
||||
"isExpression": false,
|
||||
"asc": false,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": "\"cost_events\".\"billing_type\" = 'unknown' or (\"cost_events\".\"cost_status\" = 'unpriced' and \"cost_events\".\"billing_type\" <> 'subscription_included')",
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"cost_events_company_occurred_idx": {
|
||||
"name": "cost_events_company_occurred_idx",
|
||||
"columns": [
|
||||
@@ -19826,7 +20226,7 @@
|
||||
"checkConstraints": {
|
||||
"cost_events_usage_kind_check": {
|
||||
"name": "cost_events_usage_kind_check",
|
||||
"value": "\"cost_events\".\"usage_kind\" = 'decision' or (\"cost_events\".\"usage_kind\" = 'agent' and \"cost_events\".\"agent_id\" is not null)"
|
||||
"value": "\"cost_events\".\"usage_kind\" in ('decision', 'fast_response') or (\"cost_events\".\"usage_kind\" = 'agent' and \"cost_events\".\"agent_id\" is not null)"
|
||||
},
|
||||
"cost_events_nonnegative_amounts": {
|
||||
"name": "cost_events_nonnegative_amounts",
|
||||
@@ -27491,6 +27891,378 @@
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.company_fast_responses": {
|
||||
"name": "company_fast_responses",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"company_id": {
|
||||
"name": "company_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"connection_id": {
|
||||
"name": "connection_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"grant_id": {
|
||||
"name": "grant_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"model": {
|
||||
"name": "model",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"provider": {
|
||||
"name": "provider",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"enabled": {
|
||||
"name": "enabled",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": false
|
||||
},
|
||||
"allow_sponsored": {
|
||||
"name": "allow_sponsored",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": true
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"company_fast_responses_company_id_companies_id_fk": {
|
||||
"name": "company_fast_responses_company_id_companies_id_fk",
|
||||
"tableFrom": "company_fast_responses",
|
||||
"tableTo": "companies",
|
||||
"columnsFrom": [
|
||||
"company_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {
|
||||
"company_fast_responses_configured": {
|
||||
"name": "company_fast_responses_configured",
|
||||
"value": "(\"company_fast_responses\".\"connection_id\" is null) = (\"company_fast_responses\".\"grant_id\" is null) and (not \"company_fast_responses\".\"enabled\" or (\"company_fast_responses\".\"connection_id\" is not null and \"company_fast_responses\".\"model\" is not null))"
|
||||
}
|
||||
},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.fast_response_requests": {
|
||||
"name": "fast_response_requests",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"company_id": {
|
||||
"name": "company_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"source_key": {
|
||||
"name": "source_key",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"source_comment_id": {
|
||||
"name": "source_comment_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"issue_id": {
|
||||
"name": "issue_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"project_id": {
|
||||
"name": "project_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"agent_id": {
|
||||
"name": "agent_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"responsible_user_id": {
|
||||
"name": "responsible_user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"sponsored": {
|
||||
"name": "sponsored",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": false
|
||||
},
|
||||
"endpoint_id": {
|
||||
"name": "endpoint_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"conversation_id": {
|
||||
"name": "conversation_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"delivery_id": {
|
||||
"name": "delivery_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"session_generation": {
|
||||
"name": "session_generation",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'pending'"
|
||||
},
|
||||
"publication_status": {
|
||||
"name": "publication_status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'pending'"
|
||||
},
|
||||
"connection_id": {
|
||||
"name": "connection_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"grant_id": {
|
||||
"name": "grant_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"provider": {
|
||||
"name": "provider",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"model": {
|
||||
"name": "model",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"provider_request_id": {
|
||||
"name": "provider_request_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"cost_event_id": {
|
||||
"name": "cost_event_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"error_code": {
|
||||
"name": "error_code",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"comment_id": {
|
||||
"name": "comment_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"accepted_at": {
|
||||
"name": "accepted_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"started_at": {
|
||||
"name": "started_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"finished_at": {
|
||||
"name": "finished_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"duration_ms": {
|
||||
"name": "duration_ms",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"input_tokens": {
|
||||
"name": "input_tokens",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"output_tokens": {
|
||||
"name": "output_tokens",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"fast_response_requests_source_idx": {
|
||||
"name": "fast_response_requests_source_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "company_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "source_key",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"fast_response_requests_history_idx": {
|
||||
"name": "fast_response_requests_history_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "company_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "accepted_at",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"fast_response_requests_work_idx": {
|
||||
"name": "fast_response_requests_work_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "expires_at",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": "\"fast_response_requests\".\"status\" in ('pending', 'running')",
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"fast_response_requests_company_id_companies_id_fk": {
|
||||
"name": "fast_response_requests_company_id_companies_id_fk",
|
||||
"tableFrom": "fast_response_requests",
|
||||
"tableTo": "companies",
|
||||
"columnsFrom": [
|
||||
"company_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"fast_response_requests_cost_event_id_cost_events_id_fk": {
|
||||
"name": "fast_response_requests_cost_event_id_cost_events_id_fk",
|
||||
"tableFrom": "fast_response_requests",
|
||||
"tableTo": "cost_events",
|
||||
"columnsFrom": [
|
||||
"cost_event_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {
|
||||
"fast_response_requests_status_check": {
|
||||
"name": "fast_response_requests_status_check",
|
||||
"value": "\"fast_response_requests\".\"status\" in ('pending', 'running', 'succeeded', 'failed', 'unknown', 'skipped')"
|
||||
}
|
||||
},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.feedback_exports": {
|
||||
"name": "feedback_exports",
|
||||
"schema": "",
|
||||
@@ -29938,6 +30710,34 @@
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"heartbeat_runs_cost_data_quality_idx": {
|
||||
"name": "heartbeat_runs_cost_data_quality_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "company_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "coalesce(\"finished_at\", \"created_at\") desc",
|
||||
"asc": true,
|
||||
"isExpression": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": "\"heartbeat_runs\".\"cost_accounting_pending\" = true and \"heartbeat_runs\".\"status\" in ('succeeded','failed','timed_out','cancelled','interrupted')",
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"heartbeat_runs_execution_status_delivery_idx": {
|
||||
"name": "heartbeat_runs_execution_status_delivery_idx",
|
||||
"columns": [
|
||||
@@ -32392,6 +33192,19 @@
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"origin": {
|
||||
"name": "origin",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'comment'"
|
||||
},
|
||||
"fast_response_request_id": {
|
||||
"name": "fast_response_request_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"body": {
|
||||
"name": "body",
|
||||
"type": "text",
|
||||
+7
@@ -2332,6 +2332,13 @@
|
||||
"when": 1791807836288,
|
||||
"tag": "0334_rare_slipstream",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 335,
|
||||
"version": "7",
|
||||
"when": 1791832982875,
|
||||
"tag": "0335_absurd_bulldozer",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { sql } from "drizzle-orm";
|
||||
import { check, integer, jsonb, pgTable, text, timestamp, uniqueIndex, uuid } from "drizzle-orm/pg-core";
|
||||
import { companies } from "./companies.js";
|
||||
|
||||
/** Reusable personal management authorization; never a bot runtime credential. */
|
||||
export const chatSlackManagerGrants = pgTable("chat_slack_manager_grants", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
|
||||
userId: text("user_id").notNull(),
|
||||
slackUserId: text("slack_user_id").notNull(),
|
||||
workspaceId: text("workspace_id").notNull(),
|
||||
workspaceName: text("workspace_name").notNull(),
|
||||
managerAppId: text("manager_app_id").notNull(),
|
||||
status: text("status").$type<"active" | "revoked" | "reauthorize">().notNull().default("active"),
|
||||
accessSecretId: uuid("access_secret_id"),
|
||||
refreshSecretId: uuid("refresh_secret_id"),
|
||||
revision: integer("revision").notNull().default(0),
|
||||
rateLimits: jsonb("rate_limits").$type<Record<string, string>>().notNull().default({}),
|
||||
expiresAt: timestamp("expires_at", { withTimezone: true }),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
}, t => [
|
||||
uniqueIndex("chat_slack_manager_grants_company_id_idx").on(t.companyId, t.id),
|
||||
uniqueIndex("chat_slack_manager_grants_owner_idx").on(t.companyId, t.userId, t.managerAppId, t.workspaceId),
|
||||
check("chat_slack_manager_grants_status_check", sql`${t.status} in ('active', 'revoked', 'reauthorize')`),
|
||||
]);
|
||||
@@ -1,12 +1,14 @@
|
||||
import { sql } from "drizzle-orm";
|
||||
import { check, foreignKey, jsonb, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
|
||||
import type { SlackRegistrationStatus } from "@paperclipai/shared";
|
||||
import { chatSlackManagerGrants } from "./chat_slack_manager_grants.js";
|
||||
import { chatEndpoints } from "./chat_channels.js";
|
||||
|
||||
/** App provisioning is distinct from installation and runtime credentials. No plaintext secrets. */
|
||||
export const chatSlackRegistrations = pgTable("chat_slack_registrations", {
|
||||
endpointId: uuid("endpoint_id").primaryKey(),
|
||||
companyId: uuid("company_id").notNull(),
|
||||
managerGrantId: uuid("manager_grant_id"),
|
||||
requestId: uuid("request_id").notNull(),
|
||||
status: text("status").$type<SlackRegistrationStatus>().notNull(),
|
||||
manifest: jsonb("manifest").$type<Record<string, unknown>>().notNull(),
|
||||
@@ -22,6 +24,7 @@ export const chatSlackRegistrations = pgTable("chat_slack_registrations", {
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
}, t => [
|
||||
foreignKey({ columns: [t.companyId, t.managerGrantId], foreignColumns: [chatSlackManagerGrants.companyId, chatSlackManagerGrants.id] }),
|
||||
foreignKey({ columns: [t.companyId, t.endpointId], foreignColumns: [chatEndpoints.companyId, chatEndpoints.id] }).onDelete("cascade"),
|
||||
check("chat_slack_registration_status_check", sql`${t.status} in ('creating', 'uncertain', 'failed', 'install', 'credentials_saved', 'configured', 'removed')`),
|
||||
]);
|
||||
@@ -243,6 +243,8 @@ export { chatVoiceReports } from "./chat_voice_reports.js";
|
||||
export { chatVoicePhoneLines, chatVoiceInboundCalls } from "./chat_voice_inbound.js";
|
||||
export * from "./decision_models.js";
|
||||
export { userCompanyPreferences } from "./user_company_preferences.js";
|
||||
|
||||
export { chatSlackManagerGrants } from "./chat_slack_manager_grants.js";
|
||||
export { aiSubscriptions, aiSubscriptionPrices, aiSubscriptionConnections } from "./ai_subscriptions.js";
|
||||
|
||||
export * from "./fast_responses.js";
|
||||
@@ -14,12 +14,12 @@
|
||||
"requiresProviderPolicy": false
|
||||
},
|
||||
"pi": {
|
||||
"agentProfileVersion": 24,
|
||||
"agentProfileVersion": 25,
|
||||
"agentServerPackage": "pi-acp",
|
||||
"agentServerVersion": "0.0.33",
|
||||
"agentRuntimePackage": "@earendil-works/pi-coding-agent",
|
||||
"agentRuntimeVersion": "1.0.0",
|
||||
"commandDigest": "sha256:d25d674cda9a0853bdbf0521025099f1424c573d2f533630cb67d90e1bfcda3b",
|
||||
"commandDigest": "sha256:dd67387487dadb08da76534c057dc0aa059f7698d1779cbd5fc452813828167a",
|
||||
"requiresProviderPolicy": true
|
||||
},
|
||||
"cursor": {
|
||||
|
||||
@@ -3200,7 +3200,7 @@ mod tests {
|
||||
#[test]
|
||||
fn pi_profile_matches_published_identity_and_rejects_prior_profiles() {
|
||||
let published: Value = serde_json::from_str(include_str!(
|
||||
"../../../../test-fixtures/pi-acp/profile-v24-identity.json"
|
||||
"../../../../test-fixtures/pi-acp/profile-v25-identity.json"
|
||||
))
|
||||
.unwrap();
|
||||
let mut value = descriptor("codex");
|
||||
@@ -3217,6 +3217,7 @@ mod tests {
|
||||
current.validate(&context()).unwrap();
|
||||
|
||||
for prior in [
|
||||
include_str!("../../../../test-fixtures/pi-acp/profile-v24-identity.json"),
|
||||
include_str!("../../../../test-fixtures/pi-acp/profile-v23-identity.json"),
|
||||
include_str!("../../../../test-fixtures/pi-acp/profile-v15-identity.json"),
|
||||
include_str!("../../../../test-fixtures/pi-acp/profile-v16-identity.json"),
|
||||
|
||||
@@ -29,7 +29,7 @@ pub(crate) fn acpx_release_profile(agent: &str) -> Option<AcpxReleaseProfile> {
|
||||
agent_runtime_package: Some("@earendil-works/pi-coding-agent"),
|
||||
agent_runtime_version: Some("1.0.0"),
|
||||
command_digest:
|
||||
"sha256:d25d674cda9a0853bdbf0521025099f1424c573d2f533630cb67d90e1bfcda3b",
|
||||
"sha256:dd67387487dadb08da76534c057dc0aa059f7698d1779cbd5fc452813828167a",
|
||||
requires_provider_policy: true,
|
||||
},
|
||||
"cursor" => AcpxReleaseProfile {
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ use sha2::{Digest, Sha256};
|
||||
const CODEX_ACPX_DIGEST: &str =
|
||||
"sha256:c4538599d1ab767db5dff50934f13bb5ba313a59d9c4a83e993fac4617ea63d3";
|
||||
const PI_ACPX_DIGEST: &str =
|
||||
"sha256:d25d674cda9a0853bdbf0521025099f1424c573d2f533630cb67d90e1bfcda3b";
|
||||
"sha256:dd67387487dadb08da76534c057dc0aa059f7698d1779cbd5fc452813828167a";
|
||||
|
||||
fn temporary_directory(label: &str) -> PathBuf {
|
||||
let nonce = SystemTime::now()
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// Assemble the four independently built release daemons before npm packing.
|
||||
// Usage: node packages/paperclip-runner/scripts/stage-release-runner-binaries.mjs MANIFEST.json
|
||||
// Manifest: {sourceRevision, platforms: {"darwin-arm64": {path, sha256}, ...}, remoteProviderPack: {path, sha256}}.
|
||||
import { createHash } from "node:crypto";
|
||||
@@ -32,8 +31,6 @@ function regularFile(path, maximumSize) {
|
||||
return readFileSync(path);
|
||||
}
|
||||
|
||||
// Production TypeScript contracts are built by the existing producer step.
|
||||
// Foreign artifacts are data: the assembler never executes another target.
|
||||
async function metadataContract() {
|
||||
const { PAPERCLIP_RUNNER_BUILD_METADATA } = await import("../dist/evals/build-metadata.js");
|
||||
const { parsePaperclipRunnerdBuildMetadata } = await import("../dist/evals/runnerd-artifact.js");
|
||||
@@ -143,8 +140,6 @@ async function main() {
|
||||
for (const artifact of verified) {
|
||||
const relative = `${artifact.target}/paperclip-runnerd`, destination = join(root, "dist", "bin", relative);
|
||||
mkdirSync(dirname(destination), { recursive: true }); copyFileSync(artifact.source, destination); chmodSync(destination, 0o755);
|
||||
// Copied Mach-O inodes need a fresh ad-hoc signature on macOS. Record the
|
||||
// resulting packaged bytes, as well as the verified build artifact identity.
|
||||
if (process.platform === "darwin" && artifact.target.startsWith("darwin-")) {
|
||||
execFileSync("codesign", ["--force", "--sign", "-", destination], { stdio: "pipe", timeout: 30_000 });
|
||||
}
|
||||
@@ -155,7 +150,6 @@ async function main() {
|
||||
if (!pack.bytes) continue;
|
||||
const destination = join(root, "dist", "remote-provider-packs", target, "provider-pack.json");
|
||||
mkdirSync(dirname(destination), { recursive: true });
|
||||
// Replace the inode so repeat assembly can preserve a read-only manifest.
|
||||
const staged = `${destination}.staging-${process.pid}`;
|
||||
try {
|
||||
writeFileSync(staged, pack.bytes, { mode: 0o444, flag: "wx" });
|
||||
|
||||
@@ -32,8 +32,6 @@ export async function stageBuiltRunnerBinary(packageRoot) {
|
||||
const executable = process.platform === "win32" ? "paperclip-runnerd.exe" : "paperclip-runnerd";
|
||||
const source = path.join(packageRoot, "runner", "target", "release", executable);
|
||||
const binaryRoot = path.join(packageRoot, "dist", "bin");
|
||||
// Source builds are separate from the source-bound release targets and their
|
||||
// manifest. The dev supervisor selects this fresh output for its server.
|
||||
await stageRunnerBinary(source, path.join(binaryRoot, executable));
|
||||
}
|
||||
|
||||
|
||||
@@ -140,8 +140,6 @@ test("a source rebuild selects fresh dev bytes while preserving release binaries
|
||||
const copyHostFixture = async (input, output) => {
|
||||
await copyFile(input, output);
|
||||
if (process.platform === "darwin") {
|
||||
// System fixtures can be universal; Cargo release daemons are thin.
|
||||
// Extract only this host slice before exercising the native header gate.
|
||||
const slices = execFileSync("/usr/bin/lipo", ["-archs", input], { encoding: "utf8" }).trim().split(/\s+/);
|
||||
const architecture = slices.find(value => process.arch === "arm64" ? value.startsWith("arm64") : value === "x86_64");
|
||||
assert.ok(architecture);
|
||||
|
||||
@@ -8,8 +8,6 @@ export function resolveClaudeEffort(agent: string, value: unknown): ClaudeEffort
|
||||
return value;
|
||||
}
|
||||
export function createClaudeEffortAdmission(expected: ClaudeEffort) {
|
||||
// Every connection, including session/load, must select and acknowledge the
|
||||
// configured effort. A restored preference alone never admits a prompt.
|
||||
return createAcpxConfigAdmission(expected, { label: "Claude effort", configId: "effort",
|
||||
supportedValues: ["default", "low", "medium", "high", "xhigh", "max"] });
|
||||
}
|
||||
|
||||
@@ -1699,9 +1699,6 @@ async function silentPrimaryQuorumFixture(
|
||||
if (failure?.status === "rejected") throw failure.reason;
|
||||
},
|
||||
};
|
||||
// Only fixture preparation may retry. Release the unselected candidates
|
||||
// immediately before the caller stages once. A foreign bind racing this
|
||||
// handoff remains a visible production-call failure, never a hidden retry.
|
||||
const released = await Promise.allSettled(
|
||||
owned
|
||||
.filter((_, index) => !occupiedIndices.includes(index))
|
||||
|
||||
@@ -2,8 +2,6 @@ function object(value: unknown): Record<string, unknown> {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
||||
}
|
||||
|
||||
/** Native mode is independent of permissions. Every connection must prove its
|
||||
* current mode before a prompt; a persisted ACPX preference is not proof. */
|
||||
export function createAcpxConfigAdmission(expected: string, options: { label: string; configId: string; supportedValues: readonly string[]; restoring?: boolean; sessionMode?: boolean }) {
|
||||
type State = { sessionId: string | null; mode: string | null; error: Error | null };
|
||||
let current: State | null = null;
|
||||
@@ -17,8 +15,6 @@ export function createAcpxConfigAdmission(expected: string, options: { label: st
|
||||
const configs = Array.isArray(result.configOptions) ? result.configOptions.filter(value => object(value).id === options.configId) : [];
|
||||
if (configs.length !== 1) throw failure("native mode configuration is missing or ambiguous");
|
||||
const mode = object(configs[0]).currentValue;
|
||||
// Before exact model selection, Pi can legitimately report a different
|
||||
// native model's level. Observation alone never admits the first prompt.
|
||||
if (typeof mode !== "string" || !options.supportedValues.includes(mode)) throw failure("native mode is unsupported");
|
||||
if (session && options.sessionMode && object(result.modes).currentModeId !== mode) throw failure("native mode acknowledgements conflict");
|
||||
return mode;
|
||||
@@ -56,16 +52,12 @@ export function createAcpxConfigAdmission(expected: string, options: { label: st
|
||||
pending.set(message.id, { method, sessionId: params.sessionId, selectsMode: params.configId === options.configId });
|
||||
} else if (options.sessionMode && method === "session/set_mode") {
|
||||
if (!state.sessionId || params.sessionId !== state.sessionId || params.modeId !== expected) throw failure("mode control changes the selected mode");
|
||||
// This method only returns {}; require a later config/session
|
||||
// acknowledgement rather than treating that empty result as proof.
|
||||
state.mode = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (message.method === "session/update" && object(params.update).sessionUpdate === "config_option_update") {
|
||||
const update = object(params.update);
|
||||
// Config updates may contain only model options. A mode entry is
|
||||
// authoritative observation, but never an admission acknowledgement.
|
||||
if (Array.isArray(update.configOptions) && update.configOptions.some(value => object(value).id === options.configId)) {
|
||||
if (state.sessionId && params.sessionId !== state.sessionId) throw failure("mode update belongs to a different session");
|
||||
const observed = readMode(update, false);
|
||||
|
||||
@@ -22,12 +22,12 @@ export const QUALIFIED_ACPX_PROFILE_DATA = {
|
||||
"protocolVersion": 1,
|
||||
"acpxVersion": "0.13.1",
|
||||
"agent": "pi",
|
||||
"agentProfileVersion": 24,
|
||||
"agentProfileVersion": 25,
|
||||
"agentServerPackage": "pi-acp",
|
||||
"agentServerVersion": "0.0.33",
|
||||
"agentRuntimePackage": "@earendil-works/pi-coding-agent",
|
||||
"agentRuntimeVersion": "1.0.0",
|
||||
"commandDigest": "sha256:d25d674cda9a0853bdbf0521025099f1424c573d2f533630cb67d90e1bfcda3b",
|
||||
"commandDigest": "sha256:dd67387487dadb08da76534c057dc0aa059f7698d1779cbd5fc452813828167a",
|
||||
"permissionPolicy": "interactive"
|
||||
},
|
||||
"cursor": {
|
||||
|
||||
@@ -104,11 +104,19 @@ describe("Pi installation factory", () => {
|
||||
expect(current.commandDigest).not.toBe(prior.commandDigest);
|
||||
});
|
||||
|
||||
it("retains profile 24 while binding the comment-free sandbox source in profile 25", async () => {
|
||||
const read = async (version: number) => JSON.parse(await readFile(new URL(`../../../test-fixtures/pi-acp/profile-v${version}-identity.json`, import.meta.url), "utf8"));
|
||||
const prior = await read(24), current = await read(25);
|
||||
expect(Object.keys(current.declaration).filter(key => JSON.stringify(current.declaration[key]) !== JSON.stringify(prior.declaration[key])).sort())
|
||||
.toEqual(["agentProfileVersion", "runtimeSandboxSourceSha256"]);
|
||||
expect(current.commandDigest).not.toBe(prior.commandDigest);
|
||||
});
|
||||
|
||||
it("binds the profile declaration to the reviewed patch and platform closure pins", async () => {
|
||||
const hash = (bytes: string | Uint8Array) => createHash("sha256").update(bytes).digest("hex");
|
||||
const canonical = (value: any): string => Array.isArray(value) ? `[${value.map(canonical).join(",")}]`
|
||||
: value && typeof value === "object" ? `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}` : JSON.stringify(value);
|
||||
const identity = JSON.parse(await readFile(new URL("../../../test-fixtures/pi-acp/profile-v24-identity.json", import.meta.url), "utf8"));
|
||||
const identity = JSON.parse(await readFile(new URL("../../../test-fixtures/pi-acp/profile-v25-identity.json", import.meta.url), "utf8"));
|
||||
const declaration = identity.declaration;
|
||||
expect(declaration.sharedRuntimeContract).toBe("paperclip.acpx-runtime-contract.v1");
|
||||
expect(identity.commandDigest).toBe(`sha256:${hash(canonical(declaration))}`);
|
||||
@@ -139,21 +147,21 @@ describe("Pi installation factory", () => {
|
||||
|
||||
it("rejects legacy profiles and tampered executable identities", () => {
|
||||
expect(() => assertPiInstallationProfile(candidate())).not.toThrow();
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 1 })).toThrow("version 24");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 2 })).toThrow("version 24");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 3 })).toThrow("version 24");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 4 })).toThrow("version 24");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 5 })).toThrow("version 24");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 6 })).toThrow("version 24");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 7 })).toThrow("version 24");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 8 })).toThrow("version 24");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 9 })).toThrow("version 24");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 10 })).toThrow("version 24");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 11 })).toThrow("version 24");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 12 })).toThrow("version 24");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 13 })).toThrow("version 24");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 14 })).toThrow("version 24");
|
||||
for (const priorVersion of [15, 16, 17, 18, 19, 20, 21, 22, 23]) expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: priorVersion })).toThrow("version 24");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 1 })).toThrow("version 25");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 2 })).toThrow("version 25");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 3 })).toThrow("version 25");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 4 })).toThrow("version 25");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 5 })).toThrow("version 25");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 6 })).toThrow("version 25");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 7 })).toThrow("version 25");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 8 })).toThrow("version 25");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 9 })).toThrow("version 25");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 10 })).toThrow("version 25");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 11 })).toThrow("version 25");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 12 })).toThrow("version 25");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 13 })).toThrow("version 25");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 14 })).toThrow("version 25");
|
||||
for (const priorVersion of [15, 16, 17, 18, 19, 20, 21, 22, 23, 24]) expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: priorVersion })).toThrow("version 25");
|
||||
for (const changed of [{ agentServerVersion: "latest" }, { commandDigest: `sha256:${"0".repeat(64)}` }, { agentRuntimePackage: "ambient-pi" }]) {
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), ...changed })).toThrow("trusted declaration");
|
||||
}
|
||||
|
||||
@@ -483,8 +483,6 @@ export class AcpxRuntimeHost {
|
||||
});
|
||||
}
|
||||
options.signal?.throwIfAborted();
|
||||
// Await this write under ownership even if cancellation arrives. Releasing
|
||||
// the lease before it settles could let a late write affect the next owner.
|
||||
await sandbox.initializeOwnedConfiguration?.();
|
||||
options.signal?.throwIfAborted();
|
||||
assertAcpxProfileEnvironment(options.agent, sandbox.launchEnvironment);
|
||||
|
||||
@@ -53,7 +53,6 @@ export interface AcpxRuntimeSandbox {
|
||||
cacheDirectory: string;
|
||||
agentHomeDirectory: string;
|
||||
workspaceRecordPath: string;
|
||||
/** Mutate provider settings only while the host holds its lifetime lease. */
|
||||
initializeOwnedConfiguration?: () => Promise<void>;
|
||||
launchEnvironment: Readonly<NodeJS.ProcessEnv>;
|
||||
persistedEnvironment: Readonly<NodeJS.ProcessEnv>;
|
||||
@@ -405,9 +404,6 @@ export async function prepareAcpxRuntimeSandbox(input: {
|
||||
`${JSON.stringify({
|
||||
model: input.binding.requestedModel,
|
||||
availableModels: [input.binding.requestedModel],
|
||||
// Claude reloads effort from settings on a new ACP connection. Keep
|
||||
// the bound choice available during model verification and resume;
|
||||
// the protocol guard still requires a provider acknowledgement.
|
||||
...(input.binding.claudeEffort ? { effortLevel: input.binding.claudeEffort } : {}),
|
||||
...((input.binding.permissionMode === "approve-reads" || input.binding.permissionMode === "approve-paperclip")
|
||||
? { permissions: { allow: claudePaperclipPermissionRules(input.tools ?? [], input.binding.permissionMode) } }
|
||||
|
||||
@@ -31,8 +31,6 @@ vi.mock("smol-toml", async (importOriginal) => {
|
||||
...actual,
|
||||
parse: (...args: Parameters<typeof actual.parse>) => {
|
||||
const result = actual.parse(...args);
|
||||
// Exercise both table representations independently of the installed
|
||||
// parser version while retaining the real TOML values.
|
||||
setTablePrototypes(result);
|
||||
return result;
|
||||
},
|
||||
|
||||
@@ -92,8 +92,6 @@ export function trustCodexStartupRoot(codexHome: string, cwd: string): void {
|
||||
const path = join(codexHome, "config.toml");
|
||||
const source = existsSync(path) ? readFileSync(path, "utf8") : "";
|
||||
const config = parse(source);
|
||||
// Retain the parser's table prototypes so strict candidate comparison also
|
||||
// works with parsers that return null-prototype TOML tables.
|
||||
const projects = config.projects ?? parse("");
|
||||
if (
|
||||
typeof projects !== "object" ||
|
||||
|
||||
@@ -1766,9 +1766,6 @@ describe("Capability live runnerd and Codex session", () => {
|
||||
expect((await store.load(replacement.id))?.status).toBe("suspended");
|
||||
});
|
||||
|
||||
// This case includes a deliberate 500 ms killed turn, a resumed turn with
|
||||
// its own 5-second bound, and durable writes. The enclosing test must not
|
||||
// expire at the resumed turn's deadline before those assertions can finish.
|
||||
it("durably resumes a distinct attempt, preserves partial usage, and deduplicates the semantic effect", async () => {
|
||||
const state = providerState();
|
||||
const directory = await mkdtemp(join(tmpdir(), "capability-live-durable-"));
|
||||
|
||||
@@ -396,7 +396,6 @@ it("publishes spawned runner ownership before waiting for provider startup", asy
|
||||
});
|
||||
const opening = bundle.transport.request("thread/start", { cwd: tmpdir(), dynamicTools: codexSemanticToolSpecs() });
|
||||
try {
|
||||
// Same-tick recovery must not mutate an already admitted fresh startup.
|
||||
await expect(bundle.transport.request("thread/read", { threadId: "rejected-duplicate" }))
|
||||
.rejects.toThrow("PRP provider thread is already started");
|
||||
await vi.waitFor(() => expect(onSpawn).toHaveBeenCalledOnce());
|
||||
@@ -2223,7 +2222,6 @@ it("owns blocked Codex home preparation through driver abort without later regis
|
||||
markPreparationStarted();
|
||||
await preparationReleased;
|
||||
preparationContinuedAfterClose = closeSettled;
|
||||
// A synthetic, credential-free write makes the owned preparation visible.
|
||||
await mkdir(input.codexHome, { mode: 0o700 });
|
||||
await writeFile(join(input.codexHome, "preparation-marker"), "fixture preparation drained\n", { mode: 0o600 });
|
||||
});
|
||||
@@ -2264,7 +2262,6 @@ it("owns blocked Codex home preparation through driver abort without later regis
|
||||
.then(value => { openingSettled = true; return { value }; }, error => { openingSettled = true; return { error }; });
|
||||
try {
|
||||
await preparationStarted;
|
||||
// A rejected duplicate must not replace the first admitted startup owner.
|
||||
await expect(transport.request("thread/start", { cwd: root, dynamicTools: [] }))
|
||||
.rejects.toThrow("PRP provider thread is already started");
|
||||
controller.abort(cancelled);
|
||||
@@ -2863,8 +2860,6 @@ it.each(["stuck", "late-release", "late-release-error", "stuck-release", "cooper
|
||||
runtimeDirectory, provider: "codex", model: "gpt-test", timeoutMs: 1_000,
|
||||
environment: { PATH: "", OPENAI_API_KEY: "inert-test-key" },
|
||||
transportOptions: {
|
||||
// Registration and launch are external owners; hash real inert bytes
|
||||
// without executing a daemon, Codex, or an ambient developer binary.
|
||||
runnerBinary: resolve(import.meta.dirname, "../../package.json"),
|
||||
codexCommand: resolve(import.meta.dirname, "../../package.json"),
|
||||
sourceCodexHome: "", runnerProcessLauncher: launch,
|
||||
@@ -2895,8 +2890,6 @@ it.each(["stuck", "late-release", "late-release-error", "stuck-release", "cooper
|
||||
expect(cleanup).not.toHaveBeenCalled();
|
||||
expect((await stat(runtimeDirectory)).isDirectory()).toBe(true);
|
||||
if (mode === "late-release-error") expect((failure as AggregateError).errors).toContain(releaseError);
|
||||
// A registration that finishes after the bounded return still releases
|
||||
// its authority, without retroactively certifying or deleting state.
|
||||
returnRegistration(registered);
|
||||
finishRelease();
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
|
||||
@@ -1338,7 +1338,6 @@ export interface CapabilityRunnerdCodexTransportOptions {
|
||||
controlPlaneRegistration?: (
|
||||
authority: DurablePrpControlPlane,
|
||||
identity?: DurableRecoveryIdentity,
|
||||
/** Setup owners abort preparation before draining or releasing its result. */
|
||||
signal?: AbortSignal,
|
||||
) => Promise<{
|
||||
connectUrl?: string;
|
||||
@@ -4466,8 +4465,6 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
|
||||
}
|
||||
|
||||
#trackStartup<T>(start: () => Promise<T>): Promise<T> {
|
||||
// Admit one owner before invoking startup. A duplicate request's rejection
|
||||
// must never replace the preparation that close is responsible for joining.
|
||||
if (this.#startupOperation !== null) {
|
||||
return Promise.reject(new Error("PRP provider thread is already started"));
|
||||
}
|
||||
@@ -7108,8 +7105,6 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
|
||||
}
|
||||
await Promise.race([
|
||||
ready(), this.#failureSignal,
|
||||
// Readiness is a notification wait, not launch-material preparation.
|
||||
// Its route owner is released by close; joining it first would cycle.
|
||||
this.#closedSignal.then(() => { this.#assertStartupOpen(); }),
|
||||
]);
|
||||
this.#assertStartupOpen();
|
||||
@@ -7160,7 +7155,6 @@ export function createCapabilityRunnerdCodexTransport(
|
||||
export const createRunnerdCodexTransport =
|
||||
createCapabilityRunnerdCodexTransport;
|
||||
|
||||
/** A setup turn uses the same daemon/provider boundary as ordinary native runs. */
|
||||
export async function probeNativeRunnerEnvironment(options: {
|
||||
runtimeDirectory: string;
|
||||
provider: "codex";
|
||||
@@ -7168,7 +7162,6 @@ export async function probeNativeRunnerEnvironment(options: {
|
||||
reasoningEffort?: string;
|
||||
environment: NodeJS.ProcessEnv;
|
||||
timeoutMs?: number;
|
||||
/** The selected remote filesystem supplies this cwd; never inspect it on the controller. */
|
||||
workingDirectory?: string;
|
||||
transportOptions?: CapabilityRunnerdCodexTransportOptions;
|
||||
onCodexCredentialRefresh?: (filename: string) => Promise<void>;
|
||||
@@ -7189,9 +7182,6 @@ export async function probeNativeRunnerEnvironment(options: {
|
||||
let registrationOperation: Promise<unknown> | undefined;
|
||||
let registrationCleanupConfirmed = true;
|
||||
const sourceCodexHome = options.transportOptions?.sourceCodexHome ?? resolveSourceCodexHome(environment);
|
||||
// Never let an unbound home or an external-sandbox flag admit host state or
|
||||
// weaken this read-only probe. The normal materializer copies bound auth and
|
||||
// the managed provider projection into a new daemon-owned Codex home.
|
||||
delete environment.PAPERCLIP_RUNNER_EXTERNAL_SANDBOX;
|
||||
environment.HOME = resolve(remoteFilesystemRoot ?? options.runtimeDirectory, "probe-home");
|
||||
environment.CODEX_HOME = resolve(remoteFilesystemRoot ?? options.runtimeDirectory, "codex-home");
|
||||
@@ -7310,8 +7300,6 @@ export async function probeNativeRunnerEnvironment(options: {
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
controller.abort(failure ?? new Error("Native provider hello probe complete."));
|
||||
// Preserve private recovery state if the production close cannot confirm
|
||||
// teardown; callers remove it only after an authoritative receipt.
|
||||
const teardownErrors: unknown[] = [];
|
||||
const teardownDeadline = Date.now() + 10_000;
|
||||
const awaitTeardown = async (pending: Promise<unknown>, detail: string): Promise<void> => {
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"commandDigest": "sha256:dd67387487dadb08da76534c057dc0aa059f7698d1779cbd5fc452813828167a",
|
||||
"declaration": {
|
||||
"schema": "paperclip.acpx_profile_declaration.v1",
|
||||
"agent": "pi",
|
||||
"agentProfileVersion": 25,
|
||||
"acpxVersion": "0.13.1",
|
||||
"agentServerVersion": "0.0.33",
|
||||
"agentRuntimeVersion": "1.0.0",
|
||||
"nodeVersion": "24.21.0",
|
||||
"closure": {
|
||||
"darwin-arm64": "282022db10150c6632b3444df421342e7d534bdf5d5fb1097a2e79d0625a2bcf",
|
||||
"darwin-x64": "64e251e19009f755c0b04f73ce2138246faab71a961b0f13d75ebfcc34bef12e",
|
||||
"linux-x64": "713b1fdff42fb56a1518bdc084f181d70bee8ebadc3e4b1d76321ed9108c8410"
|
||||
},
|
||||
"wrapperSha256": "9d129b3d38772e93e97080aa6c4e574ac5df4e47ce5bc331a484a9fbf8188b36",
|
||||
"helperSha256": "41e0490b617da0d60e0c8ec58ef311236f945129d99f816cff6897f78da7f91d",
|
||||
"extensionSha256": "f5818573a355702388072b28926897db546a5cf346b5171f646582ce5f9102a4",
|
||||
"agentFilesBinding": "registered-runtime-context-v1",
|
||||
"nativeQuestions": "select-confirm-input-editor-v4-root-field-types",
|
||||
"pathPolicy": "sdk-normalization-v1",
|
||||
"mcpToolNames": "provider-safe-exact-source-v1",
|
||||
"acpxPatchSha256": "00d6af17216cb0cad902b48633eb19bf0ec6b05837a984cb24661fca593f09c1",
|
||||
"nativeAssistantMessages": "pi1-rpc-deltas-v3-bound-message-and-tool-receipts",
|
||||
"nativeNotices": "session-extension-v2-bounded-runtime-failure",
|
||||
"messageProjectionSha256": "17d1e91b948dfa6a56e1905645add52b502041ad843c8d50a13b07956f22aadd",
|
||||
"noticeProjectionSha256": "014485adc690998e395d334ea3ae67f72be6060a2fc78a1185df7b8d1da28fbf",
|
||||
"sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1",
|
||||
"nativeInputDisposition": "queued-only-v1",
|
||||
"cacheWarming": "disabled-owned-decision-v1",
|
||||
"thinkingLevel": {
|
||||
"supported": [
|
||||
"off",
|
||||
"low",
|
||||
"high",
|
||||
"max"
|
||||
],
|
||||
"newConfigurationDefault": "low",
|
||||
"admission": "explicit-native-effective-before-prompt-v1",
|
||||
"recovery": "mode-bound-v1"
|
||||
},
|
||||
"modelSelection": "caller-selected-native-verified-v1",
|
||||
"providerConfigurationSourceSha256": "d5b4c1a426afc85f040c9b55358d1d645299586dc8cec72d1be2509187ca3865",
|
||||
"credentialEnvironmentSourceSha256": "f8dc40ea1aa894bd44754a35a8f65fbcaa2f4c7ffc88cc5fdaa965e0d378d9bc",
|
||||
"runtimeSandboxSourceSha256": "144d62b3415cfedf272b5b052e14af1e41fe50a644af84247fc9c20c7ef7edde",
|
||||
"recoveryIdentitySourceSha256": "8479e04da1ae2f7742e34f045b0270b99e9c3ae39a5b8fb9f164e5776d83224c",
|
||||
"nativeCancellation": "acknowledged-cancelled-terminal-preserves-usage-v1",
|
||||
"dependencySecurityPatchSha256": "5273a195b952f233336122842faaf650193c04f36b9359d20d4d63079b0ce814"
|
||||
}
|
||||
}
|
||||
@@ -441,8 +441,6 @@ beforeEach(() => {
|
||||
"div",
|
||||
{ key: routine.key },
|
||||
createElement("span", null, routine.title),
|
||||
createElement("button", null, "Run now"),
|
||||
createElement("button", { role: "switch" }, "On"),
|
||||
routine.href ? createElement("a", { href: `/PAP${routine.href}` }, "Configure") : null,
|
||||
routine.defaultDrift?.changedFields.length
|
||||
? createElement("span", null, `Plugin defaults changed: ${routine.defaultDrift.changedFields.join(", ")}`)
|
||||
@@ -656,6 +654,46 @@ function mockPersistedWikiSpace(harness: ReturnType<typeof createTestHarness>, s
|
||||
};
|
||||
}
|
||||
|
||||
function renderWikiPage(): string {
|
||||
return renderToStaticMarkup(createElement(WikiPage, {
|
||||
context: { companyId: COMPANY_ID, companyPrefix: "PAP" },
|
||||
} as never));
|
||||
}
|
||||
|
||||
function captureWikiWrites(harness: ReturnType<typeof createTestHarness>) {
|
||||
const writes: Array<{ path: string; contents: string }> = [];
|
||||
harness.ctx.localFolders.writeTextAtomic = async (_companyId, _folderKey, relativePath, contents) => {
|
||||
writes.push({ path: relativePath, contents });
|
||||
return harness.ctx.localFolders.status(COMPANY_ID, "wiki-root");
|
||||
};
|
||||
return writes;
|
||||
}
|
||||
|
||||
function managedSpaceRow(created: {
|
||||
space: { id: string; companyId: string; wikiId: string; slug: string; displayName: string; pathPrefix: string | null };
|
||||
}): Record<string, unknown> {
|
||||
return {
|
||||
id: created.space.id,
|
||||
company_id: created.space.companyId,
|
||||
wiki_id: created.space.wikiId,
|
||||
slug: created.space.slug,
|
||||
display_name: created.space.displayName,
|
||||
space_type: "local_folder",
|
||||
folder_mode: "managed_subfolder",
|
||||
root_folder_key: "wiki-root",
|
||||
path_prefix: created.space.pathPrefix,
|
||||
configured_root_path: null,
|
||||
access_scope: "shared",
|
||||
owner_user_id: null,
|
||||
owner_agent_id: null,
|
||||
team_key: null,
|
||||
settings: {},
|
||||
status: "active",
|
||||
created_at: null,
|
||||
updated_at: null,
|
||||
};
|
||||
}
|
||||
|
||||
describe("LLM Wiki plugin scaffold", () => {
|
||||
it("declares standalone plugin surfaces without core wiki coupling", () => {
|
||||
expect(manifest.id).toBe("paperclipai.plugin-llm-wiki");
|
||||
@@ -814,9 +852,7 @@ describe("LLM Wiki plugin scaffold", () => {
|
||||
|
||||
it("routes legacy Wiki operations URLs to the History issue table", () => {
|
||||
mockPathname = "/PAP/wiki/operations";
|
||||
const markup = renderToStaticMarkup(createElement(WikiPage, {
|
||||
context: { companyId: COMPANY_ID, companyPrefix: "PAP" },
|
||||
} as never));
|
||||
const markup = renderWikiPage();
|
||||
|
||||
expect(markup).toContain("Issues table · project-1 · plugin:paperclipai.plugin-llm-wiki:operation");
|
||||
expect(markup).not.toContain("Recent runs");
|
||||
@@ -825,9 +861,7 @@ describe("LLM Wiki plugin scaffold", () => {
|
||||
|
||||
it("loads AGENTS.md from the Wiki page and exposes an edit affordance", () => {
|
||||
mockPathname = "/PAP/wiki/page/templates/AGENTS.md";
|
||||
const markup = renderToStaticMarkup(createElement(WikiPage, {
|
||||
context: { companyId: COMPANY_ID, companyPrefix: "PAP" },
|
||||
} as never));
|
||||
const markup = renderWikiPage();
|
||||
|
||||
expect(markup).toContain("AGENTS");
|
||||
expect(markup).toContain(">AGENTS.md</h1>");
|
||||
@@ -851,9 +885,7 @@ describe("LLM Wiki plugin scaffold", () => {
|
||||
sourceRefs: [{ kind: "issue", issueIdentifier: "PAP-FOOTER-1" }],
|
||||
};
|
||||
|
||||
const markup = renderToStaticMarkup(createElement(WikiPage, {
|
||||
context: { companyId: COMPANY_ID, companyPrefix: "PAP" },
|
||||
} as never));
|
||||
const markup = renderWikiPage();
|
||||
|
||||
expect(markup).toContain("Current project state.");
|
||||
expect(markup).not.toContain(">Sources</");
|
||||
@@ -878,9 +910,7 @@ updated: 2026-05-04
|
||||
Route sidebar state stays attached to the selected wiki page.
|
||||
`;
|
||||
|
||||
const markup = renderToStaticMarkup(createElement(WikiPage, {
|
||||
context: { companyId: COMPANY_ID, companyPrefix: "PAP" },
|
||||
} as never));
|
||||
const markup = renderWikiPage();
|
||||
|
||||
expect(markup).toContain("<summary");
|
||||
expect(markup).toContain("Properties");
|
||||
@@ -918,9 +948,7 @@ Wiki links preserve normal browser navigation.
|
||||
Duplicate headings receive stable suffixes.
|
||||
`;
|
||||
|
||||
const markup = renderToStaticMarkup(createElement(WikiPage, {
|
||||
context: { companyId: COMPANY_ID, companyPrefix: "PAP" },
|
||||
} as never));
|
||||
const markup = renderWikiPage();
|
||||
|
||||
expect(markup).toContain('aria-label="On this page"');
|
||||
expect(markup).toContain('aria-expanded="true"');
|
||||
@@ -959,9 +987,7 @@ Duplicate headings receive stable suffixes.
|
||||
checkedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const markup = renderToStaticMarkup(createElement(WikiPage, {
|
||||
context: { companyId: COMPANY_ID, companyPrefix: "PAP" },
|
||||
} as never));
|
||||
const markup = renderWikiPage();
|
||||
|
||||
expect(markup).toContain("Repair wiki root folder");
|
||||
expect(markup).toContain("/tmp/deleted-wiki-root");
|
||||
@@ -997,9 +1023,7 @@ Duplicate headings receive stable suffixes.
|
||||
|
||||
it("renders the maintainer settings without plugin metadata or inline AGENTS.md editing", () => {
|
||||
mockPathname = "/PAP/wiki/settings/maintainer";
|
||||
const markup = renderToStaticMarkup(createElement(WikiPage, {
|
||||
context: { companyId: COMPANY_ID, companyPrefix: "PAP" },
|
||||
} as never));
|
||||
const markup = renderWikiPage();
|
||||
|
||||
expect(markup).toContain("Wiki Maintainer");
|
||||
expect(markup).toContain("Adapter: claude local");
|
||||
@@ -1024,9 +1048,7 @@ Duplicate headings receive stable suffixes.
|
||||
details: { name: "Wiki Maintainer", status: "pending_approval", adapterType: "claude_local", icon: "book-open", urlKey: "wiki-maintainer" },
|
||||
};
|
||||
|
||||
const markup = renderToStaticMarkup(createElement(WikiPage, {
|
||||
context: { companyId: COMPANY_ID, companyPrefix: "PAP" },
|
||||
} as never));
|
||||
const markup = renderWikiPage();
|
||||
|
||||
expect(markup).toContain("pending approval");
|
||||
expect(markup).toContain("Approve the agent");
|
||||
@@ -1035,9 +1057,7 @@ Duplicate headings receive stable suffixes.
|
||||
|
||||
it("renders root settings as a compact health checklist with the shared path picker", () => {
|
||||
mockPathname = "/PAP/wiki/settings";
|
||||
const markup = renderToStaticMarkup(createElement(WikiPage, {
|
||||
context: { companyId: COMPANY_ID, companyPrefix: "PAP" },
|
||||
} as never));
|
||||
const markup = renderWikiPage();
|
||||
|
||||
expect(markup).toContain(">Setup</");
|
||||
expect(markup).toContain("Base Folder");
|
||||
@@ -1107,9 +1127,7 @@ Duplicate headings receive stable suffixes.
|
||||
checkedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const markup = renderToStaticMarkup(createElement(WikiPage, {
|
||||
context: { companyId: COMPANY_ID, companyPrefix: "PAP" },
|
||||
} as never));
|
||||
const markup = renderWikiPage();
|
||||
|
||||
expect(markup).toContain("configuration errors detected, fix them all?");
|
||||
expect(markup).toContain("Wiki root folder");
|
||||
@@ -1132,9 +1150,7 @@ Duplicate headings receive stable suffixes.
|
||||
},
|
||||
}];
|
||||
|
||||
const markup = renderToStaticMarkup(createElement(WikiPage, {
|
||||
context: { companyId: COMPANY_ID, companyPrefix: "PAP" },
|
||||
} as never));
|
||||
const markup = renderWikiPage();
|
||||
|
||||
expect(markup).toContain("Managed Skills");
|
||||
expect(markup).toContain("skill issue(s) need attention");
|
||||
@@ -1156,9 +1172,7 @@ Duplicate headings receive stable suffixes.
|
||||
},
|
||||
}];
|
||||
|
||||
const markup = renderToStaticMarkup(createElement(WikiPage, {
|
||||
context: { companyId: COMPANY_ID, companyPrefix: "PAP" },
|
||||
} as never));
|
||||
const markup = renderWikiPage();
|
||||
|
||||
expect(markup).toContain("skill issue(s) need attention");
|
||||
expect(markup).toContain("LLM Wiki Maintainer differs from the plugin default: SKILL.md.");
|
||||
@@ -1178,9 +1192,7 @@ Duplicate headings receive stable suffixes.
|
||||
|
||||
it("renders project settings as a project picker without managed-resource metadata", () => {
|
||||
mockPathname = "/PAP/wiki/settings/project";
|
||||
const markup = renderToStaticMarkup(createElement(WikiPage, {
|
||||
context: { companyId: COMPANY_ID, companyPrefix: "PAP" },
|
||||
} as never));
|
||||
const markup = renderWikiPage();
|
||||
|
||||
expect(markup).toContain("Use existing project");
|
||||
expect(markup).toContain("LLM Wiki");
|
||||
@@ -1199,9 +1211,7 @@ Duplicate headings receive stable suffixes.
|
||||
it("renders space settings with health line styling and disabled access controls", () => {
|
||||
mockPathname = "/PAP/wiki/settings/spaces/default";
|
||||
|
||||
const markup = renderToStaticMarkup(createElement(WikiPage, {
|
||||
context: { companyId: COMPANY_ID, companyPrefix: "PAP" },
|
||||
} as never));
|
||||
const markup = renderWikiPage();
|
||||
|
||||
expect(markup).toContain("Space folder health checklist");
|
||||
expect(markup).toContain("left:8px");
|
||||
@@ -1231,9 +1241,7 @@ Duplicate headings receive stable suffixes.
|
||||
runs: [],
|
||||
};
|
||||
|
||||
const markup = renderToStaticMarkup(createElement(WikiPage, {
|
||||
context: { companyId: COMPANY_ID, companyPrefix: "PAP" },
|
||||
} as never));
|
||||
const markup = renderWikiPage();
|
||||
|
||||
expect(markup).toContain("Agent execution");
|
||||
expect(markup).toContain("Assigned maintainer");
|
||||
@@ -1248,7 +1256,7 @@ Duplicate headings receive stable suffixes.
|
||||
expect(markup).not.toContain("Monthly plugin cap");
|
||||
});
|
||||
|
||||
it("renders managed routines as normal routine rows with run, toggle, and configure controls", () => {
|
||||
it("passes managed routine titles, bindings, and configuration links to the host list", () => {
|
||||
mockPathname = "/PAP/wiki/settings/routines";
|
||||
mockSettingsManagedRoutines = [{
|
||||
status: "resolved",
|
||||
@@ -1265,14 +1273,10 @@ Duplicate headings receive stable suffixes.
|
||||
},
|
||||
details: { cronExpression: "0 3 * * *" },
|
||||
}];
|
||||
const markup = renderToStaticMarkup(createElement(WikiPage, {
|
||||
context: { companyId: COMPANY_ID, companyPrefix: "PAP" },
|
||||
} as never));
|
||||
const markup = renderWikiPage();
|
||||
|
||||
expect(markup).toContain("Run LLM Wiki lint");
|
||||
expect(markup).toContain("Run now");
|
||||
expect(markup).toContain("Configure");
|
||||
expect(markup).toContain("role=\"switch\"");
|
||||
expect(markup).toContain("LLM Wiki · Wiki Maintainer");
|
||||
expect(markup).toContain("href=\"/PAP/routines/routine-1\"");
|
||||
});
|
||||
@@ -1299,9 +1303,7 @@ Duplicate headings receive stable suffixes.
|
||||
details: { cronExpression: "0 3 * * *" },
|
||||
}];
|
||||
|
||||
const markup = renderToStaticMarkup(createElement(WikiPage, {
|
||||
context: { companyId: COMPANY_ID, companyPrefix: "PAP" },
|
||||
} as never));
|
||||
const markup = renderWikiPage();
|
||||
|
||||
expect(markup).toContain("Routine defaults changed");
|
||||
expect(markup).toContain("Plugin defaults changed: description");
|
||||
@@ -1319,9 +1321,7 @@ Duplicate headings receive stable suffixes.
|
||||
details: { name: "Wiki Maintainer", status: "idle", adapterType: "claude_local", icon: "book-open", urlKey: "wiki-maintainer" },
|
||||
};
|
||||
|
||||
const markup = renderToStaticMarkup(createElement(WikiPage, {
|
||||
context: { companyId: COMPANY_ID, companyPrefix: "PAP" },
|
||||
} as never));
|
||||
const markup = renderWikiPage();
|
||||
|
||||
expect(markup).toContain("Wiki Maintainer instructions differ from the plugin default: AGENTS.md.");
|
||||
expect(markup).toContain("Wiki Maintainer instruction defaults changed: AGENTS.md");
|
||||
@@ -1344,9 +1344,7 @@ Duplicate headings receive stable suffixes.
|
||||
details: { cronExpression: "0 3 * * *" },
|
||||
}];
|
||||
|
||||
const markup = renderToStaticMarkup(createElement(WikiPage, {
|
||||
context: { companyId: COMPANY_ID, companyPrefix: "PAP" },
|
||||
} as never));
|
||||
const markup = renderWikiPage();
|
||||
|
||||
expect(markup).toContain("Routine setup needs repair");
|
||||
expect(markup).toContain("Fix routines");
|
||||
@@ -1358,9 +1356,7 @@ Duplicate headings receive stable suffixes.
|
||||
|
||||
it("renders legacy lint routes inside the Wiki settings section", () => {
|
||||
mockPathname = "/PAP/wiki/lint";
|
||||
const markup = renderToStaticMarkup(createElement(WikiPage, {
|
||||
context: { companyId: COMPANY_ID, companyPrefix: "PAP" },
|
||||
} as never));
|
||||
const markup = renderWikiPage();
|
||||
|
||||
expect(markup).toContain("Run lint now");
|
||||
expect(markup).toContain("Recent lint runs");
|
||||
@@ -1369,9 +1365,7 @@ Duplicate headings receive stable suffixes.
|
||||
|
||||
it("does not expose IDEA.md pattern editing as a settings section", () => {
|
||||
mockPathname = "/PAP/wiki/settings";
|
||||
const markup = renderToStaticMarkup(createElement(WikiPage, {
|
||||
context: { companyId: COMPANY_ID, companyPrefix: "PAP" },
|
||||
} as never));
|
||||
const markup = renderWikiPage();
|
||||
|
||||
expect(markup).toContain("LLM Wiki settings sections");
|
||||
expect(markup).not.toContain(">Pattern</span>");
|
||||
@@ -1381,9 +1375,7 @@ Duplicate headings receive stable suffixes.
|
||||
|
||||
it("does not expose plugin capabilities as a settings section", () => {
|
||||
mockPathname = "/PAP/wiki/settings";
|
||||
const markup = renderToStaticMarkup(createElement(WikiPage, {
|
||||
context: { companyId: COMPANY_ID, companyPrefix: "PAP" },
|
||||
} as never));
|
||||
const markup = renderWikiPage();
|
||||
|
||||
expect(markup).toContain("LLM Wiki settings sections");
|
||||
expect(markup).not.toContain("Plugin capabilities");
|
||||
@@ -1392,11 +1384,7 @@ Duplicate headings receive stable suffixes.
|
||||
|
||||
it("reconciles managed maintenance routines through stable agent and project refs", async () => {
|
||||
const harness = createTestHarness({ manifest });
|
||||
const writes: Array<{ path: string; contents: string }> = [];
|
||||
harness.ctx.localFolders.writeTextAtomic = async (_companyId, _folderKey, relativePath, contents) => {
|
||||
writes.push({ path: relativePath, contents });
|
||||
return harness.ctx.localFolders.status(COMPANY_ID, "wiki-root");
|
||||
};
|
||||
const writes = captureWikiWrites(harness);
|
||||
|
||||
await plugin.definition.setup(harness.ctx);
|
||||
|
||||
@@ -1651,11 +1639,7 @@ Duplicate headings receive stable suffixes.
|
||||
const harness = createTestHarness({ manifest });
|
||||
const issue = paperclipIssue();
|
||||
harness.seed({ issues: [issue] });
|
||||
const writes: Array<{ path: string; contents: string }> = [];
|
||||
harness.ctx.localFolders.writeTextAtomic = async (_companyId, _folderKey, relativePath, contents) => {
|
||||
writes.push({ path: relativePath, contents });
|
||||
return harness.ctx.localFolders.status(COMPANY_ID, "wiki-root");
|
||||
};
|
||||
const writes = captureWikiWrites(harness);
|
||||
|
||||
await plugin.definition.setup(harness.ctx);
|
||||
await harness.emit("issue.created", { identifier: issue.identifier }, {
|
||||
@@ -1674,11 +1658,7 @@ Duplicate headings receive stable suffixes.
|
||||
const harness = createTestHarness({ manifest });
|
||||
const issue = paperclipIssue();
|
||||
harness.seed({ issues: [issue] });
|
||||
const writes: Array<{ path: string; contents: string }> = [];
|
||||
harness.ctx.localFolders.writeTextAtomic = async (_companyId, _folderKey, relativePath, contents) => {
|
||||
writes.push({ path: relativePath, contents });
|
||||
return harness.ctx.localFolders.status(COMPANY_ID, "wiki-root");
|
||||
};
|
||||
const writes = captureWikiWrites(harness);
|
||||
|
||||
await plugin.definition.setup(harness.ctx);
|
||||
const policy = await harness.performAction<{ enabled: boolean; sources: { issues: boolean; comments: boolean; documents: boolean } }>(
|
||||
@@ -2768,11 +2748,7 @@ Duplicate headings receive stable suffixes.
|
||||
|
||||
it("bootstraps required local wiki files through the local folder API", async () => {
|
||||
const harness = createTestHarness({ manifest });
|
||||
const writes: Array<{ path: string; contents: string }> = [];
|
||||
harness.ctx.localFolders.writeTextAtomic = async (_companyId, _folderKey, relativePath, contents) => {
|
||||
writes.push({ path: relativePath, contents });
|
||||
return harness.ctx.localFolders.status(COMPANY_ID, "wiki-root");
|
||||
};
|
||||
const writes = captureWikiWrites(harness);
|
||||
|
||||
await plugin.definition.setup(harness.ctx);
|
||||
const result = await harness.performAction<{ writtenFiles: string[]; managedSkills: WikiSkillResource[] }>("bootstrap-root", {
|
||||
@@ -3009,11 +2985,7 @@ Duplicate headings receive stable suffixes.
|
||||
|
||||
it("captures raw sources into local files and plugin metadata", async () => {
|
||||
const harness = createTestHarness({ manifest });
|
||||
const writes: Array<{ path: string; contents: string }> = [];
|
||||
harness.ctx.localFolders.writeTextAtomic = async (_companyId, _folderKey, relativePath, contents) => {
|
||||
writes.push({ path: relativePath, contents });
|
||||
return harness.ctx.localFolders.status(COMPANY_ID, "wiki-root");
|
||||
};
|
||||
const writes = captureWikiWrites(harness);
|
||||
|
||||
await plugin.definition.setup(harness.ctx);
|
||||
const result = await harness.performAction<{ rawPath: string; hash: string }>("capture-source", {
|
||||
@@ -3031,11 +3003,7 @@ Duplicate headings receive stable suffixes.
|
||||
|
||||
it("preserves manual source ingest for non-default spaces while refusing Paperclip distillation there", async () => {
|
||||
const harness = createTestHarness({ manifest });
|
||||
const writes: Array<{ path: string; contents: string }> = [];
|
||||
harness.ctx.localFolders.writeTextAtomic = async (_companyId, _folderKey, relativePath, contents) => {
|
||||
writes.push({ path: relativePath, contents });
|
||||
return harness.ctx.localFolders.status(COMPANY_ID, "wiki-root");
|
||||
};
|
||||
const writes = captureWikiWrites(harness);
|
||||
harness.seed({
|
||||
agents: [wikiMaintainerAgent()],
|
||||
projects: [existingProject()],
|
||||
@@ -3057,26 +3025,7 @@ Duplicate headings receive stable suffixes.
|
||||
companyId: COMPANY_ID,
|
||||
displayName: "Team Research",
|
||||
});
|
||||
spaces.set(created.space.slug, {
|
||||
id: created.space.id,
|
||||
company_id: created.space.companyId,
|
||||
wiki_id: created.space.wikiId,
|
||||
slug: created.space.slug,
|
||||
display_name: created.space.displayName,
|
||||
space_type: "local_folder",
|
||||
folder_mode: "managed_subfolder",
|
||||
root_folder_key: "wiki-root",
|
||||
path_prefix: created.space.pathPrefix,
|
||||
configured_root_path: null,
|
||||
access_scope: "shared",
|
||||
owner_user_id: null,
|
||||
owner_agent_id: null,
|
||||
team_key: null,
|
||||
settings: {},
|
||||
status: "active",
|
||||
created_at: null,
|
||||
updated_at: null,
|
||||
});
|
||||
spaces.set(created.space.slug, managedSpaceRow(created));
|
||||
|
||||
const source = await harness.performAction<{ rawPath: string; spaceSlug: string }>("capture-source", {
|
||||
companyId: COMPANY_ID,
|
||||
@@ -3299,26 +3248,7 @@ Duplicate headings receive stable suffixes.
|
||||
companyId: COMPANY_ID,
|
||||
displayName: "Research Space",
|
||||
});
|
||||
spaces.set(created.space.slug, {
|
||||
id: created.space.id,
|
||||
company_id: created.space.companyId,
|
||||
wiki_id: created.space.wikiId,
|
||||
slug: created.space.slug,
|
||||
display_name: created.space.displayName,
|
||||
space_type: "local_folder",
|
||||
folder_mode: "managed_subfolder",
|
||||
root_folder_key: "wiki-root",
|
||||
path_prefix: created.space.pathPrefix,
|
||||
configured_root_path: null,
|
||||
access_scope: "shared",
|
||||
owner_user_id: null,
|
||||
owner_agent_id: null,
|
||||
team_key: null,
|
||||
settings: {},
|
||||
status: "active",
|
||||
created_at: null,
|
||||
updated_at: null,
|
||||
});
|
||||
spaces.set(created.space.slug, managedSpaceRow(created));
|
||||
await harness.performAction("bootstrap-space", {
|
||||
companyId: COMPANY_ID,
|
||||
spaceSlug: created.space.slug,
|
||||
@@ -3364,6 +3294,8 @@ Duplicate headings receive stable suffixes.
|
||||
|
||||
const pageWrites = harness.dbExecutes.filter((execute) => execute.sql.includes("wiki_pages"));
|
||||
const sourceWrites = harness.dbExecutes.filter((execute) => execute.sql.includes("wiki_sources"));
|
||||
expect(pageWrites).toHaveLength(4);
|
||||
expect(sourceWrites).toHaveLength(2);
|
||||
expect(pageWrites.every((write) => write.sql.includes("space_id"))).toBe(true);
|
||||
expect(sourceWrites.every((write) => write.sql.includes("space_id"))).toBe(true);
|
||||
});
|
||||
@@ -3371,11 +3303,7 @@ Duplicate headings receive stable suffixes.
|
||||
it("ingests source metadata and creates a hidden plugin-operation issue", async () => {
|
||||
const harness = createTestHarness({ manifest });
|
||||
harness.seed({ agents: [wikiMaintainerAgent()] });
|
||||
const writes: Array<{ path: string; contents: string }> = [];
|
||||
harness.ctx.localFolders.writeTextAtomic = async (_companyId, _folderKey, relativePath, contents) => {
|
||||
writes.push({ path: relativePath, contents });
|
||||
return harness.ctx.localFolders.status(COMPANY_ID, "wiki-root");
|
||||
};
|
||||
const writes = captureWikiWrites(harness);
|
||||
|
||||
await plugin.definition.setup(harness.ctx);
|
||||
const result = await harness.performAction<{
|
||||
@@ -3429,11 +3357,7 @@ Duplicate headings receive stable suffixes.
|
||||
it("rejects oversized source capture before raw writes or operation creation", async () => {
|
||||
const harness = createTestHarness({ manifest, config: { maxSourceBytes: 16 } });
|
||||
harness.seed({ agents: [wikiMaintainerAgent()] });
|
||||
const writes: Array<{ path: string; contents: string }> = [];
|
||||
harness.ctx.localFolders.writeTextAtomic = async (_companyId, _folderKey, relativePath, contents) => {
|
||||
writes.push({ path: relativePath, contents });
|
||||
return harness.ctx.localFolders.status(COMPANY_ID, "wiki-root");
|
||||
};
|
||||
const writes = captureWikiWrites(harness);
|
||||
|
||||
await plugin.definition.setup(harness.ctx);
|
||||
await expect(harness.performAction("ingest-source", {
|
||||
@@ -3568,26 +3492,7 @@ Duplicate headings receive stable suffixes.
|
||||
companyId: COMPANY_ID,
|
||||
displayName: "Research Space",
|
||||
});
|
||||
spaces.set(created.space.slug, {
|
||||
id: created.space.id,
|
||||
company_id: created.space.companyId,
|
||||
wiki_id: created.space.wikiId,
|
||||
slug: created.space.slug,
|
||||
display_name: created.space.displayName,
|
||||
space_type: "local_folder",
|
||||
folder_mode: "managed_subfolder",
|
||||
root_folder_key: "wiki-root",
|
||||
path_prefix: created.space.pathPrefix,
|
||||
configured_root_path: null,
|
||||
access_scope: "shared",
|
||||
owner_user_id: null,
|
||||
owner_agent_id: null,
|
||||
team_key: null,
|
||||
settings: {},
|
||||
status: "active",
|
||||
created_at: null,
|
||||
updated_at: null,
|
||||
});
|
||||
spaces.set(created.space.slug, managedSpaceRow(created));
|
||||
|
||||
const result = await harness.performAction<{
|
||||
operationId: string;
|
||||
@@ -3669,6 +3574,13 @@ Duplicate headings receive stable suffixes.
|
||||
it("starts query sessions, records run ids, and forwards session events to plugin streams", async () => {
|
||||
const harness = createTestHarness({ manifest });
|
||||
harness.seed({ agents: [wikiMaintainerAgent()] });
|
||||
let issuedRunId: string | undefined;
|
||||
const originalSendMessage = harness.ctx.agents.sessions.sendMessage.bind(harness.ctx.agents.sessions);
|
||||
harness.ctx.agents.sessions.sendMessage = async (...args) => {
|
||||
const receipt = await originalSendMessage(...args);
|
||||
issuedRunId = receipt.runId;
|
||||
return receipt;
|
||||
};
|
||||
const streamEvents: unknown[] = [];
|
||||
harness.ctx.streams.emit = (_channel, event) => {
|
||||
streamEvents.push(event);
|
||||
@@ -3689,7 +3601,15 @@ Duplicate headings receive stable suffixes.
|
||||
expect(result.querySessionId).toBe(result.operationId);
|
||||
expect(result.channel).toBe(`llm-wiki:query:${result.operationId}`);
|
||||
expect(harness.dbExecutes.some((execute) => execute.sql.includes("wiki_query_sessions"))).toBe(true);
|
||||
expect(harness.dbExecutes.some((execute) => execute.sql.includes("run_ids"))).toBe(true);
|
||||
expect(issuedRunId).toEqual(expect.any(String));
|
||||
expect(issuedRunId).not.toBe("");
|
||||
expect(result.runId).toBe(issuedRunId);
|
||||
const runUpdate = harness.dbExecutes.find((execute) =>
|
||||
/UPDATE .*wiki_operations/.test(execute.sql) && execute.sql.includes("run_ids"),
|
||||
);
|
||||
expect(runUpdate?.params?.slice(0, 4)).toEqual([
|
||||
COMPANY_ID, result.operationId, "running", JSON.stringify([issuedRunId]),
|
||||
]);
|
||||
|
||||
harness.simulateSessionEvent(result.sessionId, {
|
||||
runId: result.runId,
|
||||
@@ -3722,11 +3642,7 @@ Duplicate headings receive stable suffixes.
|
||||
it("files a streamed query answer as a page through a hidden file-as-page operation", async () => {
|
||||
const harness = createTestHarness({ manifest });
|
||||
harness.seed({ agents: [wikiMaintainerAgent()] });
|
||||
const writes: Array<{ path: string; contents: string }> = [];
|
||||
harness.ctx.localFolders.writeTextAtomic = async (_companyId, _folderKey, relativePath, contents) => {
|
||||
writes.push({ path: relativePath, contents });
|
||||
return harness.ctx.localFolders.status(COMPANY_ID, "wiki-root");
|
||||
};
|
||||
const writes = captureWikiWrites(harness);
|
||||
await plugin.definition.setup(harness.ctx);
|
||||
|
||||
const result = await harness.performAction<{ path: string; operationId: string; page: { revisionId: string } }>(
|
||||
|
||||
@@ -7,7 +7,6 @@ export interface AgentRunnerAvailability {
|
||||
defaultRunner: AgentRunner;
|
||||
}
|
||||
|
||||
/** Only qualified harnesses graduate their creation default. */
|
||||
export function paperclipRunnerProfileForHarness(harness: string): { provider: "codex" } | { provider: "acpx"; acpxAgent: "claude" } | undefined {
|
||||
if (harness === "codex_local") return { provider: "codex" };
|
||||
if (harness === "claude_local") return { provider: "acpx", acpxAgent: "claude" };
|
||||
|
||||
@@ -763,11 +763,6 @@ describe("AppDefinition catalog", () => {
|
||||
APP_DEFINITIONS.find((app) => app.slug === "linear")?.methods[0]?.defaults
|
||||
?.scopesHint,
|
||||
).toEqual(["read", "write"]));
|
||||
it("requests Hugging Face MCP read, contribution and job scopes", () =>
|
||||
expect(
|
||||
APP_DEFINITIONS.find((app) => app.slug === "hugging-face")?.methods[0]
|
||||
?.defaults?.scopesHint,
|
||||
).toEqual(["read-mcp", "read-repos", "contribute-repos", "jobs"]));
|
||||
it("defaults every new connection action to allowed except Enterpret writes", () => {
|
||||
for (const app of APP_DEFINITIONS)
|
||||
for (const method of app.methods) {
|
||||
@@ -1327,21 +1322,6 @@ describe("AppDefinition catalog", () => {
|
||||
?.available,
|
||||
).toBe(false);
|
||||
});
|
||||
it("enforces method and field invariants", () => {
|
||||
for (const app of APP_DEFINITIONS)
|
||||
for (const method of app.methods) {
|
||||
if (
|
||||
method.auth === "api_key" &&
|
||||
(method.purpose ?? "tool") !== "channel"
|
||||
)
|
||||
expect(method.keyPlacement).toBeTruthy();
|
||||
if (method.auth === "oauth")
|
||||
expect(method.ownershipModes.length).toBeGreaterThan(0);
|
||||
for (const field of method.credentialFields ?? [])
|
||||
if (field.required && field.type !== "checkbox")
|
||||
expect(field.placeholder).toBeTruthy();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ export function buildSlackAppManifest(input: {
|
||||
redirectUri?: string;
|
||||
}) {
|
||||
return {
|
||||
display_information: { name: input.app.appName },
|
||||
display_information: { name: input.app.appName, description: `Talk to ${input.agentName} and manage work in Paperclip.`.slice(0, 140) },
|
||||
features: {
|
||||
app_home: { home_tab_enabled: false, messages_tab_enabled: true, messages_tab_read_only_enabled: false },
|
||||
agent_view: { agent_description: "Work with a Paperclip agent in a task-backed conversation." },
|
||||
@@ -60,6 +60,7 @@ export const slackRegistrationStateSchema = z.object({
|
||||
status: slackRegistrationStatusSchema,
|
||||
appId: z.string().nullable().optional(),
|
||||
managementUrl: z.string().url().nullable().optional(),
|
||||
managerGrantId: z.string().uuid().nullable().optional(),
|
||||
errorCode: z.string().nullable().optional(),
|
||||
}).strict();
|
||||
export type SlackRegistrationState = z.infer<typeof slackRegistrationStateSchema>;
|
||||
@@ -79,11 +80,33 @@ export const slackAccountStateSchema = z.object({
|
||||
}).strict();
|
||||
export type SlackAccountState = z.infer<typeof slackAccountStateSchema>;
|
||||
export const slackInstallAuthorizationSchema = z.object({ authorizationUrl: z.string().url(), expiresAt: z.string().datetime() });
|
||||
export const slackManagerAuthorizationSchema = slackInstallAuthorizationSchema.extend({ handoff: z.object({ kind: z.literal("paperclip_cloud"), session: z.string().min(16).max(512) }).optional() });
|
||||
export type SlackManagerAuthorization = z.infer<typeof slackManagerAuthorizationSchema>;
|
||||
export type SlackInstallAuthorization = z.infer<typeof slackInstallAuthorizationSchema>;
|
||||
export const slackSetupActionSchema = z.object({}).strict();
|
||||
|
||||
export const slackTestMessageSchema = z.object({
|
||||
requestId: z.string().uuid(),
|
||||
slackUserId: z.string().regex(/^[UW][A-Z0-9]+$/).optional(),
|
||||
accountCursor: z.string().min(1).max(512).optional(),
|
||||
}).strict();
|
||||
export type SlackTestMessageInput = z.infer<typeof slackTestMessageSchema>;
|
||||
export const slackTestMessageResultSchema = z.discriminatedUnion("status", [
|
||||
z.object({ status: z.literal("sent"), slackUrl: z.string().url() }).strict(),
|
||||
z.object({ status: z.literal("connection_required"), accounts: z.array(z.object({
|
||||
id: z.string(), name: z.string(),
|
||||
}).strict()), nextCursor: z.string().nullable() }).strict(),
|
||||
z.object({ status: z.literal("connection_pending"), principalId: z.string().uuid(), expiresAt: z.string().datetime() }).strict(),
|
||||
]);
|
||||
export type SlackTestMessageResult = z.infer<typeof slackTestMessageResultSchema>;
|
||||
|
||||
export function slackRegistrationErrorMessage(code: string): string {
|
||||
const messages: Record<string, string> = {
|
||||
slack_managed_unavailable: "Managed Slack setup is unavailable. Try again later or use your own app before creation starts.",
|
||||
slack_manager_reauthorize: "Reconnect your Slack workspace to continue setting up this agent.",
|
||||
slack_approval_pending: "Slack is waiting for your workspace administrator to approve this app.",
|
||||
slack_approval_required: "Your Slack workspace requires approval. Continue in Slack to request it.",
|
||||
slack_approval_denied: "Your Slack administrator declined this installation. Contact them to continue.",
|
||||
slack_configuration_token_invalid: "Slack rejected the app configuration access token. Generate a new access token and try again.",
|
||||
slack_manifest_invalid: "Slack rejected this app configuration. Check the app details or use manual setup.",
|
||||
slack_manifest_update_pending: "Your Slack app is saved, but its event settings still need to be applied. Enter an app configuration access token to retry configuring the same app.",
|
||||
@@ -96,9 +119,29 @@ export function slackRegistrationErrorMessage(code: string): string {
|
||||
slack_install_identity_mismatch: "Slack returned a different app, workspace, or bot. Install the app created for this connection.",
|
||||
slack_install_scopes_missing: "Slack did not grant all required bot permissions. Install the app again and approve its requested permissions.",
|
||||
slack_bot_already_connected: "This Slack bot is already connected to Paperclip. Resume its existing connection or use a different app.",
|
||||
slack_install_token_invalid: "Slack rejected the saved bot credentials. Continue in Slack to authorize this app again.",
|
||||
slack_configuration_incomplete: "The app is installed, but Paperclip could not finish connecting it. Retry connecting the saved installation.",
|
||||
slack_install_account_missing: "Slack did not identify your account. Authorize the same app again to connect your Slack account.",
|
||||
slack_install_account_conflict: "This Slack account already has an existing link. Manage account links in connection settings, or authorize with your own Slack account.",
|
||||
};
|
||||
return messages[code] ?? "Slack setup could not be completed. Resume setup and try again.";
|
||||
}
|
||||
|
||||
/** Persisted legacy values keep their original meaning. Missing means manual. */
|
||||
export type SlackSetupMethod = "managed" | "automatic" | "manual" | "existing";
|
||||
export function isProvisionedSlackSetup(method: string | undefined): boolean {
|
||||
return method === "automatic" || method === "managed";
|
||||
}
|
||||
export const SLACK_MANAGER_SCOPES = ["app_configurations:read", "app_configurations:write", "managed_apps:install"] as const;
|
||||
export const slackSetupOptionsSchema = z.object({
|
||||
managedAvailable: z.boolean(),
|
||||
defaultMethod: z.enum(["managed", "automatic"]),
|
||||
workspaces: z.array(z.object({ grantId: z.string().uuid(), workspaceId: z.string(), workspaceName: z.string(), userId: z.string() })),
|
||||
});
|
||||
export type SlackSetupOptions = z.infer<typeof slackSetupOptionsSchema>;
|
||||
export const slackManagedProvisionSchema = z.object({
|
||||
requestId: z.string().uuid(), grantId: z.string().uuid(), confirmedNoAppCreated: z.boolean().optional(), reconnect: z.boolean().optional(),
|
||||
}).strict();
|
||||
export type SlackManagedProvisionInput = z.infer<typeof slackManagedProvisionSchema>;
|
||||
export const slackManagedResultSchema = z.object({ authorization: slackInstallAuthorizationSchema.optional() });
|
||||
export type SlackManagedResult = z.infer<typeof slackManagedResultSchema>;
|
||||
@@ -190,7 +190,8 @@ export interface SlackAppConfiguration {
|
||||
}
|
||||
|
||||
export interface ChatEndpointSetupState {
|
||||
slackSetupMethod?: "automatic" | "manual" | "existing";
|
||||
slackSetupMethod?: "managed" | "automatic" | "manual" | "existing";
|
||||
slackManagerError?: string;
|
||||
slackRegistration?: import("../slack-app-manifest.js").SlackRegistrationState;
|
||||
/** Provider-confirmed app icon outcome; configuration tokens are never retained. */
|
||||
slackAvatar?: import("../slack-app-manifest.js").SlackAvatarState;
|
||||
@@ -508,10 +509,11 @@ export interface CreateChatEndpointInput {
|
||||
applicationId?: string;
|
||||
name?: string;
|
||||
slackApp?: SlackAppConfiguration;
|
||||
slackSetupMethod?: "managed" | "automatic" | "manual" | "existing";
|
||||
}
|
||||
|
||||
export interface UpdateChatEndpointInput {
|
||||
slackSetupMethod?: "automatic" | "manual" | "existing";
|
||||
slackSetupMethod?: "managed" | "automatic" | "manual" | "existing";
|
||||
slackApp?: SlackAppConfiguration;
|
||||
communicationInstructions?: string;
|
||||
allowDirectMessages?: boolean;
|
||||
|
||||
@@ -80,9 +80,10 @@ export const createChatEndpointSchema = z
|
||||
applicationId: z.string().uuid().optional(),
|
||||
name: z.string().trim().min(1).max(160).optional(),
|
||||
slackApp: slackAppConfigurationSchema.optional(),
|
||||
slackSetupMethod: z.enum(["managed", "automatic", "manual", "existing"]).optional(),
|
||||
})
|
||||
.strict()
|
||||
.refine(value => !value.slackApp || value.provider === "slack", {
|
||||
.refine(value => (!value.slackApp && !value.slackSetupMethod) || value.provider === "slack", {
|
||||
message: "Slack app details only apply to Slack connections", path: ["slackApp"],
|
||||
});
|
||||
|
||||
@@ -90,7 +91,7 @@ export const updateChatEndpointSchema = z
|
||||
.object({
|
||||
communicationInstructions: multilineTextSchema.pipe(z.string().trim().max(4000)).optional(),
|
||||
slackApp: slackAppConfigurationSchema.optional(),
|
||||
slackSetupMethod: z.enum(["automatic", "manual", "existing"]).optional(),
|
||||
slackSetupMethod: z.enum(["managed", "automatic", "manual", "existing"]).optional(),
|
||||
allowDirectMessages: z.boolean().optional(),
|
||||
requireAtMention: z.boolean().optional(),
|
||||
allowGroupChats: z.boolean().optional(),
|
||||
|
||||
@@ -7,7 +7,7 @@ import { resolveCanonicalWorktreeSeedSource } from "./worktree-seed-source.js";
|
||||
const cleanup: string[] = [];
|
||||
|
||||
function makeInstance(prefix: string, instanceId: string) {
|
||||
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
||||
const cwd = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), prefix));
|
||||
cleanup.push(cwd);
|
||||
const configDir = path.join(cwd, ".paperclip");
|
||||
const configPath = path.join(configDir, "config.json");
|
||||
@@ -22,7 +22,7 @@ function makeInstance(prefix: string, instanceId: string) {
|
||||
* names its instance by directory and has no adjacent .env.
|
||||
*/
|
||||
function makeInstanceRoot(instanceId: string) {
|
||||
const home = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-seed-home-"));
|
||||
const home = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-seed-home-"));
|
||||
cleanup.push(home);
|
||||
const configDir = path.join(home, "instances", instanceId);
|
||||
fs.mkdirSync(configDir, { recursive: true });
|
||||
@@ -33,7 +33,7 @@ function makeInstanceRoot(instanceId: string) {
|
||||
|
||||
/** A managed project checkout: a plain clone with no `.paperclip` of its own. */
|
||||
function makePlainCheckout() {
|
||||
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-seed-checkout-"));
|
||||
const cwd = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-seed-checkout-"));
|
||||
cleanup.push(cwd);
|
||||
return cwd;
|
||||
}
|
||||
|
||||
File renamed without changes.
@@ -16,6 +16,7 @@ const playwrightConfig = path.join(repoRoot, "tests", "e2e", "playwright.config.
|
||||
const prCallerWorkflow = path.join(repoRoot, ".github", "workflows", "pr.yml");
|
||||
const trustedPrWorkflowPath = ".github/workflows/pr-trusted.yml";
|
||||
const trustedPrWorkflow = path.join(repoRoot, trustedPrWorkflowPath);
|
||||
const routingWorkflow = path.join(repoRoot, ".github", "workflows", "ci-routing.yml");
|
||||
|
||||
const SHARD_COUNT = 8;
|
||||
|
||||
@@ -54,11 +55,11 @@ function readWorkflowJobs(workflow) {
|
||||
}
|
||||
|
||||
function runStackScope(stack, prBaseRef) {
|
||||
const workflow = readFileSync(trustedPrWorkflow, "utf8");
|
||||
const workflow = readFileSync(routingWorkflow, "utf8");
|
||||
const match = workflow.match(
|
||||
/ - name: Select stacked PR CI scope[\s\S]*? run: \|\n([\s\S]*?)\n\n policy:/,
|
||||
/ - name: Select stacked PR CI scope[\s\S]*? run: \|\n([\s\S]*?)\s*$/,
|
||||
);
|
||||
assert.ok(match, "trusted workflow must define the stacked PR scope script");
|
||||
assert.ok(match, "routing workflow must define the stacked PR scope script");
|
||||
const script = match[1]
|
||||
.split("\n")
|
||||
.map((line) => line.replace(/^ {10}/, ""))
|
||||
@@ -207,10 +208,22 @@ test("the trusted PR workflow keeps a stable aggregate check named e2e over the
|
||||
}
|
||||
});
|
||||
|
||||
test("runner selection has reserved AWS capacity and executes metadata checks only", () => {
|
||||
const workflow = readFileSync(routingWorkflow, "utf8");
|
||||
const jobs = readWorkflowJobs(workflow.split(/^jobs:\n/m)[1]);
|
||||
assert.deepEqual([...jobs.keys()], ["gate"]);
|
||||
assert.match(jobs.get("gate"), /^ {4}runs-on: \$\{\{ vars\.AWS_CI_ENABLED[\s\S]*paperclip-ci-routing-x64/m);
|
||||
assert.doesNotMatch(jobs.get("gate"), /uses:|actions\/checkout|secrets\./);
|
||||
assert.match(workflow, /^ {6}runner:\n {8}value: \$\{\{ jobs\.gate\.outputs\.runner \}\}$/m);
|
||||
assert.match(workflow, /^ {6}full_ci:\n {8}value: \$\{\{ jobs\.gate\.outputs\.full_ci \}\}$/m);
|
||||
assert.doesNotMatch(workflow, /^ {2}(?:actions|contents|pull-requests): write$/m);
|
||||
});
|
||||
|
||||
test("the trusted PR workflow limits full CI to merge-relevant stack layers", () => {
|
||||
const workflow = readFileSync(trustedPrWorkflow, "utf8");
|
||||
const jobs = readWorkflowJobs(workflow);
|
||||
const gate = jobs.get("gate");
|
||||
assert.match(jobs.get("gate"), /uses: paperclipai\/paperclip\/\.github\/workflows\/ci-routing\.yml@master/);
|
||||
const gate = readWorkflowJobs(readFileSync(routingWorkflow, "utf8")).get("gate");
|
||||
|
||||
assert.match(gate, /^ {6}full_ci: \$\{\{ steps\.scope\.outputs\.full_ci \}\}$/m);
|
||||
assert.match(gate, /STACK_JSON: \$\{\{ toJSON\(github\.event\.pull_request\.stack\) \}\}/);
|
||||
|
||||
@@ -150,7 +150,6 @@ test('verification never elevates PR-controlled provisioning or cleanup on the h
|
||||
|
||||
test('the image and Grok downloads start before staging and join without weakening the npm checks', () => {
|
||||
const source = readFileSync(new URL('../verify-grok-npm-install.mjs', import.meta.url), 'utf8');
|
||||
// The optional Mac consumer owns separate scratch and no Linux prefetches.
|
||||
const linuxSource = source.slice(source.indexOf("assert.equal(process.platform, 'linux'"));
|
||||
const position = fragment => { const index = linuxSource.indexOf(fragment); assert.notEqual(index, -1, `verifier must contain ${fragment}`); return index; };
|
||||
// Exactly the pinned consumer image is pulled, for the platform the probes run on.
|
||||
@@ -349,8 +348,6 @@ test('the installed Codex probe exercises the public export and rejects incomple
|
||||
writeFileSync(legacyCommand, '#!/bin/sh\nexit 0\n', { mode: 0o755 });
|
||||
writeFileSync(index, `export const resolvePinnedCodexCommand = () => ${JSON.stringify(legacyCommand)};`);
|
||||
result = run(); assert.notEqual(result.status, 0); assert.match(result.stderr, /Selected Codex platform version must match the qualified pin/);
|
||||
// Native Codex uses protocol compatibility, not the ACPX version allowlist.
|
||||
// Keep exact official-npm host provenance and the selected resource boundary.
|
||||
mkdirSync(join(legacy, 'vendor'));
|
||||
const legacyNative = join(legacy, 'vendor', triple, 'codex', process.platform === 'win32' ? 'codex.exe' : 'codex');
|
||||
mkdirSync(join(legacy, 'vendor', triple, 'codex'), { recursive: true }); writeFileSync(legacyNative, nativeBytes, { mode: 0o755 });
|
||||
|
||||
@@ -845,9 +845,6 @@ const waitForFixture = async (predicate, label) => {
|
||||
assert.fail(label);
|
||||
};
|
||||
|
||||
// Execute the workflow's actual coordinator with two tiny local Node commands.
|
||||
// Both must cross the start barrier before either can finish; no pnpm, Cargo,
|
||||
// provider, registry, or Docker build runs in these fixtures.
|
||||
async function withNativePreparationFixture(mode, check) {
|
||||
const root = realpathSync(mkdtempSync(path.join(tmpdir(), 'native-preparation-')));
|
||||
let controller;
|
||||
@@ -1014,7 +1011,6 @@ test("ARM qualification binds separate product and reusable-workflow revisions e
|
||||
TOOLING_WORKFLOW_REF: "paperclipai/paperclip/.github/workflows/release-smoke.yml@refs/heads/qualification",
|
||||
TOOLING_WORKFLOW_PATH: ".github/workflows/release-smoke.yml",
|
||||
FIXTURE_PRODUCT_HEAD: productHead, FIXTURE_TOOLING_HEAD: toolingHead, FIXTURE_PREPARED: prepared,
|
||||
// Reusable calls retain the caller context; it must not select tooling.
|
||||
GITHUB_REPOSITORY: "different/caller", GITHUB_SHA: "d".repeat(40),
|
||||
} });
|
||||
assert.equal(result.status, expectedStatus, result.stderr);
|
||||
|
||||
@@ -50,9 +50,6 @@ export function resolveNativeRunnerDevBinary({
|
||||
const flat = path.join(packageRoot, "dist", "bin", executable);
|
||||
const targetStat = statSync(target, { throwIfNoEntry: false });
|
||||
const flatStat = statSync(flat, { throwIfNoEntry: false });
|
||||
// Release targets remain authoritative unless a newer source build matches
|
||||
// this host. In particular, a build-host flat artifact cannot mask a remotely
|
||||
// staged Mac target or force a local Cargo build.
|
||||
if (flatStat?.isFile() && (!targetStat?.isFile() || flatStat.mtimeMs > targetStat.mtimeMs)
|
||||
&& runnerBinaryTarget(readFileSync(flat)) === `${platform}-${architecture}`) {
|
||||
return { binary: flat, sourceBuild: true };
|
||||
|
||||
@@ -172,8 +172,6 @@ const env: NodeJS.ProcessEnv = {
|
||||
...process.env,
|
||||
PAPERCLIP_UI_DEV_MIDDLEWARE: explicitUiDevMiddleware ?? (serveBuiltUiForManagedRuntime ? "false" : "true"),
|
||||
};
|
||||
// An automatic source-build override must not hide subsequent Rust edits from
|
||||
// the watcher. Keep the operator's original choice separate from server env.
|
||||
const configuredRunnerBinary = env.PAPERCLIP_RUNNER_BINARY;
|
||||
|
||||
if (mode === "dev") {
|
||||
|
||||
@@ -11,8 +11,6 @@ export const GROK_PUBLIC_INSTALL_LIFECYCLE = [
|
||||
'npm', 'rebuild', '--offline', '--ignore-scripts=false', '--dangerously-allow-all-scripts',
|
||||
];
|
||||
|
||||
// setup-node provides its matching SDK alongside the executing Node binary.
|
||||
// Deferred lifecycle scripts may read that SDK, but may never download headers.
|
||||
export function macNodeHeadersDirectory(nodeExecutable, nodeVersion) {
|
||||
assert.match(nodeVersion, /^\d+\.\d+\.\d+$/, 'Executing Node must have an exact release version');
|
||||
const executable = realpathSync(nodeExecutable);
|
||||
@@ -213,7 +211,6 @@ export function installedCodexProbeSource(indexPath, consumerRoot, expectedVersi
|
||||
`;
|
||||
}
|
||||
|
||||
/** Installed bytes and consumer resolution must survive host protocol startup. */
|
||||
export function assertInstalledNativeConsumerContinuity(before, after) {
|
||||
for (const field of ['lockSha256', 'runnerSha256', 'codexCommandSha256', 'codexNativeExecutableSha256']) {
|
||||
if (!/^[a-f0-9]{64}$/.test(before[field]) || before[field] !== after[field]) {
|
||||
@@ -222,7 +219,6 @@ export function assertInstalledNativeConsumerContinuity(before, after) {
|
||||
}
|
||||
}
|
||||
|
||||
/** The installed consumer must select its packaged host daemon, never PATH. */
|
||||
export function installedDaemonProbeSource(outputRoot, consumerRoot, sourceRevision) {
|
||||
return `
|
||||
import assert from 'node:assert/strict';
|
||||
@@ -267,8 +263,6 @@ export function installedDaemonProbeSource(outputRoot, consumerRoot, sourceRevis
|
||||
`;
|
||||
}
|
||||
|
||||
// Hosted, credential-free Mac consumers defer only npm lifecycle execution.
|
||||
// Offline npm flags are not an OS network boundary; sandbox-exec supplies it.
|
||||
export function macPublicInstallLifecycleProfile(ownedRoot) {
|
||||
if (!ownedRoot.startsWith('/') || ownedRoot.includes('\0')) throw new Error('Mac lifecycle requires an absolute owned root');
|
||||
return `(version 1)\n(allow default)\n(deny network*)\n(deny file-write* (require-all (require-not (subpath ${JSON.stringify(ownedRoot)})) (require-not (literal "/dev/null"))))\n`;
|
||||
|
||||
@@ -32,8 +32,6 @@ export async function verifyPiPackagedDaemon(server, daemon, target, runnerBinar
|
||||
assert.equal(`sha256:${createHash('sha256').update(bytes).digest('hex')}`, artifact.sha256);
|
||||
assert.equal(runnerBinaryTarget(bytes), target);
|
||||
} else {
|
||||
// Existing host-only source packages have no multi-target release manifest.
|
||||
// They must still use their exact normal flat daemon, never an override.
|
||||
assert.equal(await realpath(daemon), expectedPath);
|
||||
assert.equal(runnerBinaryTarget(await readFile(expectedPath)), target);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
# Prepare the disposable GitHub ARM qualification host using stock distro files.
|
||||
# Extraction skips package maintainer scripts and their global policy changes.
|
||||
set -euo pipefail
|
||||
|
||||
if [[ "${GITHUB_ACTIONS:-}" != true || "${RUNNER_OS:-}" != Linux ]]; then
|
||||
@@ -87,7 +85,6 @@ test "$(od -An -tx1 -N4 "$stock_binary" | tr -d ' \n')" = 7f454c46
|
||||
readelf -h "$stock_binary" | tee "$prerequisite_report_dir/bwrap-elf-header.txt"
|
||||
grep -E 'Machine:.*AArch64' "$prerequisite_report_dir/bwrap-elf-header.txt"
|
||||
|
||||
# Reject every target symlink, including dangling links, before privileged copy.
|
||||
test ! -L /usr/bin/bwrap
|
||||
if test -e /usr/bin/bwrap; then
|
||||
test -f /usr/bin/bwrap
|
||||
|
||||
@@ -284,9 +284,6 @@ release_info ""
|
||||
release_info "==> Step 2/7: Building workspace artifacts..."
|
||||
cd "$REPO_ROOT"
|
||||
pnpm build
|
||||
# Hosted producers supply data-only, hash/source/architecture-bound daemon
|
||||
# receipts. Assemble after the build (which stages its host binary), before
|
||||
# public packages are packed. The server vendors this same compiled runner.
|
||||
if [ -n "${PAPERCLIP_RELEASE_RUNNER_ARTIFACTS_DIR:-}" ]; then
|
||||
pack_args=()
|
||||
if [ -n "${PAPERCLIP_RELEASE_PROVIDER_PACK_ARTIFACTS_DIR:-}" ]; then
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
#!/usr/bin/env node
|
||||
import { createHash } from "node:crypto";
|
||||
import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const scriptPath = fileURLToPath(import.meta.url);
|
||||
const defaultRoot = path.resolve(path.dirname(scriptPath), "..");
|
||||
const storyRoot = "ui/storybook/stories";
|
||||
const catalogPath = "maps/storybook-map/catalog.json";
|
||||
const indexPath = "maps/storybook-map/INDEX.md";
|
||||
const statuses = ["current", "proposed", "historical", "mixed", "unreviewed"];
|
||||
const implementations = ["production", "prototype", "mixed", "guide"];
|
||||
const boundaries = ["fixtures", "fixture-server", "static"];
|
||||
|
||||
function files(root, dir) {
|
||||
return readdirSync(path.join(root, dir), { withFileTypes: true }).flatMap(entry => {
|
||||
const name = `${dir}/${entry.name}`;
|
||||
return entry.isDirectory() ? files(root, name) : [name];
|
||||
}).sort();
|
||||
}
|
||||
|
||||
export function fingerprint(root, sources) {
|
||||
const hash = createHash("sha256");
|
||||
for (const file of [...new Set(sources)].sort()) {
|
||||
hash.update(file).update("\0").update(readFileSync(path.join(root, file))).update("\0");
|
||||
}
|
||||
return hash.digest("hex");
|
||||
}
|
||||
|
||||
export function capabilities(root) {
|
||||
const result = new Map();
|
||||
for (const file of files(root, "maps/feature-map").filter(file => file.endsWith(".md") && !file.endsWith("README.md"))) {
|
||||
const text = readFileSync(path.join(root, file), "utf8");
|
||||
const area = path.basename(file, ".md");
|
||||
const section = text.split(/^## Sub-features\s*$/m)[1]?.split(/^## /m)[0] ?? "";
|
||||
for (const match of section.matchAll(/^- `([^`]+)`:/gm)) result.set(`${area}/${match[1]}`, file);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function featureNavigation(root) {
|
||||
const navigation = new Map();
|
||||
let section;
|
||||
for (const line of readFileSync(path.join(root, "maps/feature-map/README.md"), "utf8").split("\n")) {
|
||||
if (line.startsWith("### ")) section = line.slice(4);
|
||||
const match = line.match(/^\| \[([^\]]+)\]\(\.\/([^.)]+)\.md\)/);
|
||||
if (match && section) navigation.set(match[2], `${section}/${match[1]}`);
|
||||
}
|
||||
return navigation;
|
||||
}
|
||||
|
||||
export function scanStories(root = defaultRoot) {
|
||||
const require = createRequire(path.join(root, "ui/package.json"));
|
||||
const { loadCsf } = require("storybook/internal/csf-tools");
|
||||
return files(root, storyRoot).filter(file => /\.stories\.(ts|tsx|mdx)$/.test(file)).map(file => {
|
||||
const csf = loadCsf(readFileSync(path.join(root, file), "utf8"), { fileName: file, makeTitle: title => title }).parse();
|
||||
return { file, title: csf.meta.title, metaId: csf.meta.id, ids: csf.stories.map(story => story.id), plays: csf.stories.filter(story => story.__stats?.play).length };
|
||||
});
|
||||
}
|
||||
|
||||
export function validateCatalog(root, catalog, stories) {
|
||||
const errors = [];
|
||||
const refs = capabilities(root);
|
||||
const navigation = featureNavigation(root);
|
||||
const byFile = new Map(stories.map(story => [story.file, story]));
|
||||
const seen = new Set();
|
||||
const local = file => typeof file === "string" && !path.isAbsolute(file) && !file.split("/").includes("..") && existsSync(path.join(root, file));
|
||||
if (catalog.version !== 1 || !Array.isArray(catalog.entries)) return ["Unsupported catalog version or missing entries"];
|
||||
function review(label, sources, hash) {
|
||||
if (!Array.isArray(sources) || !sources.length || sources.some(file => !local(file))) {
|
||||
errors.push(`${label}: missing/invalid review sources`);
|
||||
} else if (fingerprint(root, sources) !== hash) errors.push(`${label}: source changed; review before updating its fingerprint`);
|
||||
}
|
||||
review("Shared Storybook setup", catalog.sharedReview?.sources, catalog.sharedReview?.fingerprint);
|
||||
for (const entry of catalog.entries) {
|
||||
const label = entry.file;
|
||||
if (seen.has(label)) errors.push(`${label}: duplicate catalog entry`);
|
||||
seen.add(label);
|
||||
if (!byFile.has(label)) errors.push(`${label}: missing story file`);
|
||||
if (!statuses.includes(entry.status)) errors.push(`${label}: invalid product status`);
|
||||
if (!implementations.includes(entry.implementation)) errors.push(`${label}: invalid implementation`);
|
||||
if (!boundaries.includes(entry.boundary)) errors.push(`${label}: invalid data boundary`);
|
||||
if (!entry.purpose?.trim() || !entry.notes?.trim()) errors.push(`${label}: purpose and review notes are required`);
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(entry.reviewedAt ?? "")) errors.push(`${label}: missing review date`);
|
||||
if (!Array.isArray(entry.capabilities)) { errors.push(`${label}: missing capabilities`); continue; }
|
||||
for (const ref of entry.capabilities) if (!refs.has(ref)) errors.push(`${label}: unknown feature capability ${ref}`);
|
||||
if (!entry.capabilities.length && !entry.exception?.trim()) errors.push(`${label}: unmapped story needs an explicit reason`);
|
||||
if (entry.capabilities.length !== new Set(entry.capabilities).size) errors.push(`${label}: duplicate capability references`);
|
||||
const area = entry.capabilities[0]?.split("/")[0] ?? (entry.implementation === "guide" ? "overview" : "foundations");
|
||||
const prefix = navigation.get(area) ?? ({ overview: "Overview", foundations: "Foundations" })[area];
|
||||
const story = byFile.get(label);
|
||||
if (!label.startsWith(`${storyRoot}/${area}/`)) errors.push(`${label}: story must live in its primary feature folder ${area}`);
|
||||
if (!prefix || !entry.sidebarTitle?.startsWith(`${prefix}/`) || story?.title !== entry.sidebarTitle) errors.push(`${label}: sidebar title must match its feature-map home`);
|
||||
if (!entry.stableId || story?.metaId !== entry.stableId) errors.push(`${label}: explicit meta ID must preserve the catalog stableId`);
|
||||
if (entry.replacement && !byFile.has(entry.replacement)) errors.push(`${label}: replacement story is missing`);
|
||||
if (!entry.sources?.includes(label)) errors.push(`${label}: review must include its story source`);
|
||||
review(label, entry.sources, entry.fingerprint);
|
||||
}
|
||||
for (const story of stories) if (!seen.has(story.file)) errors.push(`${story.file}: story missing from catalog`);
|
||||
const ids = stories.flatMap(story => story.ids);
|
||||
if (ids.length !== new Set(ids).size) errors.push("Duplicate story IDs");
|
||||
return errors;
|
||||
}
|
||||
|
||||
function cell(value) { return String(value).replaceAll("|", "\\|").replaceAll("\n", " "); }
|
||||
|
||||
export function renderIndex(catalog, stories, refs) {
|
||||
const byFile = new Map(stories.map(story => [story.file, story]));
|
||||
const groups = new Map();
|
||||
for (const entry of catalog.entries) {
|
||||
const area = entry.capabilities[0]?.split("/")[0] ?? "foundations-and-guides";
|
||||
if (!groups.has(area)) groups.set(area, []);
|
||||
groups.get(area).push(entry);
|
||||
}
|
||||
const lines = ["# Storybook by feature", "", "<!-- Generated by node scripts/storybook-catalog.mjs --write. Edit catalog.json. -->", "",
|
||||
"[Contract and maintenance](README.md) · [Review findings](REVIEW.md)", "",
|
||||
`${stories.length} story files; ${stories.reduce((n, story) => n + story.ids.length, 0)} indexed stories. Counts describe inventory, not passed tests or complete feature coverage.`, "",
|
||||
"Status describes product alignment; implementation describes the rendered subject; boundary describes simulated data. Sources and limitations are in catalog.json. Related capabilities cross-list one canonical story file.", ""];
|
||||
for (const [area, entries] of [...groups].sort(([a], [b]) => a.localeCompare(b))) {
|
||||
lines.push(`## ${area}`, "", ...(area !== "foundations-and-guides" ? [`[Feature recipe](../feature-map/${area}.md)`, ""] : []),
|
||||
"| Story suite | Capabilities | Product / implementation / data | Stories / play functions | Review note |",
|
||||
"| --- | --- | --- | --- | --- |");
|
||||
for (const entry of entries.sort((a, b) => a.file.localeCompare(b.file))) {
|
||||
const story = byFile.get(entry.file);
|
||||
lines.push(`| [${cell(story.title)}](../../${entry.file}) | ${entry.capabilities.map(ref => `\`${ref}\``).join(", ") || cell(entry.exception)} | ${entry.status} / ${entry.implementation} / ${entry.boundary} | ${story.ids.length} / ${story.plays} | ${cell(entry.notes)} |`);
|
||||
}
|
||||
lines.push("");
|
||||
}
|
||||
lines.push("## Capability lookup", "", "References are navigation links, not proof of parity. An absent link means no cataloged story, not a missing product feature. CLI/operator capabilities may need no Storybook example.", "");
|
||||
for (const ref of refs.keys()) {
|
||||
const entries = catalog.entries.filter(entry => entry.capabilities.includes(ref));
|
||||
lines.push(`- \`${ref}\`: ${entries.length ? entries.map(entry => `[${cell(byFile.get(entry.file).title)}](../../${entry.file}) (${entry.status})`).join(", ") : "No cataloged story."}`);
|
||||
}
|
||||
return lines.join("\n") + "\n";
|
||||
}
|
||||
|
||||
if (process.argv[1] && path.resolve(process.argv[1]) === scriptPath) {
|
||||
const args = process.argv.slice(2);
|
||||
if (args.some(arg => arg !== "--write")) throw new Error("Usage: node scripts/storybook-catalog.mjs [--write]");
|
||||
const catalog = JSON.parse(readFileSync(path.join(defaultRoot, catalogPath), "utf8"));
|
||||
const stories = scanStories();
|
||||
const errors = validateCatalog(defaultRoot, catalog, stories);
|
||||
if (!errors.length) {
|
||||
const generated = renderIndex(catalog, stories, capabilities(defaultRoot));
|
||||
if (args.includes("--write")) writeFileSync(path.join(defaultRoot, indexPath), generated);
|
||||
else if (!existsSync(path.join(defaultRoot, indexPath)) || readFileSync(path.join(defaultRoot, indexPath), "utf8") !== generated) errors.push("Generated INDEX.md is stale; run with --write after reviewing catalog changes");
|
||||
}
|
||||
if (errors.length) { errors.forEach(error => console.error(error)); process.exitCode = 1; }
|
||||
else console.log(`Storybook catalog aligned: ${stories.length} files, ${stories.reduce((n, story) => n + story.ids.length, 0)} stories. Structure and review fingerprints only; no runtime proof.`);
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, symlinkSync } from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { test } from "node:test";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { capabilities, fingerprint, renderIndex, scanStories, validateCatalog } from "./storybook-catalog.mjs";
|
||||
|
||||
function fixture(t) {
|
||||
const root = mkdtempSync(path.join(os.tmpdir(), "storybook-catalog-"));
|
||||
t.after(() => rmSync(root, { recursive: true, force: true }));
|
||||
function write(file, text) {
|
||||
mkdirSync(path.dirname(path.join(root, file)), { recursive: true });
|
||||
writeFileSync(path.join(root, file), text);
|
||||
}
|
||||
write("maps/feature-map/README.md", "# Features\n\n### Tasks and conversations\n\n| [Tasks](./tasks.md) | Tasks |\n");
|
||||
write("maps/feature-map/tasks.md", "# Tasks\n\n## Sub-features\n\n- `create`: create\n- `discussion`: discuss\n\n## How to get to it\n");
|
||||
write("ui/storybook/.storybook/preview.tsx", "// shared fixtures\n");
|
||||
const file = "ui/storybook/stories/tasks/task.stories.tsx";
|
||||
write(file, "// a story\n");
|
||||
write("ui/src/Task.tsx", "// production subject\n");
|
||||
const sharedSources = ["ui/storybook/.storybook/preview.tsx"];
|
||||
const sources = [file, "ui/src/Task.tsx"];
|
||||
const catalog = {
|
||||
version: 1, sharedReview: { sources: sharedSources, fingerprint: fingerprint(root, sharedSources) },
|
||||
entries: [{ file, stableId: "tasks-create", sidebarTitle: "Tasks and conversations/Tasks/Create", capabilities: ["tasks/create", "tasks/discussion"], purpose: "Create a task", status: "current",
|
||||
implementation: "production", boundary: "fixtures", notes: "Production subject; callbacks simulated.",
|
||||
reviewedAt: "2026-10-12", sources, fingerprint: fingerprint(root, sources) }],
|
||||
};
|
||||
const stories = [{ file, title: "Tasks and conversations/Tasks/Create", metaId: "tasks-create", ids: ["tasks-create--empty"], plays: 0 }];
|
||||
return { root, write, file, catalog, stories };
|
||||
}
|
||||
|
||||
test("complete inventory accepts production subjects with simulated data", t => {
|
||||
const f = fixture(t);
|
||||
assert.deepEqual(validateCatalog(f.root, f.catalog, f.stories), []);
|
||||
});
|
||||
|
||||
test("adding, removing or duplicating story files requires reconciliation", t => {
|
||||
const f = fixture(t);
|
||||
assert.match(validateCatalog(f.root, { ...f.catalog, entries: [] }, f.stories).join("\n"), /missing from catalog/);
|
||||
assert.match(validateCatalog(f.root, f.catalog, []).join("\n"), /missing story file/);
|
||||
f.catalog.entries.push(f.catalog.entries[0]);
|
||||
assert.match(validateCatalog(f.root, f.catalog, f.stories).join("\n"), /duplicate catalog entry/);
|
||||
});
|
||||
|
||||
test("capability renames and unjustified unmapped stories fail", t => {
|
||||
const f = fixture(t);
|
||||
f.catalog.entries[0].capabilities = ["tasks/old-name"];
|
||||
assert.match(validateCatalog(f.root, f.catalog, f.stories).join("\n"), /unknown feature capability/);
|
||||
f.catalog.entries[0].capabilities = [];
|
||||
assert.match(validateCatalog(f.root, f.catalog, f.stories).join("\n"), /explicit reason/);
|
||||
f.catalog.entries[0].exception = "Shared foundation";
|
||||
const file = "ui/storybook/stories/foundations/task.stories.tsx";
|
||||
f.write(file, "export default {};");
|
||||
const entry = f.catalog.entries[0];
|
||||
entry.file = file;
|
||||
entry.sources = [file, "ui/src/Task.tsx"];
|
||||
entry.fingerprint = fingerprint(f.root, entry.sources);
|
||||
entry.sidebarTitle = "Foundations/Task";
|
||||
f.stories[0].file = file;
|
||||
f.stories[0].title = entry.sidebarTitle;
|
||||
assert.deepEqual(validateCatalog(f.root, f.catalog, f.stories), []);
|
||||
});
|
||||
|
||||
test("feature organization rejects wrong folders, sidebar homes and unstable meta IDs", t => {
|
||||
const f = fixture(t);
|
||||
f.catalog.entries[0].capabilities = ["tasks/discussion"];
|
||||
assert.deepEqual(validateCatalog(f.root, f.catalog, f.stories), []);
|
||||
f.stories[0].metaId = "renamed-task";
|
||||
assert.match(validateCatalog(f.root, f.catalog, f.stories).join("\n"), /explicit meta ID/);
|
||||
f.stories[0].metaId = "tasks-create";
|
||||
f.stories[0].title = "Unrelated/Task";
|
||||
assert.match(validateCatalog(f.root, f.catalog, f.stories).join("\n"), /sidebar title/);
|
||||
f.stories[0].title = f.catalog.entries[0].sidebarTitle;
|
||||
const originalFile = f.catalog.entries[0].file;
|
||||
f.catalog.entries[0].file = "ui/storybook/stories/foundations/task.stories.tsx";
|
||||
assert.match(validateCatalog(f.root, f.catalog, f.stories).join("\n"), /primary feature folder tasks/);
|
||||
f.catalog.entries[0].file = originalFile;
|
||||
f.write("maps/feature-map/README.md", "### Conversations\n| [Tasks](./tasks.md) | Tasks |\n");
|
||||
assert.match(validateCatalog(f.root, f.catalog, f.stories).join("\n"), /sidebar title/);
|
||||
});
|
||||
|
||||
test("production and shared fixture drift invalidate review fingerprints", t => {
|
||||
const f = fixture(t);
|
||||
f.write("ui/src/Task.tsx", "// changed contract\n");
|
||||
f.write("ui/storybook/.storybook/preview.tsx", "// changed shared mocks\n");
|
||||
const errors = validateCatalog(f.root, f.catalog, f.stories);
|
||||
assert.equal(errors.filter(error => error.includes("source changed")).length, 2);
|
||||
assert.equal(f.catalog.entries[0].fingerprint.length, 64);
|
||||
});
|
||||
|
||||
test("missing and escaping source references cannot certify a review", t => {
|
||||
const f = fixture(t);
|
||||
f.catalog.entries[0].sources.push("../outside.ts");
|
||||
assert.match(validateCatalog(f.root, f.catalog, f.stories).join("\n"), /invalid review sources/);
|
||||
});
|
||||
|
||||
test("cross-capability lookup retains one canonical file and honest status", t => {
|
||||
const f = fixture(t);
|
||||
f.catalog.entries[0].status = "historical";
|
||||
const index = renderIndex(f.catalog, f.stories, capabilities(f.root));
|
||||
assert.match(index, /tasks\/create.*\(historical\)/);
|
||||
assert.match(index, /tasks\/discussion.*\(historical\)/);
|
||||
assert.match(index, /1 story files; 1 indexed stories/);
|
||||
assert.match(index, /historical \/ production \/ fixtures/);
|
||||
});
|
||||
|
||||
test("duplicate story IDs cannot silently collapse distinct examples", t => {
|
||||
const f = fixture(t);
|
||||
f.stories[0].ids.push(f.stories[0].ids[0]);
|
||||
assert.match(validateCatalog(f.root, f.catalog, f.stories).join("\n"), /Duplicate story IDs/);
|
||||
});
|
||||
|
||||
test("Storybook parser discovers nested files, filters helpers, preserves explicit IDs and counts play functions", t => {
|
||||
const f = fixture(t);
|
||||
f.write("ui/package.json", '{"type":"module"}');
|
||||
symlinkSync(fileURLToPath(new URL("../ui/node_modules", import.meta.url)), path.join(f.root, "ui/node_modules"), "junction");
|
||||
f.write(f.file, `
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
const meta = { title: "Tasks/Create", id: "stable-task", excludeStories: ["empty"] } satisfies Meta;
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
export const empty = { value: "fixture" };
|
||||
export const Empty: Story = {};
|
||||
export const Filled: Story = { play: async () => {} };
|
||||
`);
|
||||
f.write("ui/storybook/stories/nested/other.stories.ts", `
|
||||
export default { title: "Other/Subject", includeStories: ["Included"] };
|
||||
export const helper = {};
|
||||
export const Included = {};
|
||||
`);
|
||||
f.write("ui/storybook/stories/nested/fixture.ts", "export const fixture = {};");
|
||||
const stories = scanStories(f.root);
|
||||
assert.equal(stories.length, 2);
|
||||
assert.deepEqual(stories.find(story => story.file === f.file), {
|
||||
file: f.file, title: "Tasks/Create", metaId: "stable-task", ids: ["stable-task--empty", "stable-task--filled"], plays: 1,
|
||||
});
|
||||
assert.deepEqual(stories.find(story => story.file.endsWith("other.stories.ts")), {
|
||||
file: "ui/storybook/stories/nested/other.stories.ts", title: "Other/Subject", metaId: undefined, ids: ["other-subject--included"], plays: 0,
|
||||
});
|
||||
});
|
||||
@@ -36,8 +36,6 @@ async function verifyMacConsumer(packageDirectory, sourceRevision, receiptPath)
|
||||
for (const directory of [consumer, home, tools, join(root, 'tmp'), join(root, 'cache')]) mkdirSync(directory);
|
||||
const npmCli = realpathSync(execFileSync('/usr/bin/which', ['npm'], { encoding: 'utf8' }).trim());
|
||||
symlinkSync(process.execPath, join(tools, 'node'));
|
||||
// npm subprocesses get a Node-only owned tool directory; no ambient harness
|
||||
// command can hide missing installed dependencies.
|
||||
const env = { PATH: `${tools}:/usr/bin:/bin:/usr/sbin:/sbin`, HOME: home, TMPDIR: join(root, 'tmp'),
|
||||
npm_config_cache: join(root, 'cache'), npm_config_audit: 'false', npm_config_fund: 'false' };
|
||||
const run = (command, args, cwd = consumer, runEnv = env) => execFileSync(command, args, { cwd, env: runEnv,
|
||||
@@ -148,13 +146,8 @@ async function verifyLinuxArmConsumer(packageDirectory, sourceRevision, receiptP
|
||||
const daemon = probe('daemon-probe.mjs', installedDaemonProbeSource(installed, '/consumer', sourceRevision));
|
||||
assert.equal(daemon.target, 'linux-arm64');
|
||||
const codex = probe('codex-probe.mjs', installedCodexProbeSource(installed + '/index.js', '/consumer', undefined, { nativeCodex: true }));
|
||||
// Reuse the existing zero-turn hook as read-only verifier source. All
|
||||
// production imports still resolve from the installed consumer graph.
|
||||
cpSync(join(repo, 'tests/runner-e2e/installed-cli.ts'), join(assets, 'installed-cli.ts'));
|
||||
writeFileSync(join(assets, 'types.js'), stripTypeScriptTypes(readFileSync(join(repo, 'tests/runner-e2e/types.ts'), 'utf8')));
|
||||
// The restricted install container deliberately forbids nested namespaces.
|
||||
// Keep its lifecycle/no-network proof, then test the exact same bind-mounted
|
||||
// graph on the native host using Codex's normal read-only namespace policy.
|
||||
const home = join(root, 'host-home'), hostTmp = join(root, 'host-tmp');
|
||||
for (const directory of [home, hostTmp]) mkdirSync(directory, { mode: 0o700 });
|
||||
const hostEnv = { PATH: `${dirname(process.execPath)}:/usr/bin:/bin:/usr/sbin:/sbin`, HOME: home, TMPDIR: hostTmp, LC_ALL: 'C', TZ: 'UTC' };
|
||||
@@ -332,9 +325,6 @@ try {
|
||||
timing.stage_pack_ms = Math.round(performance.now() - packStarted);
|
||||
Object.assign(timing, packTimings(packs, concurrency));
|
||||
|
||||
// Preserve the exact Linux-produced public tarballs for independent Mac
|
||||
// consumers even if a later Linux runtime probe fails. This is a packaging
|
||||
// receipt, never an installation or provider-admission pass.
|
||||
if (process.env.PAPERCLIP_RUNNER_PUBLIC_PACKS_DIR) {
|
||||
const output = resolve(process.env.PAPERCLIP_RUNNER_PUBLIC_PACKS_DIR);
|
||||
assert.equal(JSON.parse(readFileSync(join(repo, 'server/dist/build-info.json'))).commit, sourceRevision);
|
||||
|
||||
@@ -261,6 +261,17 @@ function makeAgent(adapterType: string) {
|
||||
};
|
||||
}
|
||||
|
||||
function skillSnapshot(adapterType = "claude_local", mode = "ephemeral") {
|
||||
return {
|
||||
adapterType: adapterType,
|
||||
supported: true,
|
||||
mode: mode,
|
||||
desiredSkills: ["paperclipai/paperclip/paperclip"],
|
||||
entries: [],
|
||||
warnings: [],
|
||||
};
|
||||
}
|
||||
|
||||
describe("agent skill routes", () => {
|
||||
beforeEach(async () => {
|
||||
runnerTarget.platform = "linux";
|
||||
@@ -320,22 +331,8 @@ describe("agent skill routes", () => {
|
||||
unresolved: [],
|
||||
}),
|
||||
);
|
||||
mockAdapter.listSkills.mockResolvedValue({
|
||||
adapterType: "claude_local",
|
||||
supported: true,
|
||||
mode: "ephemeral",
|
||||
desiredSkills: ["paperclipai/paperclip/paperclip"],
|
||||
entries: [],
|
||||
warnings: [],
|
||||
});
|
||||
mockAdapter.syncSkills.mockResolvedValue({
|
||||
adapterType: "claude_local",
|
||||
supported: true,
|
||||
mode: "ephemeral",
|
||||
desiredSkills: ["paperclipai/paperclip/paperclip"],
|
||||
entries: [],
|
||||
warnings: [],
|
||||
});
|
||||
mockAdapter.listSkills.mockResolvedValue(skillSnapshot());
|
||||
mockAdapter.syncSkills.mockResolvedValue(skillSnapshot());
|
||||
mockAgentService.update.mockImplementation(async (_id: string, patch: Record<string, unknown>) => {
|
||||
const previousAgent = persistedAgent ?? makeAgent("claude_local");
|
||||
persistedAgent = {
|
||||
@@ -478,61 +475,11 @@ describe("agent skill routes", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("threads a non-undefined actor secret context into resolveAdapterConfigForRuntime on both skills routes (audit fidelity, skipUserSecrets preserved)", async () => {
|
||||
const expectedContext = {
|
||||
consumerType: "agent",
|
||||
consumerId: "11111111-1111-4111-8111-111111111111",
|
||||
actorType: "user",
|
||||
actorId: "local-board",
|
||||
actorSource: "local_implicit",
|
||||
responsibleUserId: "local-board",
|
||||
};
|
||||
|
||||
// GET /agents/:id/skills
|
||||
mockAgentService.getById.mockResolvedValue(makeAgent("claude_local"));
|
||||
const listRes = await requestApp(
|
||||
await createApp(),
|
||||
(baseUrl) => request(baseUrl)
|
||||
.get("/api/agents/11111111-1111-4111-8111-111111111111/skills?companyId=company-1"),
|
||||
);
|
||||
expect(listRes.status, JSON.stringify(listRes.body)).toBe(200);
|
||||
const listCall = mockSecretService.resolveAdapterConfigForRuntime.mock.calls.at(-1);
|
||||
expect(listCall?.[2]).toBeDefined();
|
||||
expect(listCall?.[2]).toEqual(expectedContext);
|
||||
expect(listCall?.[3]).toEqual({ adapterType: "claude_local", skipUserSecrets: true });
|
||||
|
||||
// POST /agents/:id/skills/sync
|
||||
mockAdapter.syncSkills.mockResolvedValue({
|
||||
adapterType: "claude_local",
|
||||
supported: true,
|
||||
mode: "ephemeral",
|
||||
desiredSkills: ["paperclipai/paperclip/paperclip"],
|
||||
entries: [],
|
||||
warnings: [],
|
||||
});
|
||||
const syncRes = await requestApp(
|
||||
await createApp(),
|
||||
(baseUrl) => request(baseUrl)
|
||||
.post("/api/agents/11111111-1111-4111-8111-111111111111/skills/sync?companyId=company-1")
|
||||
.send({ desiredSkills: ["paperclip"], mode: "replace" }),
|
||||
);
|
||||
expect(syncRes.status, JSON.stringify(syncRes.body)).toBe(200);
|
||||
const syncCall = mockSecretService.resolveAdapterConfigForRuntime.mock.calls.at(-1);
|
||||
expect(syncCall?.[2]).toBeDefined();
|
||||
expect(syncCall?.[2]).toEqual(expectedContext);
|
||||
expect(syncCall?.[3]).toEqual({ adapterType: "claude_local", skipUserSecrets: true });
|
||||
});
|
||||
|
||||
it("skips runtime materialization when listing Codex skills", async () => {
|
||||
mockAgentService.getById.mockResolvedValue(makeAgent("codex_local"));
|
||||
mockAdapter.listSkills.mockResolvedValue({
|
||||
adapterType: "codex_local",
|
||||
supported: true,
|
||||
mode: "ephemeral",
|
||||
desiredSkills: ["paperclipai/paperclip/paperclip"],
|
||||
entries: [],
|
||||
warnings: [],
|
||||
});
|
||||
mockAdapter.listSkills.mockResolvedValue(skillSnapshot("codex_local"));
|
||||
|
||||
const res = await requestApp(
|
||||
await createApp(),
|
||||
@@ -555,14 +502,7 @@ describe("agent skill routes", () => {
|
||||
mockSecretService.resolveAdapterConfigForRuntime.mockResolvedValueOnce({
|
||||
config: { agent: "claude" },
|
||||
});
|
||||
mockAdapter.listSkills.mockResolvedValue({
|
||||
adapterType: "acpx_local",
|
||||
supported: true,
|
||||
mode: "ephemeral",
|
||||
desiredSkills: ["paperclipai/paperclip/paperclip"],
|
||||
entries: [],
|
||||
warnings: [],
|
||||
});
|
||||
mockAdapter.listSkills.mockResolvedValue(skillSnapshot("acpx_local"));
|
||||
|
||||
const res = await requestApp(
|
||||
await createApp(),
|
||||
@@ -603,14 +543,7 @@ describe("agent skill routes", () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
mockAdapter.syncSkills.mockResolvedValue({
|
||||
adapterType: "acpx_local",
|
||||
supported: true,
|
||||
mode: "ephemeral",
|
||||
desiredSkills: ["paperclipai/paperclip/paperclip"],
|
||||
entries: [],
|
||||
warnings: [],
|
||||
});
|
||||
mockAdapter.syncSkills.mockResolvedValue(skillSnapshot("acpx_local"));
|
||||
|
||||
const res = await requestApp(await createApp(), (baseUrl) => request(baseUrl)
|
||||
.post("/api/agents/11111111-1111-4111-8111-111111111111/skills/sync?companyId=company-1")
|
||||
@@ -831,14 +764,7 @@ describe("agent skill routes", () => {
|
||||
|
||||
it("skips runtime materialization when listing persistent skill adapters", async () => {
|
||||
mockAgentService.getById.mockResolvedValue(makeAgent("cursor"));
|
||||
mockAdapter.listSkills.mockResolvedValue({
|
||||
adapterType: "cursor",
|
||||
supported: true,
|
||||
mode: "persistent",
|
||||
desiredSkills: ["paperclipai/paperclip/paperclip"],
|
||||
entries: [],
|
||||
warnings: [],
|
||||
});
|
||||
mockAdapter.listSkills.mockResolvedValue(skillSnapshot("cursor", "persistent"));
|
||||
|
||||
const res = await requestApp(
|
||||
await createApp(),
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import * as activityLogModule from "../services/activity-log.js";
|
||||
import { chatSlackManagerGrants, chatSlackRegistrations, toolOauthStates } from "@paperclipai/db";
|
||||
import type { PaperclipCloudConnector, SealedConnectorCredentials } from "../services/paperclip-cloud-connector.js";
|
||||
import { buildSlackAppManifest, SLACK_MANAGER_SCOPES } from "@paperclipai/shared";
|
||||
import { registerChatActionWork, dueChatAction } from "../services/chat-action-work.js";
|
||||
import { createDeliveryWorkCoordinator } from "../services/delivery-work-coordinator.js";
|
||||
import { registerChatDeliveryWork } from "../services/chat-delivery-work.js";
|
||||
@@ -7,14 +11,11 @@ import { aiConnectionService } from "../services/ai-connections.js";
|
||||
import { fastResponseService } from "../services/fast-responses.js";
|
||||
import { fastResponseReceipt } from "../services/fast-response-provider.js";
|
||||
import { fastResponseRequests } from "@paperclipai/db";
|
||||
import { chatSlackRegistrations, toolOauthStates } from "@paperclipai/db";
|
||||
import { buildSlackAppManifest } from "@paperclipai/shared";
|
||||
import { toolAccessService } from "../services/tool-access.js";
|
||||
import { toolActionRequests, toolInvocations } from "@paperclipai/db";
|
||||
import { GitHubPublicationLeaseLost, withGitHubPublicationLease } from "../services/chat-github-publication-lease.js";
|
||||
import { secretService } from "../services/secrets.js";
|
||||
import { githubChatWizardService } from "../services/chat-github-wizard.js";
|
||||
import type { PaperclipCloudConnector } from "../services/paperclip-cloud-connector.js";
|
||||
import { dispatchGitHubBotCloudEvent } from "../services/chat-github-cloud-ingress.js";
|
||||
import { githubChatManagementService } from "../services/chat-github-management.js";
|
||||
import { githubChatReviewService } from "../services/chat-github-reviews.js";
|
||||
@@ -6262,7 +6263,9 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => {
|
||||
const token = `xoxb-bot-token-canary-${id}`;
|
||||
const scopes = buildSlackAppManifest({ app: appDetails, agentName: "Maya", webhookUrl: "https://paperclip.example/hook" }).oauth_config.scopes.bot;
|
||||
const state = {
|
||||
failCreate: false, failValidate: false, failInventory: false, failAuth: false, failExchange: false,
|
||||
managedAccessToken: token, rejectOldBotToken: false,
|
||||
userListCursor: null as string | null, userListNextCursor: "",
|
||||
managedBotAppId: appId, grantedScopes: scopes, managedError: null as string | null, failCreate: false, failValidate: false, failInventory: false, failAuth: false, failExchange: false,
|
||||
failManifestUpdate: false, timeoutManifestUpdate: false,
|
||||
beforeManifestUpdate: null as null | (() => Promise<void>),
|
||||
failIcon: false, timeoutIcon: false, failUser: false, failWelcome: false, timeoutWelcome: false,
|
||||
@@ -6270,20 +6273,23 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => {
|
||||
user: { id: installerId, team_id: "TAUTO", name: "installer", real_name: "Installing Person", is_bot: false, deleted: false },
|
||||
beforeIcon: null as null | (() => Promise<void>),
|
||||
beforeAuth: null as null | (() => Promise<void>),
|
||||
beforeExchange: null as null | (() => Promise<void>),
|
||||
oauth: { ok: true, app_id: appId, token_type: "bot", access_token: token, team: { id: "TAUTO" }, bot_user_id: botId, authed_user: { id: installerId }, scope: scopes.join(",") },
|
||||
};
|
||||
const fallback = fakeSlackFetch(botId);
|
||||
const provider = vi.fn(async (input: string | URL | Request, init?: RequestInit) => {
|
||||
const url = String(input);
|
||||
if (url.endsWith("bots.info")) return Response.json({ ok: true, bot: { id: "BMANAGED", app_id: state.managedBotAppId, user_id: botId, deleted: false } });
|
||||
if (url.endsWith("apps.managedInstall")) return Response.json(state.managedError ? { ok: false, error: state.managedError } : { ok: true, app_id: appId, team_id: "TAUTO", api_access_tokens: { bot_access_token: state.managedAccessToken } });
|
||||
if (url.endsWith("apps.manifest.validate")) return Response.json(state.failValidate ? { ok: false, error: "invalid_auth", detail: configToken } : { ok: true });
|
||||
if (url.endsWith("apps.manifest.create")) {
|
||||
if (state.failCreate) throw new Error(`Timeout ${configToken} ${clientSecret}`);
|
||||
return Response.json({ ok: true, app_id: appId, credentials: { client_id: "123.456", client_secret: clientSecret, signing_secret: signingSecret } });
|
||||
return Response.json({ ok: true, app_id: appId, team_id: "TAUTO", credentials: { client_id: "123.456", client_secret: clientSecret, signing_secret: signingSecret } });
|
||||
}
|
||||
if (url.endsWith("apps.manifest.update")) {
|
||||
await state.beforeManifestUpdate?.();
|
||||
if (state.timeoutManifestUpdate) throw new Error(`Manifest timeout ${configToken} ${clientSecret}`);
|
||||
return Response.json(state.failManifestUpdate ? { ok: false, error: "invalid_auth", detail: configToken } : { ok: true, app_id: appId });
|
||||
return Response.json(state.failManifestUpdate ? { ok: false, error: "invalid_auth", detail: configToken } : { ok: true, app_id: appId, permissions_updated: true });
|
||||
}
|
||||
if (url.endsWith("apps.icon.set")) {
|
||||
await state.beforeIcon?.();
|
||||
@@ -6291,13 +6297,19 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => {
|
||||
return Response.json(state.failIcon ? { ok: false, error: "missing_scope", detail: `${configToken} ${signingSecret}` } : { ok: true });
|
||||
}
|
||||
if (url.endsWith("oauth.v2.access")) {
|
||||
await state.beforeExchange?.();
|
||||
if (state.failExchange) throw new Error(`Timeout ${clientSecret} ${token}`);
|
||||
return Response.json(state.oauth);
|
||||
}
|
||||
if (url.endsWith("auth.test")) {
|
||||
await state.beforeAuth?.();
|
||||
if (state.rejectOldBotToken && new Headers(init?.headers).get("authorization") !== `Bearer ${state.managedAccessToken}`) return Response.json({ ok: false, error: "token_revoked" });
|
||||
if (state.failAuth) throw new Error(`Temporary failure ${token}`);
|
||||
return Response.json({ ok: true, team_id: "TAUTO", user_id: botId, user: `maya-${id}`, team: "Test workspace" }, { headers: { "x-oauth-scopes": scopes.join(",") } });
|
||||
return Response.json({ ok: true, team_id: "TAUTO", user_id: botId, bot_id: "BMANAGED", user: `maya-${id}`, team: "Test workspace" }, { headers: { "x-oauth-scopes": state.grantedScopes.join(",") } });
|
||||
}
|
||||
if (url.endsWith("users.list")) {
|
||||
state.userListCursor = new URLSearchParams(String(init?.body)).get("cursor");
|
||||
return Response.json({ ok: true, members: [state.user], response_metadata: { next_cursor: state.userListNextCursor } });
|
||||
}
|
||||
if (url.endsWith("users.info")) {
|
||||
await state.beforeUser?.();
|
||||
@@ -6323,6 +6335,408 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => {
|
||||
const install = async () => new URL((await service.slackRegistration.install(endpoint.id, actor)).authorizationUrl).searchParams.get("state")!;
|
||||
return { ...company, endpoint, service, sdk, app, state, provider, create, install, createInput, token, appId, botId, installerId, avatarBytes, renderAvatar };
|
||||
}
|
||||
async function managedFixture() {
|
||||
const broker = {
|
||||
getCapabilities: vi.fn(async () => ["slack.manager"]),
|
||||
getSlackManagerAppId: vi.fn(async () => "AMANAGER"),
|
||||
startAuthorization: vi.fn(async (_input: { returnState: string }) => ({ authorizationUrl: "https://slack.com/oauth/v2/authorize?state=broker", expiresAt: new Date(Date.now() + 600_000).toISOString() })),
|
||||
claim: vi.fn(), refresh: vi.fn(), revoke: vi.fn(async () => undefined),
|
||||
};
|
||||
const f = await fixture({ slackManaged: { enabled: true, broker: broker as unknown as PaperclipCloudConnector } });
|
||||
const credentials: SealedConnectorCredentials = { v: 1, provider: "slack", profile: "slack.manager", instanceId: "test", environment: "staging", subject: actor.userId, companyId: f.companyId,
|
||||
accessToken: "xoxe.xoxp-manager-access-canary", refreshToken: "xoxe-manager-refresh-canary", tokenType: "user", accessTokenExpiresAt: new Date(Date.now() + 43_200_000).toISOString(), refreshTokenExpiresAt: null,
|
||||
scopes: [...SLACK_MANAGER_SCOPES], slackManager: { appId: "AMANAGER", workspaceId: "TAUTO", workspaceName: "Test workspace", userId: f.installerId } };
|
||||
broker.claim.mockResolvedValue(credentials);
|
||||
broker.refresh.mockResolvedValue({ ...credentials, accessToken: "xoxe.rotated-canary", refreshToken: "xoxe.rotated-refresh-canary" });
|
||||
await f.service.update(f.endpoint.id, { slackSetupMethod: "managed" }, actor.userId);
|
||||
const grants = f.service.slackRegistration.managerGrants;
|
||||
const authorize = async () => { await grants.authorize(f.endpoint.id, actor); return broker.startAuthorization.mock.calls.at(-1)![0].returnState; };
|
||||
const authorizeState = await authorize();
|
||||
await grants.complete(authorizeState, "claim-canary", null, actor);
|
||||
const choices = await grants.choices(f.companyId, actor);
|
||||
expect(choices.workspaces).toHaveLength(1);
|
||||
const grantId = choices.workspaces[0]!.grantId;
|
||||
const input = { requestId: randomUUID(), grantId };
|
||||
const provision = () => f.service.slackRegistration.managed.provision(f.endpoint.id, actor, input);
|
||||
return { ...f, broker, credentials, grants, authorize, authorizeState, grantId, input, provision };
|
||||
}
|
||||
describe("Slack test messages", () => {
|
||||
const posts = (f: Awaited<ReturnType<typeof managedFixture>>) => f.provider.mock.calls.filter(([url]) => String(url).endsWith("chat.postMessage"));
|
||||
it("sends to the current linked user and reuses a durable result on duplicate requests", async () => {
|
||||
const f = await managedFixture(); await f.provision(); f.provider.mockClear();
|
||||
const input = { requestId: randomUUID() };
|
||||
const result = await request(f.app).post(`/api/chat-endpoints/${f.endpoint.id}/slack/test-message`).send(input).expect(200);
|
||||
expect(result.body).toEqual({ status: "sent", slackUrl: "https://app.slack.com/client/TAUTO/DWELCOME" });
|
||||
await f.service.sendSlackTestMessage(f.endpoint.id, actor, input);
|
||||
expect(posts(f)).toHaveLength(1);
|
||||
expect(new URLSearchParams(String(posts(f)[0]![1]!.body)).get("channel")).toBe(f.installerId);
|
||||
});
|
||||
it("links the actor's own manager identity without requiring a connect command", async () => {
|
||||
const f = await managedFixture(); await f.provision();
|
||||
await db.delete(chatIdentityLinks).where(eq(chatIdentityLinks.endpointId, f.endpoint.id));
|
||||
expect(await f.service.sendSlackTestMessage(f.endpoint.id, actor, { requestId: randomUUID() })).toMatchObject({ status: "sent" });
|
||||
expect(await db.select().from(chatIdentityLinks).where(eq(chatIdentityLinks.endpointId, f.endpoint.id))).toEqual([expect.objectContaining({ paperclipUserId: actor.userId, status: "linked" })]);
|
||||
});
|
||||
it("requires a private Slack challenge for an unknown account before linking and sending the test", async () => {
|
||||
const f = await managedFixture(); await f.provision();
|
||||
await db.delete(chatIdentityLinks).where(eq(chatIdentityLinks.endpointId, f.endpoint.id));
|
||||
await db.update(chatSlackManagerGrants).set({ userId: "another-user" }).where(eq(chatSlackManagerGrants.id, f.grantId));
|
||||
f.provider.mockClear(); const input = { requestId: randomUUID() };
|
||||
expect(await f.service.sendSlackTestMessage(f.endpoint.id, actor, input)).toEqual({ status: "connection_required", accounts: [{ id: f.installerId, name: "Installing Person" }], nextCursor: null });
|
||||
expect(posts(f)).toHaveLength(0);
|
||||
const pending = await f.service.sendSlackTestMessage(f.endpoint.id, actor, { ...input, slackUserId: f.installerId });
|
||||
expect(pending.status).toBe("connection_pending");
|
||||
const links = await db.select().from(chatIdentityLinks).where(eq(chatIdentityLinks.endpointId, f.endpoint.id));
|
||||
expect(links).toEqual([expect.objectContaining({ status: "pending", paperclipUserId: null })]);
|
||||
const fields = new URLSearchParams(String(posts(f)[0]![1]!.body));
|
||||
expect(fields.get("text")).not.toContain("This is your test message");
|
||||
const blocks = JSON.parse(fields.get("blocks")!);
|
||||
const invitationUrl = /<([^|]+)\|/.exec(blocks[1].text.text)![1]!;
|
||||
const token = new URL(invitationUrl).searchParams.get("token")!;
|
||||
expect(JSON.stringify(pending)).not.toContain(token);
|
||||
expect(await f.service.sendSlackTestMessage(f.endpoint.id, actor, input)).toEqual(pending);
|
||||
expect(posts(f)).toHaveLength(1);
|
||||
await f.service.confirmIdentityLink(token, actor.userId);
|
||||
expect(await f.service.sendSlackTestMessage(f.endpoint.id, actor, input)).toMatchObject({ status: "sent" });
|
||||
expect(new URLSearchParams(String(posts(f).at(-1)![1]!.body)).get("text")).toContain("This is your test message");
|
||||
});
|
||||
it("loads another workspace page instead of truncating account discovery", async () => {
|
||||
const f = await managedFixture(); await f.provision();
|
||||
await db.delete(chatIdentityLinks).where(eq(chatIdentityLinks.endpointId, f.endpoint.id));
|
||||
await db.update(chatSlackManagerGrants).set({ userId: "another-user" }).where(eq(chatSlackManagerGrants.id, f.grantId));
|
||||
f.provider.mockClear(); f.state.userListNextCursor = "next-page";
|
||||
const input = { requestId: randomUUID() };
|
||||
expect(await f.service.sendSlackTestMessage(f.endpoint.id, actor, input)).toMatchObject({ status: "connection_required", nextCursor: "next-page" });
|
||||
f.state.userListNextCursor = "";
|
||||
expect(await f.service.sendSlackTestMessage(f.endpoint.id, actor, { ...input, accountCursor: "next-page" })).toMatchObject({ status: "connection_required", nextCursor: null });
|
||||
expect(f.state.userListCursor).toBe("next-page");
|
||||
expect(posts(f)).toHaveLength(0);
|
||||
});
|
||||
it("does not downgrade confirmed delivery when activity logging fails", async () => {
|
||||
const f = await managedFixture(); await f.provision(); f.provider.mockClear();
|
||||
const original = activityLogModule.logActivity;
|
||||
const audit = vi.spyOn(activityLogModule, "logActivity").mockImplementation(async (database, entry) => {
|
||||
if (entry.action === "chat_slack.test_message_sent") throw new Error("Synthetic activity failure");
|
||||
return original(database, entry);
|
||||
});
|
||||
onTestFinished(() => audit.mockRestore());
|
||||
const input = { requestId: randomUUID() };
|
||||
expect(await f.service.sendSlackTestMessage(f.endpoint.id, actor, input)).toMatchObject({ status: "sent" });
|
||||
expect(await f.service.sendSlackTestMessage(f.endpoint.id, actor, input)).toMatchObject({ status: "sent" });
|
||||
expect(posts(f)).toHaveLength(1);
|
||||
});
|
||||
it("does not restore revoked access or take another user's identity", async () => {
|
||||
const f = await managedFixture(); await f.provision(); f.provider.mockClear();
|
||||
await db.update(chatIdentityLinks).set({ status: "revoked" }).where(eq(chatIdentityLinks.endpointId, f.endpoint.id));
|
||||
await expect(f.service.sendSlackTestMessage(f.endpoint.id, actor, { requestId: randomUUID() })).rejects.toThrow("existing link");
|
||||
await db.update(chatIdentityLinks).set({ status: "linked", paperclipUserId: "another-user" }).where(eq(chatIdentityLinks.endpointId, f.endpoint.id));
|
||||
await expect(f.service.sendSlackTestMessage(f.endpoint.id, actor, { requestId: randomUUID(), slackUserId: f.installerId })).rejects.toThrow("existing link");
|
||||
expect(posts(f)).toHaveLength(0);
|
||||
});
|
||||
it("rejects wrong-workspace users, disabled DMs, and membership revocation", async () => {
|
||||
const f = await managedFixture(); await f.provision(); f.provider.mockClear();
|
||||
f.state.user.team_id = "TOTHER";
|
||||
await expect(f.service.sendSlackTestMessage(f.endpoint.id, actor, { requestId: randomUUID() })).rejects.toThrow("personal Slack account");
|
||||
f.state.user.team_id = "TAUTO";
|
||||
await db.update(chatEndpoints).set({ allowDirectMessages: false }).where(eq(chatEndpoints.id, f.endpoint.id));
|
||||
await expect(f.service.sendSlackTestMessage(f.endpoint.id, actor, { requestId: randomUUID() })).rejects.toThrow("Enable direct messages");
|
||||
await db.update(chatEndpoints).set({ allowDirectMessages: true }).where(eq(chatEndpoints.id, f.endpoint.id));
|
||||
await db.update(companyMemberships).set({ status: "inactive" }).where(and(eq(companyMemberships.companyId, f.companyId), eq(companyMemberships.principalId, actor.userId)));
|
||||
await expect(f.service.sendSlackTestMessage(f.endpoint.id, actor, { requestId: randomUUID() })).rejects.toThrow("active Paperclip account");
|
||||
expect(posts(f)).toHaveLength(0);
|
||||
});
|
||||
it("redacts provider failures and never retries an ambiguous send", async () => {
|
||||
const f = await managedFixture(); await f.provision(); f.provider.mockClear(); f.state.timeoutWelcome = true;
|
||||
const input = { requestId: randomUUID() };
|
||||
const result = await request(f.app).post(`/api/chat-endpoints/${f.endpoint.id}/slack/test-message`).send(input).expect(422);
|
||||
expect(JSON.stringify(result.body)).not.toContain("canary");
|
||||
await expect(f.service.sendSlackTestMessage(f.endpoint.id, actor, input)).rejects.toThrow("may already have been sent");
|
||||
expect(posts(f)).toHaveLength(1);
|
||||
const actions = await db.select().from(chatActions).where(and(eq(chatActions.endpointId, f.endpoint.id), eq(chatActions.kind, "slack_test_message")));
|
||||
expect(JSON.stringify(actions)).not.toContain("canary");
|
||||
expect(actions[0]!.status).toBe("uncertain");
|
||||
});
|
||||
});
|
||||
describe("managed Slack", () => {
|
||||
it("keeps open-source setup independent of the broker", async () => {
|
||||
const f = await fixture();
|
||||
expect(await f.service.slackRegistration.managerGrants.choices(f.companyId, actor)).toEqual({ managedAvailable: false, defaultMethod: "automatic", workspaces: [] });
|
||||
await f.create();
|
||||
expect((await f.service.get(f.endpoint.id)).setup.slackSetupMethod).toBe("automatic");
|
||||
await expect(f.service.update(f.endpoint.id, { slackSetupMethod: "managed" }, actor.userId)).rejects.toThrow();
|
||||
});
|
||||
it("keeps configured Cloud drafts managed when the broker profile disappears", async () => {
|
||||
const f = await managedFixture();
|
||||
f.broker.getSlackManagerAppId.mockResolvedValue(null);
|
||||
expect(f.grants.defaultMethod()).toBe("managed");
|
||||
expect(await f.grants.choices(f.companyId, actor)).toEqual({ managedAvailable: false, defaultMethod: "managed", workspaces: [] });
|
||||
const next = await f.service.create(f.companyId, { provider: "slack", assignedAgentId: f.assignedAgentId }, actor.userId);
|
||||
expect(next.setup.slackSetupMethod).toBe("managed");
|
||||
await expect(f.provision()).rejects.toThrow();
|
||||
});
|
||||
it("creates and installs once, uploads the avatar and links only the manager grant owner", async () => {
|
||||
const f = await managedFixture();
|
||||
await Promise.all([f.provision(), f.provision()]);
|
||||
const saved = await f.service.get(f.endpoint.id);
|
||||
expect(saved.setup).toMatchObject({ slackSetupMethod: "managed", slackRegistration: { status: "configured", appId: f.appId }, slackAvatar: { status: "uploaded" }, slackAccount: { status: "linked", paperclipUserId: actor.userId, externalUserId: f.installerId } });
|
||||
expect(f.provider.mock.calls.filter(([url]) => String(url).endsWith("apps.manifest.create"))).toHaveLength(1);
|
||||
expect(f.provider.mock.calls.filter(([url]) => String(url).endsWith("apps.managedInstall"))).toHaveLength(1);
|
||||
const activities = await db.select().from(activityLog).where(eq(activityLog.companyId, f.companyId));
|
||||
for (const secret of [f.credentials.accessToken, f.credentials.refreshToken!, f.token, clientSecret, signingSecret, "claim-canary"]) expect(JSON.stringify({ saved, activities, choices: await f.grants.choices(f.companyId, actor) })).not.toContain(secret);
|
||||
await expect(f.service.update(f.endpoint.id, { slackSetupMethod: "automatic" }, actor.userId)).rejects.toThrow();
|
||||
});
|
||||
it("reuses the same personal workspace grant for another draft without another authorization", async () => {
|
||||
const f = await managedFixture();
|
||||
const draft = await f.service.create(f.companyId, { provider: "slack", assignedAgentId: f.assignedAgentId, slackSetupMethod: "managed" }, actor.userId);
|
||||
expect((await f.grants.choices(f.companyId, actor)).workspaces[0]!.grantId).toBe(f.grantId);
|
||||
const otherActor = { ...actor, userId: "another-user", bypassPermissionCheck: true };
|
||||
expect((await f.grants.choices(f.companyId, otherActor)).workspaces).toEqual([]);
|
||||
await expect(f.service.slackRegistration.managed.provision(draft.id, otherActor, { ...f.input, requestId: randomUUID() })).rejects.toThrow();
|
||||
expect(f.broker.startAuthorization).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it("installs a second bot using the same grant without new manager consent", async () => {
|
||||
const f = await managedFixture(); await f.provision();
|
||||
const secondBot = `${f.botId}SECOND`;
|
||||
const secondApp = `${f.appId}SECOND`;
|
||||
const scopes = buildSlackAppManifest({ app: appDetails, agentName: "Maya", webhookUrl: "https://paperclip.example/hook" }).oauth_config.scopes.bot;
|
||||
const provider: typeof fetch = async (input, init) => {
|
||||
const url = String(input);
|
||||
if (url.endsWith("apps.manifest.create")) return Response.json({ ok: true, app_id: secondApp, team_id: "TAUTO", credentials: { client_id: "222.333", client_secret: clientSecret, signing_secret: signingSecret } });
|
||||
if (url.endsWith("apps.managedInstall")) return Response.json({ ok: true, app_id: secondApp, team_id: "TAUTO", api_access_tokens: { bot_access_token: "xoxb-second-managed" } });
|
||||
if (url.endsWith("auth.test")) return Response.json({ ok: true, team_id: "TAUTO", user_id: secondBot, bot_id: "BSECOND", team: "Test workspace" }, { headers: { "x-oauth-scopes": scopes.join(",") } });
|
||||
if (url.endsWith("bots.info")) return Response.json({ ok: true, bot: { id: "BSECOND", app_id: secondApp, user_id: secondBot, deleted: false } });
|
||||
return f.provider(input, init);
|
||||
};
|
||||
const { service } = createService(new FakeChatSdkRuntime(), provider, { renderSlackAvatar: f.renderAvatar, slackManaged: { enabled: true, broker: f.broker as unknown as PaperclipCloudConnector } });
|
||||
const second = await service.create(f.companyId, { provider: "slack", assignedAgentId: f.assignedAgentId, slackSetupMethod: "managed", slackApp: appDetails }, actor.userId);
|
||||
await service.slackRegistration.managed.provision(second.id, actor, { grantId: f.grantId, requestId: randomUUID() });
|
||||
expect((await service.get(second.id)).setup.slackRegistration).toMatchObject({ status: "configured", appId: secondApp });
|
||||
expect(f.broker.startAuthorization).toHaveBeenCalledTimes(1);
|
||||
expect(f.broker.claim).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it("rejects missing tool scopes before activating a managed bot", async () => {
|
||||
const f = await managedFixture(); f.state.grantedScopes = [];
|
||||
await f.provision();
|
||||
const saved = await f.service.get(f.endpoint.id);
|
||||
expect(saved.providerAccountId).toBeNull();
|
||||
expect(saved.setup.slackRegistration).toMatchObject({ status: "credentials_saved", errorCode: "slack_install_scopes_missing" });
|
||||
});
|
||||
it.each(["revoked", "attention", "active"] as const)("reinstalls the configured app on an explicit %s reconnect", async status => {
|
||||
const f = await managedFixture(); await f.provision(); f.provider.mockClear();
|
||||
await db.update(chatEndpoints).set({ status }).where(eq(chatEndpoints.id, f.endpoint.id));
|
||||
f.state.managedAccessToken = `xoxb-reconnected-${f.botId}`; f.state.rejectOldBotToken = true;
|
||||
const reconnectInput = { ...f.input, requestId: randomUUID(), reconnect: true };
|
||||
await f.service.slackRegistration.managed.provision(f.endpoint.id, actor, reconnectInput);
|
||||
expect(f.provider.mock.calls.filter(([url]) => String(url).endsWith("apps.managedInstall"))).toHaveLength(1);
|
||||
expect(f.provider.mock.calls.filter(([url]) => String(url).endsWith("apps.manifest.create"))).toHaveLength(0);
|
||||
expect((await f.service.get(f.endpoint.id)).status).not.toBe("revoked");
|
||||
expect((await f.service.get(f.endpoint.id)).setup.slackRegistration).toMatchObject({ status: "configured", appId: f.appId });
|
||||
await f.service.slackRegistration.managed.provision(f.endpoint.id, actor, reconnectInput);
|
||||
await f.provision();
|
||||
expect(f.provider.mock.calls.filter(([url]) => String(url).endsWith("apps.managedInstall"))).toHaveLength(1);
|
||||
});
|
||||
it("preserves configured-app reconnect intent when managed installation fails", async () => {
|
||||
const f = await managedFixture(); await f.provision(); f.provider.mockClear();
|
||||
await db.update(chatEndpoints).set({ status: "revoked" }).where(eq(chatEndpoints.id, f.endpoint.id));
|
||||
f.state.managedError = "temporary_error";
|
||||
await f.provision();
|
||||
expect((await f.service.get(f.endpoint.id)).setup.slackRegistration).toMatchObject({ status: "install", appId: f.appId });
|
||||
f.state.managedError = null;
|
||||
await f.provision();
|
||||
expect((await f.service.get(f.endpoint.id)).setup.slackRegistration?.status).toBe("configured");
|
||||
expect(f.provider.mock.calls.filter(([url]) => String(url).endsWith("apps.manifest.create"))).toHaveLength(0);
|
||||
});
|
||||
it("reinstalls the saved app after missing bot scopes without creating another app", async () => {
|
||||
const f = await managedFixture(); const scopes = f.state.grantedScopes;
|
||||
f.state.grantedScopes = []; await f.provision();
|
||||
f.state.grantedScopes = scopes; await f.provision();
|
||||
expect((await f.service.get(f.endpoint.id)).setup.slackRegistration?.status).toBe("configured");
|
||||
expect(f.provider.mock.calls.filter(([url]) => String(url).endsWith("apps.managedInstall"))).toHaveLength(2);
|
||||
expect(f.provider.mock.calls.filter(([url]) => String(url).endsWith("apps.manifest.create"))).toHaveLength(1);
|
||||
});
|
||||
it("retains reinstall intent after a provider failure instead of retrying rejected credentials", async () => {
|
||||
const f = await managedFixture(); const scopes = f.state.grantedScopes;
|
||||
f.state.grantedScopes = []; await f.provision();
|
||||
f.state.managedError = "internal_error"; await f.provision();
|
||||
expect((await f.service.get(f.endpoint.id)).setup.slackRegistration).toMatchObject({ status: "install", errorCode: "slack_provider_failure" });
|
||||
f.state.managedError = null; f.state.grantedScopes = scopes; await f.provision();
|
||||
expect((await f.service.get(f.endpoint.id)).setup.slackRegistration?.status).toBe("configured");
|
||||
expect(f.provider.mock.calls.filter(([url]) => String(url).endsWith("apps.managedInstall"))).toHaveLength(3);
|
||||
expect(f.provider.mock.calls.filter(([url]) => String(url).endsWith("apps.manifest.create"))).toHaveLength(1);
|
||||
});
|
||||
it("clears a manager reconnect error after renewing the bound grant", async () => {
|
||||
const f = await managedFixture(); f.state.managedError = "invalid_auth"; await f.provision();
|
||||
expect((await f.service.get(f.endpoint.id)).setup.slackRegistration?.errorCode).toBe("slack_manager_reauthorize");
|
||||
await f.grants.complete(await f.authorize(), "new-claim", null, actor);
|
||||
expect((await f.service.get(f.endpoint.id)).setup.slackRegistration?.errorCode).toBeNull();
|
||||
f.state.managedError = null; await f.provision();
|
||||
expect((await f.service.get(f.endpoint.id)).setup.slackRegistration?.status).toBe("configured");
|
||||
expect(f.provider.mock.calls.filter(([url]) => String(url).endsWith("apps.manifest.create"))).toHaveLength(1);
|
||||
});
|
||||
it("reconnects a rejected manager token without losing pending manifest configuration", async () => {
|
||||
const f = await managedFixture(); f.state.failManifestUpdate = true; await f.provision();
|
||||
expect((await f.service.get(f.endpoint.id)).setup.slackRegistration?.errorCode).toBe("slack_manifest_update_pending");
|
||||
expect((await f.grants.choices(f.companyId, actor)).workspaces).toEqual([]);
|
||||
expect((await f.grants.get(f.grantId, f.companyId, actor)).status).toBe("reauthorize");
|
||||
await f.grants.complete(await f.authorize(), "new-claim", null, actor);
|
||||
f.state.failManifestUpdate = false; await f.provision();
|
||||
expect((await f.service.get(f.endpoint.id)).setup.slackRegistration?.status).toBe("configured");
|
||||
expect((await f.service.get(f.endpoint.id)).setup.slackAvatar?.status).toBe("uploaded");
|
||||
expect(f.provider.mock.calls.filter(([url]) => String(url).endsWith("apps.icon.set"))).toHaveLength(1);
|
||||
expect(f.provider.mock.calls.filter(([url]) => String(url).endsWith("apps.manifest.create"))).toHaveLength(1);
|
||||
});
|
||||
it("does not restore a revoked grant when an in-flight refresh returns", async () => {
|
||||
const f = await managedFixture();
|
||||
await db.update(chatSlackManagerGrants).set({ expiresAt: new Date(0) }).where(eq(chatSlackManagerGrants.id, f.grantId));
|
||||
f.broker.refresh.mockImplementation(async () => {
|
||||
await f.grants.revoke(f.grantId, f.companyId, actor);
|
||||
return f.credentials;
|
||||
});
|
||||
await expect(f.grants.token(f.grantId, f.companyId, actor)).rejects.toThrow();
|
||||
expect((await f.grants.get(f.grantId, f.companyId, actor)).status).toBe("revoked");
|
||||
});
|
||||
it("claims concurrent manager callbacks once", async () => {
|
||||
const f = await managedFixture(); const state = await f.authorize();
|
||||
const results = await Promise.allSettled([1, 2].map(() => f.grants.complete(state, "claim", null, actor)));
|
||||
expect(results.filter(result => result.status === "fulfilled")).toHaveLength(1);
|
||||
expect(f.broker.claim).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
it("atomically consumes manager state and rejects wrong actors and expired attempts", async () => {
|
||||
const f = await managedFixture();
|
||||
await expect(f.grants.complete(f.authorizeState, "replay", null, actor)).rejects.toThrow();
|
||||
const state = await f.authorize();
|
||||
await expect(f.grants.complete(state, "claim", null, { ...actor, userId: "another-user", bypassPermissionCheck: true })).rejects.toThrow();
|
||||
await db.update(toolOauthStates).set({ expiresAt: new Date(0) }).where(eq(toolOauthStates.state, state));
|
||||
await expect(f.grants.complete(state, "claim", null, actor)).rejects.toThrow();
|
||||
expect(f.broker.claim).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it("preserves declined manager authorization as a safe recoverable outcome", async () => {
|
||||
const f = await managedFixture();
|
||||
await f.grants.complete(await f.authorize(), null, "access_denied", actor);
|
||||
expect((await f.service.get(f.endpoint.id)).setup.slackManagerError).toBe("slack_install_declined");
|
||||
});
|
||||
it("resumes saved installation credentials after inventory failure without creating or reinstalling", async () => {
|
||||
const f = await managedFixture(); f.state.failInventory = true;
|
||||
await f.provision();
|
||||
expect((await f.service.slackRegistration.registration(f.endpoint.id))?.status).toBe("credentials_saved");
|
||||
f.state.failInventory = false; await f.provision();
|
||||
expect((await f.service.get(f.endpoint.id)).setup.slackRegistration?.status).toBe("configured");
|
||||
expect(f.provider.mock.calls.filter(([url]) => String(url).endsWith("apps.managedInstall"))).toHaveLength(1);
|
||||
expect(f.provider.mock.calls.filter(([url]) => String(url).endsWith("apps.manifest.create"))).toHaveLength(1);
|
||||
});
|
||||
it("never recreates an uncertain app", async () => {
|
||||
const f = await managedFixture(); f.state.failCreate = true;
|
||||
await f.provision(); await f.provision();
|
||||
expect((await f.service.get(f.endpoint.id)).setup.slackRegistration?.status).toBe("uncertain");
|
||||
expect(f.provider.mock.calls.filter(([url]) => String(url).endsWith("apps.manifest.create"))).toHaveLength(1);
|
||||
});
|
||||
it("uses bound OAuth for approval fallback and rejects a different installer", async () => {
|
||||
const f = await managedFixture(); f.state.managedError = "app_approval_request_eligible";
|
||||
const result = await f.provision();
|
||||
const state = new URL(result.authorization!.authorizationUrl).searchParams.get("state")!;
|
||||
f.state.oauth.authed_user.id = "UOTHER";
|
||||
await f.service.slackRegistration.complete(state, "code-canary", null, actor);
|
||||
expect((await f.service.get(f.endpoint.id)).setup.slackRegistration?.errorCode).toBe("slack_install_identity_mismatch");
|
||||
expect((await f.service.get(f.endpoint.id)).providerAccountId).toBeNull();
|
||||
});
|
||||
it("does not activate a bot when the manager grant is revoked during child OAuth", async () => {
|
||||
const f = await managedFixture(); f.state.managedError = "app_approval_request_eligible";
|
||||
const result = await f.provision();
|
||||
const state = new URL(result.authorization!.authorizationUrl).searchParams.get("state")!;
|
||||
f.state.beforeExchange = () => f.grants.revoke(f.grantId, f.companyId, actor);
|
||||
await f.service.slackRegistration.complete(state, "code-canary", null, actor);
|
||||
const saved = await f.service.get(f.endpoint.id);
|
||||
expect(saved.providerAccountId).toBeNull();
|
||||
expect(saved.setup.slackRegistration?.errorCode).toBe("slack_install_identity_mismatch");
|
||||
});
|
||||
it("uses the held transaction for grant queries without acquiring another pool connection", async () => {
|
||||
const f = await managedFixture();
|
||||
const transactionContext = new AsyncLocalStorage<boolean>();
|
||||
const originalTransaction = db.transaction.bind(db);
|
||||
const transactionSpy = vi.spyOn(db, "transaction").mockImplementation((work, ...args) =>
|
||||
originalTransaction(tx => transactionContext.run(true, () => work(tx)), ...args));
|
||||
const querySpies = (["select", "insert", "update", "delete", "execute"] as const).map(method => {
|
||||
const original = db[method].bind(db) as (...args: unknown[]) => unknown;
|
||||
return vi.spyOn(db, method).mockImplementation(((...args: unknown[]) => {
|
||||
if (transactionContext.getStore()) throw new Error("Acquired outer pool connection from held transaction");
|
||||
return original(...args);
|
||||
}) as never);
|
||||
});
|
||||
try {
|
||||
await f.grants.token(f.grantId, f.companyId, actor);
|
||||
await db.update(chatSlackManagerGrants).set({ expiresAt: new Date(0) }).where(eq(chatSlackManagerGrants.id, f.grantId));
|
||||
await f.grants.token(f.grantId, f.companyId, actor);
|
||||
expect(f.broker.refresh).toHaveBeenCalledTimes(1);
|
||||
await f.grants.request(f.grantId, f.companyId, actor, "apps.manifest.validate", { manifest: "{}" });
|
||||
await f.grants.complete(await f.authorize(), "renewed-claim", null, actor);
|
||||
expect((await f.service.get(f.endpoint.id)).setup.slackManagerError).toBeUndefined();
|
||||
await f.grants.revoke(f.grantId, f.companyId, actor);
|
||||
expect(f.broker.revoke).toHaveBeenCalledTimes(2);
|
||||
} finally {
|
||||
transactionSpy.mockRestore();
|
||||
for (const spy of querySpies) spy.mockRestore();
|
||||
}
|
||||
});
|
||||
it("serializes refreshes across endpoints and never replays an ambiguous refresh", async () => {
|
||||
const f = await managedFixture();
|
||||
await db.update(chatSlackManagerGrants).set({ expiresAt: new Date(0) }).where(eq(chatSlackManagerGrants.id, f.grantId));
|
||||
const tokens = await Promise.all([1, 2].map(() => f.grants.token(f.grantId, f.companyId, actor)));
|
||||
expect(tokens.every(result => result.token === "xoxe.rotated-canary")).toBe(true);
|
||||
expect(f.broker.refresh).toHaveBeenCalledTimes(1);
|
||||
await db.update(chatSlackManagerGrants).set({ expiresAt: new Date(0) }).where(eq(chatSlackManagerGrants.id, f.grantId));
|
||||
f.broker.refresh.mockRejectedValue(new Error("provider echoed manager-canary"));
|
||||
await expect(f.grants.token(f.grantId, f.companyId, actor)).rejects.toThrow();
|
||||
await expect(f.grants.token(f.grantId, f.companyId, actor)).rejects.toThrow();
|
||||
expect(f.broker.refresh).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
it("rejects a bot token belonging to a different Slack app before activation", async () => {
|
||||
const f = await managedFixture(); f.state.managedBotAppId = "ADIFFERENT";
|
||||
await f.provision();
|
||||
const saved = await f.service.get(f.endpoint.id);
|
||||
expect(saved.providerAccountId).toBeNull();
|
||||
expect(saved.setup.slackRegistration).toMatchObject({ status: "credentials_saved", errorCode: "slack_install_identity_mismatch" });
|
||||
});
|
||||
it("rejects manager identity changes and both callback-origin and ingress-origin drift", async () => {
|
||||
const f = await managedFixture(); const state = await f.authorize();
|
||||
for (const change of [{ publicBaseUrl: "https://changed.example" }, { webhookPublicBaseUrl: "https://changed.example" }]) {
|
||||
const changed = createService(new FakeChatSdkRuntime(), f.provider, { ...change, slackManaged: { enabled: true, broker: f.broker as unknown as PaperclipCloudConnector } }).service;
|
||||
await expect(changed.slackRegistration.managerGrants.complete(state, "claim", null, actor)).rejects.toThrow();
|
||||
}
|
||||
f.broker.getSlackManagerAppId.mockResolvedValue("ANEWMANAGER");
|
||||
await expect(f.provision()).rejects.toThrow();
|
||||
expect((await f.grants.choices(f.companyId, actor)).workspaces).toEqual([]);
|
||||
});
|
||||
it("preserves provider rate-limit backoff across subsequent attempts", async () => {
|
||||
const f = await managedFixture(); f.state.managedError = "ratelimited";
|
||||
await f.provision();
|
||||
f.state.managedError = null; await f.provision();
|
||||
expect(f.provider.mock.calls.filter(([url]) => String(url).endsWith("apps.managedInstall"))).toHaveLength(1);
|
||||
expect((await f.service.get(f.endpoint.id)).setup.slackRegistration?.errorCode).toBe("slack_setup_rate_limited");
|
||||
});
|
||||
it("routes the broker claim_id callback back to the saved wizard without exposing the handoff", async () => {
|
||||
const f = await managedFixture(); const state = await f.authorize();
|
||||
const response = await request(f.app).get(`/api/chat-slack/managed/oauth/callback?state=${state}&claim_id=sealed-claim-canary`).set("accept", "application/json").expect(303);
|
||||
expect(f.broker.claim.mock.calls.at(-1)![0].claimId).toBe("sealed-claim-canary");
|
||||
expect(response.headers.location).toContain(f.endpoint.id);
|
||||
expect(response.text).not.toContain("sealed-claim-canary");
|
||||
expect(response.headers["cache-control"]).toBe("no-store");
|
||||
});
|
||||
it("does not accept another company's grant or keep authorizations after removal", async () => {
|
||||
const f = await managedFixture(); const other = await seedCompany();
|
||||
await expect(f.grants.token(f.grantId, other.companyId, { ...actor, bypassPermissionCheck: true })).rejects.toThrow();
|
||||
const state = await f.authorize();
|
||||
await f.service.configure(f.endpoint.id, { action: "remove" }, actor.userId);
|
||||
await expect(f.grants.complete(state, "claim", null, actor)).rejects.toThrow();
|
||||
expect((await f.grants.get(f.grantId, f.companyId, actor)).status).toBe("active");
|
||||
});
|
||||
it("revocation invalidates pending authorization and prevents future provisioning", async () => {
|
||||
const f = await managedFixture(); const state = await f.authorize();
|
||||
await f.grants.revoke(f.grantId, f.companyId, actor);
|
||||
await expect(f.grants.complete(state, "claim", null, actor)).rejects.toThrow();
|
||||
await expect(f.provision()).rejects.toThrow();
|
||||
expect((await f.grants.choices(f.companyId, actor)).workspaces).toEqual([]);
|
||||
});
|
||||
});
|
||||
function setupEvent(f: Awaited<ReturnType<typeof fixture>>, patch: Record<string, unknown> = {}, url = f.endpoint.setup.webhookUrl!, secret = signingSecret, timestamp?: string) {
|
||||
return signedSlackWebhookRequest({ url, contentType: "application/json", signingSecret: secret, ...(timestamp ? { timestamp } : {}),
|
||||
body: JSON.stringify({ type: "event_callback", api_app_id: f.appId, team_id: "TAUTO", event_id: "EvSETUP", event: {
|
||||
|
||||
@@ -522,6 +522,17 @@ describe("company portability", () => {
|
||||
}));
|
||||
});
|
||||
|
||||
function exportCompanyAndAgents(portability: ReturnType<typeof companyPortabilityService>) {
|
||||
return portability.exportBundle("company-1", {
|
||||
include: {
|
||||
company: true,
|
||||
agents: true,
|
||||
projects: false,
|
||||
issues: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
it("renders high-volume YAML blocks without overflowing the call stack", () => {
|
||||
const tasks = Array.from({ length: 130_000 }, (_, index) => `issue-${index}`);
|
||||
|
||||
@@ -705,14 +716,7 @@ describe("company portability", () => {
|
||||
};
|
||||
const portability = companyPortabilityService(db as any);
|
||||
|
||||
const exported = await portability.exportBundle("company-1", {
|
||||
include: {
|
||||
company: true,
|
||||
agents: true,
|
||||
projects: false,
|
||||
issues: false,
|
||||
},
|
||||
});
|
||||
const exported = await exportCompanyAndAgents(portability);
|
||||
|
||||
const extension = asTextFile(exported.files[".paperclip.yaml"]);
|
||||
expect(extension).toContain("permissionGrants:");
|
||||
@@ -794,14 +798,7 @@ describe("company portability", () => {
|
||||
},
|
||||
]);
|
||||
|
||||
const exported = await portability.exportBundle("company-1", {
|
||||
include: {
|
||||
company: true,
|
||||
agents: true,
|
||||
projects: false,
|
||||
issues: false,
|
||||
},
|
||||
});
|
||||
const exported = await exportCompanyAndAgents(portability);
|
||||
|
||||
const serialized = JSON.stringify(exported);
|
||||
expect(serialized).not.toContain("sk-inline-secret-value");
|
||||
@@ -1778,14 +1775,7 @@ describe("company portability", () => {
|
||||
it("reads env inputs back from .paperclip.yaml during preview import", async () => {
|
||||
const portability = companyPortabilityService({} as any);
|
||||
|
||||
const exported = await portability.exportBundle("company-1", {
|
||||
include: {
|
||||
company: true,
|
||||
agents: true,
|
||||
projects: false,
|
||||
issues: false,
|
||||
},
|
||||
});
|
||||
const exported = await exportCompanyAndAgents(portability);
|
||||
|
||||
const preview = await portability.previewImport({
|
||||
source: {
|
||||
@@ -3321,14 +3311,7 @@ describe("company portability", () => {
|
||||
},
|
||||
]);
|
||||
|
||||
const exported = await portability.exportBundle("company-1", {
|
||||
include: {
|
||||
company: true,
|
||||
agents: true,
|
||||
projects: false,
|
||||
issues: false,
|
||||
},
|
||||
});
|
||||
const exported = await exportCompanyAndAgents(portability);
|
||||
|
||||
const extension = asTextFile(exported.files[".paperclip.yaml"]);
|
||||
expect(extension).toContain("APIKEY:");
|
||||
@@ -3365,14 +3348,7 @@ describe("company portability", () => {
|
||||
reason: "Existing skill matched; renamed to paperclip-2.",
|
||||
}]);
|
||||
|
||||
const exported = await portability.exportBundle("company-1", {
|
||||
include: {
|
||||
company: true,
|
||||
agents: true,
|
||||
projects: false,
|
||||
issues: false,
|
||||
},
|
||||
});
|
||||
const exported = await exportCompanyAndAgents(portability);
|
||||
|
||||
agentSvc.list.mockResolvedValue([]);
|
||||
|
||||
@@ -3445,14 +3421,7 @@ describe("company portability", () => {
|
||||
});
|
||||
|
||||
const portability = companyPortabilityService({} as any, storage as any);
|
||||
const exported = await portability.exportBundle("company-1", {
|
||||
include: {
|
||||
company: true,
|
||||
agents: true,
|
||||
projects: false,
|
||||
issues: false,
|
||||
},
|
||||
});
|
||||
const exported = await exportCompanyAndAgents(portability);
|
||||
|
||||
exported.files["images/company-logo.png"] = {
|
||||
encoding: "base64",
|
||||
@@ -3518,14 +3487,7 @@ describe("company portability", () => {
|
||||
name: "ClaudeCoder",
|
||||
});
|
||||
|
||||
const exported = await portability.exportBundle("company-1", {
|
||||
include: {
|
||||
company: true,
|
||||
agents: true,
|
||||
projects: false,
|
||||
issues: false,
|
||||
},
|
||||
});
|
||||
const exported = await exportCompanyAndAgents(portability);
|
||||
|
||||
agentSvc.list.mockResolvedValue([]);
|
||||
|
||||
@@ -3585,14 +3547,7 @@ describe("company portability", () => {
|
||||
})));
|
||||
agentSvc.list.mockClear();
|
||||
|
||||
const exported = await portability.exportBundle("company-1", {
|
||||
include: {
|
||||
company: true,
|
||||
agents: true,
|
||||
projects: false,
|
||||
issues: false,
|
||||
},
|
||||
});
|
||||
const exported = await exportCompanyAndAgents(portability);
|
||||
|
||||
agentSvc.list.mockResolvedValue([]);
|
||||
|
||||
@@ -3637,14 +3592,7 @@ describe("company portability", () => {
|
||||
it("imports only selected files and leaves unchecked company metadata alone", async () => {
|
||||
const portability = companyPortabilityService({} as any);
|
||||
|
||||
const exported = await portability.exportBundle("company-1", {
|
||||
include: {
|
||||
company: true,
|
||||
agents: true,
|
||||
projects: false,
|
||||
issues: false,
|
||||
},
|
||||
});
|
||||
const exported = await exportCompanyAndAgents(portability);
|
||||
|
||||
agentSvc.list.mockResolvedValue([]);
|
||||
projectSvc.list.mockResolvedValue([]);
|
||||
@@ -3735,14 +3683,7 @@ describe("company portability", () => {
|
||||
name: "ClaudeCoder",
|
||||
});
|
||||
|
||||
const exported = await portability.exportBundle("company-1", {
|
||||
include: {
|
||||
company: true,
|
||||
agents: true,
|
||||
projects: false,
|
||||
issues: false,
|
||||
},
|
||||
});
|
||||
const exported = await exportCompanyAndAgents(portability);
|
||||
|
||||
agentSvc.list.mockResolvedValue([]);
|
||||
|
||||
@@ -3818,14 +3759,7 @@ describe("company portability", () => {
|
||||
adapterConfig: input.adapterConfig,
|
||||
}));
|
||||
|
||||
const exported = await portability.exportBundle("company-1", {
|
||||
include: {
|
||||
company: true,
|
||||
agents: true,
|
||||
projects: false,
|
||||
issues: false,
|
||||
},
|
||||
});
|
||||
const exported = await exportCompanyAndAgents(portability);
|
||||
|
||||
agentSvc.list.mockResolvedValue([]);
|
||||
|
||||
@@ -5517,14 +5451,7 @@ describe("company portability", () => {
|
||||
name: "ClaudeCoder",
|
||||
});
|
||||
|
||||
const exported = await portability.exportBundle("company-1", {
|
||||
include: {
|
||||
company: true,
|
||||
agents: true,
|
||||
projects: false,
|
||||
issues: false,
|
||||
},
|
||||
});
|
||||
const exported = await exportCompanyAndAgents(portability);
|
||||
const originalAgentsMarkdown = exported.files["agents/claudecoder/AGENTS.md"];
|
||||
expect(typeof originalAgentsMarkdown).toBe("string");
|
||||
|
||||
@@ -5576,14 +5503,7 @@ describe("company portability", () => {
|
||||
|
||||
it("rejects dangerous adapter types on agent-safe imports", async () => {
|
||||
const portability = companyPortabilityService({} as any);
|
||||
const exported = await portability.exportBundle("company-1", {
|
||||
include: {
|
||||
company: true,
|
||||
agents: true,
|
||||
projects: false,
|
||||
issues: false,
|
||||
},
|
||||
});
|
||||
const exported = await exportCompanyAndAgents(portability);
|
||||
|
||||
agentSvc.list.mockResolvedValue([]);
|
||||
|
||||
@@ -5760,14 +5680,7 @@ describe("company portability", () => {
|
||||
|
||||
it("imports new agents as active while preserving future hire approval settings", async () => {
|
||||
const portability = companyPortabilityService({} as any);
|
||||
const exported = await portability.exportBundle("company-1", {
|
||||
include: {
|
||||
company: true,
|
||||
agents: true,
|
||||
projects: false,
|
||||
issues: false,
|
||||
},
|
||||
});
|
||||
const exported = await exportCompanyAndAgents(portability);
|
||||
|
||||
agentSvc.list.mockResolvedValue([]);
|
||||
secretSvc.normalizeAdapterConfigForPersistence.mockResolvedValueOnce({
|
||||
@@ -5836,14 +5749,7 @@ describe("company portability", () => {
|
||||
},
|
||||
})));
|
||||
agentSvc.list.mockClear();
|
||||
const exported = await portability.exportBundle("company-1", {
|
||||
include: {
|
||||
company: true,
|
||||
agents: true,
|
||||
projects: false,
|
||||
issues: false,
|
||||
},
|
||||
});
|
||||
const exported = await exportCompanyAndAgents(portability);
|
||||
|
||||
secretSvc.normalizeAdapterConfigForPersistence.mockResolvedValueOnce({
|
||||
normalized: "updated",
|
||||
|
||||
@@ -201,6 +201,75 @@ function registerModuleMocks() {
|
||||
}));
|
||||
}
|
||||
|
||||
function boardActor(userId = "local-board", source = "local_implicit") {
|
||||
return {
|
||||
type: "board",
|
||||
userId: userId,
|
||||
companyIds: ["company-1"],
|
||||
source: source,
|
||||
isInstanceAdmin: false,
|
||||
};
|
||||
}
|
||||
|
||||
function assignedAgentActor() {
|
||||
return {
|
||||
type: "agent",
|
||||
agentId: "55555555-5555-4555-8555-555555555555",
|
||||
companyId: "company-1",
|
||||
runId: "run-1",
|
||||
};
|
||||
}
|
||||
|
||||
function createCatalogSkill() {
|
||||
return {
|
||||
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",
|
||||
};
|
||||
}
|
||||
|
||||
function createSkillTestRunDetails(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
id: "22222222-2222-4222-8222-222222222222",
|
||||
companyId: "company-1",
|
||||
skillId: "skill-1",
|
||||
inputId: "11111111-1111-4111-8111-111111111111",
|
||||
inputSnapshot: "Try the skill",
|
||||
skillVersionId: "33333333-3333-4333-8333-333333333333",
|
||||
agentId: "55555555-5555-4555-8555-555555555555",
|
||||
agentConfigSnapshot: { adapterType: "codex_local" },
|
||||
issueId: "44444444-4444-4444-8444-444444444444",
|
||||
templateId: "built-in:default-test-template",
|
||||
templateName: "Default test template",
|
||||
templateBody: "Default {{skillName}}",
|
||||
renderedTemplateBody: "Default Review",
|
||||
harnessIssueDescription: "Try the skill\n\n---\n\nDefault Review",
|
||||
status: "queued",
|
||||
outputDocumentKey: "output",
|
||||
outputSnapshot: "",
|
||||
error: null,
|
||||
deletedAt: null,
|
||||
supersededAt: null,
|
||||
harnessIssueExpiresAt: null,
|
||||
harnessIssueDeletedAt: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("company skill mutation permissions", () => {
|
||||
const routeModules = hoistModuleGraph(registerModuleMocks, async () => {
|
||||
const [{ companySkillRoutes }, { errorHandler }] = await Promise.all([
|
||||
@@ -399,25 +468,7 @@ describe("company skill mutation permissions", () => {
|
||||
createdAt: new Date("2026-05-26T00:00:00.000Z"),
|
||||
updatedAt: new Date("2026-05-26T00:00:00.000Z"),
|
||||
},
|
||||
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",
|
||||
},
|
||||
catalogSkill: createCatalogSkill(),
|
||||
warnings: [],
|
||||
});
|
||||
mockCompanySkillService.createLocalSkill.mockResolvedValue({
|
||||
@@ -590,56 +641,14 @@ describe("company skill mutation permissions", () => {
|
||||
mockCompanySkillService.listTestRuns.mockResolvedValue([]);
|
||||
mockCompanySkillService.getTestRunDetail.mockResolvedValue(null);
|
||||
mockCompanySkillService.createTestRun.mockResolvedValue({
|
||||
id: "22222222-2222-4222-8222-222222222222",
|
||||
companyId: "company-1",
|
||||
skillId: "skill-1",
|
||||
inputId: "11111111-1111-4111-8111-111111111111",
|
||||
inputSnapshot: "Try the skill",
|
||||
skillVersionId: "33333333-3333-4333-8333-333333333333",
|
||||
agentId: "55555555-5555-4555-8555-555555555555",
|
||||
agentConfigSnapshot: { adapterType: "codex_local" },
|
||||
issueId: "44444444-4444-4444-8444-444444444444",
|
||||
templateId: "built-in:default-test-template",
|
||||
templateName: "Default test template",
|
||||
templateBody: "Default {{skillName}}",
|
||||
renderedTemplateBody: "Default Review",
|
||||
harnessIssueDescription: "Try the skill\n\n---\n\nDefault Review",
|
||||
status: "queued",
|
||||
outputDocumentKey: "output",
|
||||
outputSnapshot: "",
|
||||
error: null,
|
||||
deletedAt: null,
|
||||
supersededAt: null,
|
||||
harnessIssueExpiresAt: null,
|
||||
harnessIssueDeletedAt: null,
|
||||
...createSkillTestRunDetails(),
|
||||
createdAt: new Date("2026-05-26T00:00:00.000Z"),
|
||||
updatedAt: new Date("2026-05-26T00:00:00.000Z"),
|
||||
cost: { costCents: 0, inputTokens: 0, cachedInputTokens: 0, outputTokens: 0 },
|
||||
taskExpired: false,
|
||||
});
|
||||
mockCompanySkillService.cancelTestRun.mockResolvedValue({
|
||||
id: "22222222-2222-4222-8222-222222222222",
|
||||
companyId: "company-1",
|
||||
skillId: "skill-1",
|
||||
inputId: "11111111-1111-4111-8111-111111111111",
|
||||
inputSnapshot: "Try the skill",
|
||||
skillVersionId: "33333333-3333-4333-8333-333333333333",
|
||||
agentId: "55555555-5555-4555-8555-555555555555",
|
||||
agentConfigSnapshot: { adapterType: "codex_local" },
|
||||
issueId: "44444444-4444-4444-8444-444444444444",
|
||||
templateId: "built-in:default-test-template",
|
||||
templateName: "Default test template",
|
||||
templateBody: "Default {{skillName}}",
|
||||
renderedTemplateBody: "Default Review",
|
||||
harnessIssueDescription: "Try the skill\n\n---\n\nDefault Review",
|
||||
status: "cancelled",
|
||||
outputDocumentKey: "output",
|
||||
outputSnapshot: "",
|
||||
error: "Cancelled by operator",
|
||||
deletedAt: null,
|
||||
supersededAt: null,
|
||||
harnessIssueExpiresAt: null,
|
||||
harnessIssueDeletedAt: null,
|
||||
...createSkillTestRunDetails({ status: "cancelled", error: "Cancelled by operator" }),
|
||||
createdAt: new Date("2026-05-26T00:00:00.000Z"),
|
||||
updatedAt: new Date("2026-05-26T00:01:00.000Z"),
|
||||
cost: { costCents: 0, inputTokens: 0, cachedInputTokens: 0, outputTokens: 0 },
|
||||
@@ -661,25 +670,7 @@ describe("company skill mutation permissions", () => {
|
||||
mockHeartbeatService.wakeup.mockResolvedValue({});
|
||||
mockHeartbeatService.cancelRun.mockResolvedValue({});
|
||||
mockCatalogService.listCatalogSkillsOrEmpty.mockReturnValue([]);
|
||||
mockCatalogService.getCatalogSkillOrThrow.mockReturnValue({
|
||||
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",
|
||||
});
|
||||
mockCatalogService.getCatalogSkillOrThrow.mockReturnValue(createCatalogSkill());
|
||||
mockCatalogService.readCatalogSkillFile.mockResolvedValue({
|
||||
catalogSkillId: "paperclipai:bundled:software-development:review",
|
||||
path: "SKILL.md",
|
||||
@@ -709,13 +700,7 @@ describe("company skill mutation permissions", () => {
|
||||
});
|
||||
|
||||
it("allows local board operators to mutate company skills", async () => {
|
||||
const res = await request(await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
companyIds: ["company-1"],
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: false,
|
||||
}))
|
||||
const res = await request(await createApp(boardActor()))
|
||||
.post("/api/companies/company-1/skills/import")
|
||||
.send({ source: "https://github.com/vercel-labs/agent-browser" });
|
||||
|
||||
@@ -750,13 +735,7 @@ describe("company skill mutation permissions", () => {
|
||||
}],
|
||||
warnings: [],
|
||||
});
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
companyIds: ["company-1"],
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: false,
|
||||
});
|
||||
const app = await createApp(boardActor());
|
||||
|
||||
const preview = await request(app)
|
||||
.post("/api/companies/company-1/skills/scan-projects")
|
||||
@@ -798,13 +777,7 @@ describe("company skill mutation permissions", () => {
|
||||
"Actor is restricted from changing skill configuration.",
|
||||
));
|
||||
|
||||
const res = await request(await createApp({
|
||||
type: "board",
|
||||
userId: "board-user",
|
||||
companyIds: ["company-1"],
|
||||
source: "session",
|
||||
isInstanceAdmin: false,
|
||||
}))
|
||||
const res = await request(await createApp(boardActor("board-user", "session")))
|
||||
.post("/api/companies/company-1/skills/scan-projects")
|
||||
.send({ mode: "preview", workspaceIds: [workspaceId] });
|
||||
|
||||
@@ -814,13 +787,7 @@ describe("company skill mutation permissions", () => {
|
||||
});
|
||||
|
||||
it("allows board users with skills:create to create, import, install, update, delete, audit, and reset company skills", async () => {
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "board-user",
|
||||
companyIds: ["company-1"],
|
||||
source: "session",
|
||||
isInstanceAdmin: false,
|
||||
});
|
||||
const app = await createApp(boardActor("board-user", "session"));
|
||||
|
||||
await request(app)
|
||||
.post("/api/companies/company-1/skills")
|
||||
@@ -867,13 +834,7 @@ describe("company skill mutation permissions", () => {
|
||||
it("allows board users without skills:create when no explicit skill policy exists", async () => {
|
||||
mockAccessService.decide.mockResolvedValue(denySkillChangeDecision());
|
||||
|
||||
const res = await request(await createApp({
|
||||
type: "board",
|
||||
userId: "board-user",
|
||||
companyIds: ["company-1"],
|
||||
source: "session",
|
||||
isInstanceAdmin: false,
|
||||
}))
|
||||
const res = await request(await createApp(boardActor("board-user", "session")))
|
||||
.post("/api/companies/company-1/skills/import")
|
||||
.send({ source: "https://github.com/vercel-labs/agent-browser" });
|
||||
|
||||
@@ -897,12 +858,7 @@ describe("company skill mutation permissions", () => {
|
||||
remediation: "Contact a company administrator to change the skill policy.",
|
||||
});
|
||||
|
||||
const res = await request(await createApp({
|
||||
type: "agent",
|
||||
agentId: "55555555-5555-4555-8555-555555555555",
|
||||
companyId: "company-1",
|
||||
runId: "run-1",
|
||||
}))
|
||||
const res = await request(await createApp(assignedAgentActor()))
|
||||
.post("/api/companies/company-1/skills/import")
|
||||
.send({ source: "https://packages.example.com/skill.tgz" });
|
||||
|
||||
@@ -921,13 +877,7 @@ describe("company skill mutation permissions", () => {
|
||||
it("rejects secret-bearing remote import URLs without echoing the secret", async () => {
|
||||
const source = "https://github.com/acme/private-skill?token=secret#token=secret";
|
||||
|
||||
const res = await request(await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
companyIds: ["company-1"],
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: false,
|
||||
}))
|
||||
const res = await request(await createApp(boardActor()))
|
||||
.post("/api/companies/company-1/skills/import")
|
||||
.send({ source });
|
||||
|
||||
@@ -941,13 +891,7 @@ describe("company skill mutation permissions", () => {
|
||||
});
|
||||
|
||||
it("rejects malformed remote import URLs before policy evaluation", async () => {
|
||||
const res = await request(await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
companyIds: ["company-1"],
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: false,
|
||||
}))
|
||||
const res = await request(await createApp(boardActor()))
|
||||
.post("/api/companies/company-1/skills/import")
|
||||
.send({ source: "https://" });
|
||||
|
||||
@@ -965,12 +909,7 @@ describe("company skill mutation permissions", () => {
|
||||
"Low-trust agents cannot use company-wide skill APIs.",
|
||||
));
|
||||
|
||||
const res = await request(await createApp({
|
||||
type: "agent",
|
||||
agentId: "55555555-5555-4555-8555-555555555555",
|
||||
companyId: "company-1",
|
||||
runId: "run-1",
|
||||
}))
|
||||
const res = await request(await createApp(assignedAgentActor()))
|
||||
.post("/api/companies/company-1/skills/import")
|
||||
.send({ source: "https://github.com/vercel-labs/agent-browser" });
|
||||
|
||||
@@ -998,13 +937,7 @@ describe("company skill mutation permissions", () => {
|
||||
};
|
||||
});
|
||||
|
||||
const res = await request(await createApp({
|
||||
type: "board",
|
||||
userId: "board-user",
|
||||
companyIds: ["company-1"],
|
||||
source: "session",
|
||||
isInstanceAdmin: false,
|
||||
}))
|
||||
const res = await request(await createApp(boardActor("board-user", "session")))
|
||||
.post("/api/companies/company-1/skills/import")
|
||||
.send({ source: "vercel-labs/agent-browser" });
|
||||
|
||||
@@ -1042,13 +975,7 @@ describe("company skill mutation permissions", () => {
|
||||
}
|
||||
));
|
||||
|
||||
const res = await request(await createApp({
|
||||
type: "board",
|
||||
userId: "board-user",
|
||||
companyIds: ["company-1"],
|
||||
source: "session",
|
||||
isInstanceAdmin: false,
|
||||
}))
|
||||
const res = await request(await createApp(boardActor("board-user", "session")))
|
||||
.patch("/api/companies/company-1/skills/skill-1")
|
||||
.send({ name: "Updated review" });
|
||||
|
||||
@@ -1073,13 +1000,7 @@ describe("company skill mutation permissions", () => {
|
||||
});
|
||||
mockCompanySkillPolicyService.evaluate.mockResolvedValue(denySkillPolicy("skills.create"));
|
||||
|
||||
const res = await request(await createApp({
|
||||
type: "board",
|
||||
userId: "board-user",
|
||||
companyIds: ["company-1"],
|
||||
source: "session",
|
||||
isInstanceAdmin: false,
|
||||
}))
|
||||
const res = await request(await createApp(boardActor("board-user", "session")))
|
||||
.post("/api/companies/company-1/skills/skill-1/versions")
|
||||
.send({ label: "v1" });
|
||||
|
||||
@@ -1110,13 +1031,7 @@ describe("company skill mutation permissions", () => {
|
||||
};
|
||||
});
|
||||
|
||||
const res = await request(await createApp({
|
||||
type: "board",
|
||||
userId: "board-user",
|
||||
companyIds: ["company-1"],
|
||||
source: "session",
|
||||
isInstanceAdmin: false,
|
||||
}))
|
||||
const res = await request(await createApp(boardActor("board-user", "session")))
|
||||
.post("/api/companies/company-1/skills/import")
|
||||
.send({ source: "npx skills add Vercel-Labs/Agent-Browser --skill agent-browser -g" });
|
||||
|
||||
@@ -1160,13 +1075,7 @@ describe("company skill mutation permissions", () => {
|
||||
};
|
||||
});
|
||||
|
||||
const res = await request(await createApp({
|
||||
type: "board",
|
||||
userId: "board-user",
|
||||
companyIds: ["company-1"],
|
||||
source: "session",
|
||||
isInstanceAdmin: false,
|
||||
}))
|
||||
const res = await request(await createApp(boardActor("board-user", "session")))
|
||||
.post("/api/companies/company-1/skills/import")
|
||||
.send({ source });
|
||||
|
||||
@@ -1185,34 +1094,10 @@ describe("company skill mutation permissions", () => {
|
||||
|
||||
it("serves catalog listing without mutating company skills", async () => {
|
||||
mockCatalogService.listCatalogSkillsOrEmpty.mockReturnValue([
|
||||
{
|
||||
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",
|
||||
},
|
||||
createCatalogSkill(),
|
||||
]);
|
||||
|
||||
const res = await request(await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
companyIds: ["company-1"],
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: false,
|
||||
}))
|
||||
const res = await request(await createApp(boardActor()))
|
||||
.get("/api/skills/catalog?kind=bundled&q=review");
|
||||
|
||||
expect(res.status, JSON.stringify(res.body)).toBe(200);
|
||||
@@ -1267,13 +1152,7 @@ describe("company skill mutation permissions", () => {
|
||||
});
|
||||
|
||||
it("serves catalog detail and files by catalog reference", async () => {
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
companyIds: ["company-1"],
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: false,
|
||||
});
|
||||
const app = await createApp(boardActor());
|
||||
|
||||
const detail = await request(app)
|
||||
.get("/api/skills/catalog/review");
|
||||
@@ -1288,13 +1167,7 @@ describe("company skill mutation permissions", () => {
|
||||
});
|
||||
|
||||
it("installs catalog skills with mutation permissions and logs provenance", async () => {
|
||||
const res = await request(await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
companyIds: ["company-1"],
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: false,
|
||||
}))
|
||||
const res = await request(await createApp(boardActor()))
|
||||
.post("/api/companies/company-1/skills/install-catalog")
|
||||
.send({
|
||||
catalogSkillId: "paperclipai:bundled:software-development:review",
|
||||
@@ -1348,13 +1221,7 @@ describe("company skill mutation permissions", () => {
|
||||
warnings: [],
|
||||
});
|
||||
|
||||
const res = await request(await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
companyIds: ["company-1"],
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: false,
|
||||
}))
|
||||
const res = await request(await createApp(boardActor()))
|
||||
.post("/api/companies/company-1/skills/import")
|
||||
.send({ source: "https://github.com/vercel-labs/agent-browser" });
|
||||
|
||||
@@ -1486,13 +1353,7 @@ describe("company skill mutation permissions", () => {
|
||||
warnings: [],
|
||||
});
|
||||
|
||||
const res = await request(await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
companyIds: ["company-1"],
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: false,
|
||||
}))
|
||||
const res = await request(await createApp(boardActor()))
|
||||
.post("/api/companies/company-1/skills/import")
|
||||
.send({ source: "https://ghe.example.com/acme/private-skill" });
|
||||
|
||||
@@ -1528,13 +1389,7 @@ describe("company skill mutation permissions", () => {
|
||||
warnings: [],
|
||||
});
|
||||
|
||||
const res = await request(await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
companyIds: ["company-1"],
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: false,
|
||||
}))
|
||||
const res = await request(await createApp(boardActor()))
|
||||
.post("/api/companies/company-1/skills/import")
|
||||
.send({ source: "https://github.com/acme/private-skill" });
|
||||
|
||||
@@ -1553,12 +1408,7 @@ describe("company skill mutation permissions", () => {
|
||||
permissions: { canCreateSkills: false },
|
||||
});
|
||||
|
||||
const res = await request(await createApp({
|
||||
type: "agent",
|
||||
agentId: "55555555-5555-4555-8555-555555555555",
|
||||
companyId: "company-1",
|
||||
runId: "run-1",
|
||||
}))
|
||||
const res = await request(await createApp(assignedAgentActor()))
|
||||
.post("/api/companies/company-1/skills/import")
|
||||
.send({ source: "https://github.com/vercel-labs/agent-browser" });
|
||||
|
||||
@@ -1578,12 +1428,7 @@ describe("company skill mutation permissions", () => {
|
||||
permissions: { canCreateSkills: true },
|
||||
});
|
||||
|
||||
const res = await request(await createApp({
|
||||
type: "agent",
|
||||
agentId: "55555555-5555-4555-8555-555555555555",
|
||||
companyId: "company-1",
|
||||
runId: "run-1",
|
||||
}))
|
||||
const res = await request(await createApp(assignedAgentActor()))
|
||||
.post("/api/companies/company-2/skills/install-catalog")
|
||||
.send({ catalogSkillId: "paperclipai:bundled:software-development:review" });
|
||||
|
||||
@@ -1861,12 +1706,7 @@ describe("company skill mutation permissions", () => {
|
||||
permissions: { canCreateSkills: false },
|
||||
});
|
||||
|
||||
const res = await request(await createApp({
|
||||
type: "agent",
|
||||
agentId: "55555555-5555-4555-8555-555555555555",
|
||||
companyId: "company-1",
|
||||
runId: "run-1",
|
||||
}))
|
||||
const res = await request(await createApp(assignedAgentActor()))
|
||||
.post("/api/companies/company-1/skills/import")
|
||||
.send({ source: "https://github.com/vercel-labs/agent-browser" });
|
||||
|
||||
@@ -1906,26 +1746,7 @@ describe("company skill mutation permissions", () => {
|
||||
expect(mockCompanySkillService.createLocalSkill).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not invoke the legacy consent gate under the open default", async () => {
|
||||
mockAccessService.decide.mockResolvedValue(denySkillChangeDecision(
|
||||
"deny_missing_consent",
|
||||
"Permission skills:suggest-changes requires accepted change consent before applying this mutation.",
|
||||
));
|
||||
mockReflectionCoachMutationGate.assertConsented.mockRejectedValue(new Error("database unavailable"));
|
||||
|
||||
const res = await request(await createApp({
|
||||
type: "agent",
|
||||
agentId: "reflection-coach",
|
||||
companyId: "company-1",
|
||||
runId: "run-apply",
|
||||
}))
|
||||
.post("/api/companies/company-1/skills")
|
||||
.send({ name: "Reflection Draft", slug: "reflection-draft", markdown: "# Draft" });
|
||||
|
||||
expect(res.status, JSON.stringify(res.body)).toBe(201);
|
||||
expect(mockReflectionCoachMutationGate.assertConsented).not.toHaveBeenCalled();
|
||||
expect(mockCompanySkillService.createLocalSkill).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("allows suggest-tier skill mutations without a second consent decision", async () => {
|
||||
mockAccessService.decide
|
||||
@@ -1940,12 +1761,7 @@ describe("company skill mutation permissions", () => {
|
||||
permissions: {},
|
||||
});
|
||||
|
||||
const res = await request(await createApp({
|
||||
type: "agent",
|
||||
agentId: "55555555-5555-4555-8555-555555555555",
|
||||
companyId: "company-1",
|
||||
runId: "run-1",
|
||||
}))
|
||||
const res = await request(await createApp(assignedAgentActor()))
|
||||
.post("/api/companies/company-1/skills/import")
|
||||
.send({ source: "https://github.com/vercel-labs/agent-browser" });
|
||||
|
||||
@@ -1966,12 +1782,7 @@ describe("company skill mutation permissions", () => {
|
||||
permissions: {},
|
||||
});
|
||||
|
||||
const res = await request(await createApp({
|
||||
type: "agent",
|
||||
agentId: "55555555-5555-4555-8555-555555555555",
|
||||
companyId: "company-1",
|
||||
runId: "run-1",
|
||||
}))
|
||||
const res = await request(await createApp(assignedAgentActor()))
|
||||
.post("/api/companies/company-1/skills/import")
|
||||
.send({ source: "https://github.com/vercel-labs/agent-browser" });
|
||||
|
||||
@@ -2016,13 +1827,7 @@ describe("company skill mutation permissions", () => {
|
||||
});
|
||||
|
||||
it("routes skill test input CRUD through skills mutation permissions", async () => {
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
companyIds: ["company-1"],
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: false,
|
||||
});
|
||||
const app = await createApp(boardActor());
|
||||
|
||||
const created = await request(app)
|
||||
.post("/api/companies/company-1/skills/skill-1/test-inputs")
|
||||
@@ -2057,13 +1862,7 @@ describe("company skill mutation permissions", () => {
|
||||
});
|
||||
|
||||
it("routes skill test run template CRUD through skills mutation permissions", async () => {
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
companyIds: ["company-1"],
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: false,
|
||||
});
|
||||
const app = await createApp(boardActor());
|
||||
|
||||
const listed = await request(app).get("/api/companies/company-1/skill-test-run-templates");
|
||||
expect(listed.status, JSON.stringify(listed.body)).toBe(200);
|
||||
@@ -2124,28 +1923,7 @@ describe("company skill mutation permissions", () => {
|
||||
});
|
||||
await deps.wakeHarnessIssue("44444444-4444-4444-8444-444444444444", "55555555-5555-4555-8555-555555555555");
|
||||
return {
|
||||
id: "22222222-2222-4222-8222-222222222222",
|
||||
companyId: "company-1",
|
||||
skillId: "skill-1",
|
||||
inputId: "11111111-1111-4111-8111-111111111111",
|
||||
inputSnapshot: "Try the skill",
|
||||
skillVersionId: "33333333-3333-4333-8333-333333333333",
|
||||
agentId: "55555555-5555-4555-8555-555555555555",
|
||||
agentConfigSnapshot: { adapterType: "codex_local" },
|
||||
issueId: "44444444-4444-4444-8444-444444444444",
|
||||
templateId: "built-in:default-test-template",
|
||||
templateName: "Default test template",
|
||||
templateBody: "Default {{skillName}}",
|
||||
renderedTemplateBody: "Default Review",
|
||||
harnessIssueDescription: "Try the skill\n\n---\n\nDefault Review",
|
||||
status: "queued",
|
||||
outputDocumentKey: "output",
|
||||
outputSnapshot: "",
|
||||
error: null,
|
||||
deletedAt: null,
|
||||
supersededAt: null,
|
||||
harnessIssueExpiresAt: null,
|
||||
harnessIssueDeletedAt: null,
|
||||
...createSkillTestRunDetails(),
|
||||
createdAt: new Date("2026-05-26T00:00:00.000Z"),
|
||||
updatedAt: new Date("2026-05-26T00:00:00.000Z"),
|
||||
cost: { costCents: 0, inputTokens: 0, cachedInputTokens: 0, outputTokens: 0 },
|
||||
@@ -2160,28 +1938,7 @@ describe("company skill mutation permissions", () => {
|
||||
) => {
|
||||
await deps.cancelHarnessIssue("44444444-4444-4444-8444-444444444444");
|
||||
return {
|
||||
id: "22222222-2222-4222-8222-222222222222",
|
||||
companyId: "company-1",
|
||||
skillId: "skill-1",
|
||||
inputId: "11111111-1111-4111-8111-111111111111",
|
||||
inputSnapshot: "Try the skill",
|
||||
skillVersionId: "33333333-3333-4333-8333-333333333333",
|
||||
agentId: "55555555-5555-4555-8555-555555555555",
|
||||
agentConfigSnapshot: { adapterType: "codex_local" },
|
||||
issueId: "44444444-4444-4444-8444-444444444444",
|
||||
templateId: "built-in:default-test-template",
|
||||
templateName: "Default test template",
|
||||
templateBody: "Default {{skillName}}",
|
||||
renderedTemplateBody: "Default Review",
|
||||
harnessIssueDescription: "Try the skill\n\n---\n\nDefault Review",
|
||||
status: "cancelled",
|
||||
outputDocumentKey: "output",
|
||||
outputSnapshot: "",
|
||||
error: "Cancelled by operator",
|
||||
deletedAt: null,
|
||||
supersededAt: null,
|
||||
harnessIssueExpiresAt: null,
|
||||
harnessIssueDeletedAt: null,
|
||||
...createSkillTestRunDetails({ status: "cancelled", error: "Cancelled by operator" }),
|
||||
createdAt: new Date("2026-05-26T00:00:00.000Z"),
|
||||
updatedAt: new Date("2026-05-26T00:01:00.000Z"),
|
||||
cost: { costCents: 0, inputTokens: 0, cachedInputTokens: 0, outputTokens: 0 },
|
||||
@@ -2189,13 +1946,7 @@ describe("company skill mutation permissions", () => {
|
||||
};
|
||||
});
|
||||
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
companyIds: ["company-1"],
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: false,
|
||||
});
|
||||
const app = await createApp(boardActor());
|
||||
|
||||
const created = await request(app)
|
||||
.post("/api/companies/company-1/skills/skill-1/test-runs")
|
||||
@@ -2282,13 +2033,7 @@ describe("company skill mutation permissions", () => {
|
||||
harnessContent: { available: false, unavailableReason: "expired", documents: [], attachments: [], workProducts: [] },
|
||||
});
|
||||
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
companyIds: ["company-1"],
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: false,
|
||||
});
|
||||
const app = await createApp(boardActor());
|
||||
|
||||
const listed = await request(app)
|
||||
.get("/api/companies/company-1/skills/skill-1/test-runs");
|
||||
@@ -2322,28 +2067,7 @@ describe("company skill mutation permissions", () => {
|
||||
) => {
|
||||
await deps.hideHarnessIssue("44444444-4444-4444-8444-444444444444");
|
||||
return {
|
||||
id: "22222222-2222-4222-8222-222222222222",
|
||||
companyId: "company-1",
|
||||
skillId: "skill-1",
|
||||
inputId: null,
|
||||
inputSnapshot: "Try the skill",
|
||||
skillVersionId: "33333333-3333-4333-8333-333333333333",
|
||||
agentId: "55555555-5555-4555-8555-555555555555",
|
||||
agentConfigSnapshot: { adapterType: "codex_local" },
|
||||
issueId: "44444444-4444-4444-8444-444444444444",
|
||||
templateId: "built-in:default-test-template",
|
||||
templateName: "Default test template",
|
||||
templateBody: "Default {{skillName}}",
|
||||
renderedTemplateBody: "Default Review",
|
||||
harnessIssueDescription: "Try the skill\n\n---\n\nDefault Review",
|
||||
status: "succeeded",
|
||||
outputDocumentKey: "output",
|
||||
outputSnapshot: "",
|
||||
error: null,
|
||||
deletedAt: new Date("2026-05-26T00:02:00.000Z"),
|
||||
supersededAt: null,
|
||||
harnessIssueExpiresAt: null,
|
||||
harnessIssueDeletedAt: null,
|
||||
...createSkillTestRunDetails({ inputId: null, status: "succeeded", deletedAt: new Date("2026-05-26T00:02:00.000Z") }),
|
||||
createdAt: new Date("2026-05-26T00:00:00.000Z"),
|
||||
updatedAt: new Date("2026-05-26T00:02:00.000Z"),
|
||||
cost: { costCents: 0, inputTokens: 0, cachedInputTokens: 0, outputTokens: 0 },
|
||||
@@ -2351,13 +2075,7 @@ describe("company skill mutation permissions", () => {
|
||||
};
|
||||
});
|
||||
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
companyIds: ["company-1"],
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: false,
|
||||
});
|
||||
const app = await createApp(boardActor());
|
||||
|
||||
const deleted = await request(app)
|
||||
.delete("/api/companies/company-1/skills/skill-1/test-runs/22222222-2222-4222-8222-222222222222");
|
||||
@@ -2371,13 +2089,7 @@ describe("company skill mutation permissions", () => {
|
||||
|
||||
it("returns 404 when deleting a missing test run", async () => {
|
||||
mockCompanySkillService.deleteTestRun.mockResolvedValueOnce(null);
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
companyIds: ["company-1"],
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: false,
|
||||
});
|
||||
const app = await createApp(boardActor());
|
||||
const res = await request(app)
|
||||
.delete("/api/companies/company-1/skills/skill-1/test-runs/22222222-2222-4222-8222-222222222222");
|
||||
expect(res.status).toBe(404);
|
||||
@@ -2391,13 +2103,7 @@ describe("company skill mutation permissions", () => {
|
||||
);
|
||||
});
|
||||
|
||||
const res = await request(await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
companyIds: ["company-1"],
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: false,
|
||||
}))
|
||||
const res = await request(await createApp(boardActor()))
|
||||
.delete("/api/companies/company-1/skills/skill-1");
|
||||
|
||||
expect(res.status, JSON.stringify(res.body)).toBe(422);
|
||||
|
||||
@@ -149,7 +149,6 @@ describe("paperclip runner native dev prerequisite", () => {
|
||||
fs.utimesSync(target, current, current);
|
||||
const input = { repoRoot: checkout.root, nativeRunnerRequired: true, platform: "darwin", architecture };
|
||||
expect(paperclipRunnerBinaryNeedsBuild(input)).toBe(false);
|
||||
// A newer Linux build-host output cannot mask the remote Mac artifact.
|
||||
fs.writeFileSync(checkout.binary, nativeHeader("linux", "x64"));
|
||||
const wrongHostNewer = new Date(Date.now() + 4000);
|
||||
fs.utimesSync(checkout.binary, wrongHostNewer, wrongHostNewer);
|
||||
|
||||
@@ -223,11 +223,7 @@ function createDeleteBlastRadius(overrides: Partial<{
|
||||
}
|
||||
|
||||
let server: Server | null = null;
|
||||
let currentActor: Record<string, unknown> = {
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
};
|
||||
let currentActor: Record<string, unknown> = localBoardActor();
|
||||
const routeOptions: Record<string, unknown> = {};
|
||||
const originalSecretsProviderEnv = process.env.PAPERCLIP_SECRETS_PROVIDER;
|
||||
|
||||
@@ -260,6 +256,67 @@ function createApp(actor: Record<string, unknown>, options: Record<string, unkno
|
||||
return server;
|
||||
}
|
||||
|
||||
function localBoardActor(userId = "user-1") {
|
||||
return {
|
||||
type: "board",
|
||||
userId: userId,
|
||||
source: "local_implicit",
|
||||
};
|
||||
}
|
||||
|
||||
function createSecurePluginDriver() {
|
||||
return {
|
||||
driverKey: "secure-plugin",
|
||||
kind: "sandbox_provider",
|
||||
displayName: "Secure Sandbox",
|
||||
configSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
template: { type: "string" },
|
||||
apiKey: { type: "string", format: "secret-ref" },
|
||||
timeoutMs: { type: "number" },
|
||||
reuseLease: { type: "boolean" },
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createSecretBackedSshEnvironment() {
|
||||
return {
|
||||
...createEnvironment(),
|
||||
id: "env-ssh",
|
||||
name: "SSH Fixture",
|
||||
driver: "ssh" as const,
|
||||
config: {
|
||||
host: "ssh.example.test",
|
||||
port: 22,
|
||||
username: "ssh-user",
|
||||
remoteWorkspacePath: "/srv/paperclip/workspace",
|
||||
privateKey: null,
|
||||
privateKeySecretRef: {
|
||||
type: "secret_ref",
|
||||
secretId: "11111111-1111-1111-1111-111111111111",
|
||||
version: "latest",
|
||||
},
|
||||
knownHosts: null,
|
||||
strictHostKeyChecking: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createSshConfig() {
|
||||
return {
|
||||
host: "ssh.example.test",
|
||||
port: 22,
|
||||
username: "ssh-user",
|
||||
remoteWorkspacePath: "/srv/paperclip/workspace",
|
||||
privateKey: null,
|
||||
privateKeySecretRef: null,
|
||||
knownHosts: null,
|
||||
strictHostKeyChecking: true,
|
||||
};
|
||||
}
|
||||
|
||||
describe("environment routes", () => {
|
||||
afterAll(async () => {
|
||||
if (originalSecretsProviderEnv === undefined) {
|
||||
@@ -369,20 +426,7 @@ describe("environment routes", () => {
|
||||
? {
|
||||
pluginId: "plugin-secure",
|
||||
pluginKey: "acme.secure-sandbox-provider",
|
||||
driver: {
|
||||
driverKey: "secure-plugin",
|
||||
kind: "sandbox_provider",
|
||||
displayName: "Secure Sandbox",
|
||||
configSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
template: { type: "string" },
|
||||
apiKey: { type: "string", format: "secret-ref" },
|
||||
timeoutMs: { type: "number" },
|
||||
reuseLease: { type: "boolean" },
|
||||
},
|
||||
},
|
||||
},
|
||||
driver: createSecurePluginDriver(),
|
||||
}
|
||||
: null
|
||||
));
|
||||
@@ -401,11 +445,7 @@ describe("environment routes", () => {
|
||||
|
||||
it("lists instance-scoped environments through the company route alias", async () => {
|
||||
mockEnvironmentService.list.mockResolvedValue([createEnvironment()]);
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor());
|
||||
|
||||
const res = await request(app).get("/api/companies/company-1/environments?driver=local");
|
||||
|
||||
@@ -718,15 +758,7 @@ describe("environment routes", () => {
|
||||
expect(mockEnvironmentService.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("still rejects deletion of the managed sandbox row", async () => {
|
||||
mockEnvironmentService.getById.mockResolvedValue(createPlatformSandboxEnvironment());
|
||||
const app = createApp(ownerAdminActor);
|
||||
|
||||
const res = await request(app).delete("/api/environments/env-managed-1");
|
||||
|
||||
expect(res.status).toBe(403);
|
||||
expect(res.body.details).toMatchObject({ code: "environment_platform_managed" });
|
||||
});
|
||||
|
||||
it("hides the local environment from every read surface under managed-sandbox-only", async () => {
|
||||
mockInstanceSettingsService.getExperimental.mockResolvedValue({ enableManagedSandboxOnly: true });
|
||||
@@ -1173,11 +1205,7 @@ describe("environment routes", () => {
|
||||
});
|
||||
|
||||
it("returns provider capabilities for the company", async () => {
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor());
|
||||
|
||||
const res = await request(app).get("/api/companies/company-1/environments/capabilities");
|
||||
|
||||
@@ -1213,11 +1241,7 @@ describe("environment routes", () => {
|
||||
},
|
||||
},
|
||||
]);
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor());
|
||||
|
||||
const res = await request(app).get("/api/companies/company-1/environments/capabilities");
|
||||
|
||||
@@ -1271,11 +1295,7 @@ describe("environment routes", () => {
|
||||
configSchema: { type: "object", properties: {} },
|
||||
},
|
||||
]);
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor());
|
||||
|
||||
const res = await request(app).get("/api/companies/company-1/environments/capabilities");
|
||||
|
||||
@@ -1295,11 +1315,7 @@ describe("environment routes", () => {
|
||||
configSchema: { type: "object", properties: {} },
|
||||
},
|
||||
]);
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor());
|
||||
|
||||
const res = await request(app).get("/api/companies/company-1/environments/capabilities");
|
||||
|
||||
@@ -1322,11 +1338,7 @@ describe("environment routes", () => {
|
||||
configSchema: { type: "object", properties: {} },
|
||||
},
|
||||
]);
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor());
|
||||
|
||||
const res = await request(app).get("/api/companies/company-1/environments/capabilities");
|
||||
|
||||
@@ -1384,11 +1396,7 @@ describe("environment routes", () => {
|
||||
it("creates an environment and logs activity", async () => {
|
||||
const environment = createEnvironment();
|
||||
mockEnvironmentService.create.mockResolvedValue(environment);
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor());
|
||||
|
||||
const res = await request(app)
|
||||
.post("/api/companies/company-1/environments")
|
||||
@@ -1425,11 +1433,7 @@ describe("environment routes", () => {
|
||||
|
||||
it("returns conflict when creating a second local environment", async () => {
|
||||
mockEnvironmentService.list.mockResolvedValue([createEnvironment()]);
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor());
|
||||
|
||||
const res = await request(app)
|
||||
.post("/api/companies/company-1/environments")
|
||||
@@ -1529,11 +1533,7 @@ describe("environment routes", () => {
|
||||
mockEnvironmentService.getDeleteBlastRadius.mockResolvedValue(createDeleteBlastRadius({
|
||||
isManagedLocal: true,
|
||||
}));
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "admin-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor("admin-1"));
|
||||
|
||||
const res = await request(app).delete("/api/environments/env-1");
|
||||
|
||||
@@ -1549,26 +1549,13 @@ describe("environment routes", () => {
|
||||
...createEnvironment(),
|
||||
driver: "ssh" as const,
|
||||
name: "SSH Fixture",
|
||||
config: {
|
||||
host: "ssh.example.test",
|
||||
port: 22,
|
||||
username: "ssh-user",
|
||||
remoteWorkspacePath: "/srv/paperclip/workspace",
|
||||
privateKey: null,
|
||||
privateKeySecretRef: null,
|
||||
knownHosts: null,
|
||||
strictHostKeyChecking: true,
|
||||
},
|
||||
config: createSshConfig(),
|
||||
};
|
||||
mockEnvironmentService.getById.mockResolvedValue(environment);
|
||||
mockEnvironmentService.getDeleteBlastRadius.mockResolvedValue(createDeleteBlastRadius({
|
||||
isInstanceDefault: true,
|
||||
}));
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "admin-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor("admin-1"));
|
||||
|
||||
const res = await request(app).delete("/api/environments/env-1");
|
||||
|
||||
@@ -1585,26 +1572,13 @@ describe("environment routes", () => {
|
||||
...createEnvironment(),
|
||||
driver: "ssh" as const,
|
||||
name: "SSH Fixture",
|
||||
config: {
|
||||
host: "ssh.example.test",
|
||||
port: 22,
|
||||
username: "ssh-user",
|
||||
remoteWorkspacePath: "/srv/paperclip/workspace",
|
||||
privateKey: null,
|
||||
privateKeySecretRef: null,
|
||||
knownHosts: null,
|
||||
strictHostKeyChecking: true,
|
||||
},
|
||||
config: createSshConfig(),
|
||||
};
|
||||
mockEnvironmentService.getById.mockResolvedValue(environment);
|
||||
mockEnvironmentService.getDeleteBlastRadius.mockResolvedValue(createDeleteBlastRadius({
|
||||
pendingCleanupLeaseCount: 1,
|
||||
}));
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "admin-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor("admin-1"));
|
||||
|
||||
const res = await request(app).delete("/api/environments/env-1");
|
||||
|
||||
@@ -1632,11 +1606,7 @@ describe("environment routes", () => {
|
||||
activeLeaseCount: 1,
|
||||
reusableSandboxLeaseCount: 1,
|
||||
}));
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "admin-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor("admin-1"));
|
||||
|
||||
const res = await request(app).delete("/api/environments/env-1");
|
||||
|
||||
@@ -1662,11 +1632,7 @@ describe("environment routes", () => {
|
||||
});
|
||||
mockEnvironmentService.removeIfDeletable.mockResolvedValue(environment);
|
||||
mockInstanceSettingsService.listCompanyIds.mockResolvedValue(["company-1"]);
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "admin-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor("admin-1"));
|
||||
|
||||
const res = await request(app).delete("/api/environments/env-1?destroyReusableSandboxLeases=true");
|
||||
|
||||
@@ -1696,11 +1662,7 @@ describe("environment routes", () => {
|
||||
isInstanceDefault: true,
|
||||
reusableSandboxLeaseCount: 1,
|
||||
}));
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "admin-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor("admin-1"));
|
||||
|
||||
const res = await request(app).delete("/api/environments/env-1?destroyReusableSandboxLeases=true");
|
||||
|
||||
@@ -1722,11 +1684,7 @@ describe("environment routes", () => {
|
||||
failed: 1,
|
||||
skippedLiveRun: 0,
|
||||
});
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "admin-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor("admin-1"));
|
||||
|
||||
const res = await request(app).delete("/api/environments/env-1?destroyReusableSandboxLeases=true");
|
||||
|
||||
@@ -1746,24 +1704,11 @@ describe("environment routes", () => {
|
||||
id: "env-ssh",
|
||||
driver: "ssh" as const,
|
||||
name: "SSH Fixture",
|
||||
config: {
|
||||
host: "ssh.example.test",
|
||||
port: 22,
|
||||
username: "ssh-user",
|
||||
remoteWorkspacePath: "/srv/paperclip/workspace",
|
||||
privateKey: null,
|
||||
privateKeySecretRef: null,
|
||||
knownHosts: null,
|
||||
strictHostKeyChecking: true,
|
||||
},
|
||||
config: createSshConfig(),
|
||||
};
|
||||
mockEnvironmentService.getById.mockResolvedValue(environment);
|
||||
mockEnvironmentService.hasUnresolvedPendingCleanupLeases.mockResolvedValue(true);
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "admin-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor("admin-1"));
|
||||
|
||||
const res = await request(app)
|
||||
.patch("/api/environments/env-ssh")
|
||||
@@ -1790,25 +1735,12 @@ describe("environment routes", () => {
|
||||
id: "env-ssh",
|
||||
driver: "ssh" as const,
|
||||
name: "SSH Fixture",
|
||||
config: {
|
||||
host: "ssh.example.test",
|
||||
port: 22,
|
||||
username: "ssh-user",
|
||||
remoteWorkspacePath: "/srv/paperclip/workspace",
|
||||
privateKey: null,
|
||||
privateKeySecretRef: null,
|
||||
knownHosts: null,
|
||||
strictHostKeyChecking: true,
|
||||
},
|
||||
config: createSshConfig(),
|
||||
};
|
||||
mockEnvironmentService.getById.mockResolvedValue(environment);
|
||||
mockEnvironmentService.hasUnresolvedPendingCleanupLeases.mockResolvedValue(true);
|
||||
mockEnvironmentService.update.mockResolvedValue({ ...environment, description: "Updated" });
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "admin-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor("admin-1"));
|
||||
|
||||
const res = await request(app)
|
||||
.patch("/api/environments/env-ssh")
|
||||
@@ -1844,11 +1776,7 @@ describe("environment routes", () => {
|
||||
companyName: "Other Team",
|
||||
},
|
||||
]);
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "admin-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor("admin-1"));
|
||||
|
||||
const res = await request(app).get("/api/environments/env-sandbox/secret-refs");
|
||||
|
||||
@@ -1890,35 +1818,12 @@ describe("environment routes", () => {
|
||||
});
|
||||
|
||||
it("clears environment selections and secret bindings across all companies when deleting an environment", async () => {
|
||||
const environment = {
|
||||
...createEnvironment(),
|
||||
id: "env-ssh",
|
||||
name: "SSH Fixture",
|
||||
driver: "ssh" as const,
|
||||
config: {
|
||||
host: "ssh.example.test",
|
||||
port: 22,
|
||||
username: "ssh-user",
|
||||
remoteWorkspacePath: "/srv/paperclip/workspace",
|
||||
privateKey: null,
|
||||
privateKeySecretRef: {
|
||||
type: "secret_ref",
|
||||
secretId: "11111111-1111-1111-1111-111111111111",
|
||||
version: "latest",
|
||||
},
|
||||
knownHosts: null,
|
||||
strictHostKeyChecking: true,
|
||||
},
|
||||
};
|
||||
const environment = createSecretBackedSshEnvironment();
|
||||
mockEnvironmentService.getById.mockResolvedValue(environment);
|
||||
mockEnvironmentService.getDeleteBlastRadius.mockResolvedValue(createDeleteBlastRadius());
|
||||
mockEnvironmentService.removeIfDeletable.mockResolvedValue(environment);
|
||||
mockInstanceSettingsService.listCompanyIds.mockResolvedValue(["company-1", "company-2"]);
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "admin-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor("admin-1"));
|
||||
|
||||
const res = await request(app).delete("/api/environments/env-ssh");
|
||||
|
||||
@@ -1956,11 +1861,7 @@ describe("environment routes", () => {
|
||||
});
|
||||
|
||||
it("rejects invalid SSH config on create", async () => {
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor());
|
||||
|
||||
const res = await request(app)
|
||||
.post("/api/companies/company-1/environments")
|
||||
@@ -1979,33 +1880,10 @@ describe("environment routes", () => {
|
||||
});
|
||||
|
||||
it("normalizes SSH private keys into secret refs before persistence", async () => {
|
||||
const environment = {
|
||||
...createEnvironment(),
|
||||
id: "env-ssh",
|
||||
name: "SSH Fixture",
|
||||
driver: "ssh" as const,
|
||||
config: {
|
||||
host: "ssh.example.test",
|
||||
port: 22,
|
||||
username: "ssh-user",
|
||||
remoteWorkspacePath: "/srv/paperclip/workspace",
|
||||
privateKey: null,
|
||||
privateKeySecretRef: {
|
||||
type: "secret_ref",
|
||||
secretId: "11111111-1111-1111-1111-111111111111",
|
||||
version: "latest",
|
||||
},
|
||||
knownHosts: null,
|
||||
strictHostKeyChecking: true,
|
||||
},
|
||||
};
|
||||
const environment = createSecretBackedSshEnvironment();
|
||||
mockEnvironmentService.create.mockResolvedValue(environment);
|
||||
const pluginWorkerManager = {};
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
}, { pluginWorkerManager });
|
||||
const app = createApp(localBoardActor(), { pluginWorkerManager });
|
||||
|
||||
const res = await request(app)
|
||||
.post("/api/companies/company-1/environments")
|
||||
@@ -2045,32 +1923,9 @@ describe("environment routes", () => {
|
||||
|
||||
it("uses the configured provider for SSH private key secret materialization", async () => {
|
||||
process.env.PAPERCLIP_SECRETS_PROVIDER = "aws_secrets_manager";
|
||||
const environment = {
|
||||
...createEnvironment(),
|
||||
id: "env-ssh",
|
||||
name: "SSH Fixture",
|
||||
driver: "ssh" as const,
|
||||
config: {
|
||||
host: "ssh.example.test",
|
||||
port: 22,
|
||||
username: "ssh-user",
|
||||
remoteWorkspacePath: "/srv/paperclip/workspace",
|
||||
privateKey: null,
|
||||
privateKeySecretRef: {
|
||||
type: "secret_ref",
|
||||
secretId: "11111111-1111-1111-1111-111111111111",
|
||||
version: "latest",
|
||||
},
|
||||
knownHosts: null,
|
||||
strictHostKeyChecking: true,
|
||||
},
|
||||
};
|
||||
const environment = createSecretBackedSshEnvironment();
|
||||
mockEnvironmentService.create.mockResolvedValue(environment);
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor());
|
||||
|
||||
const res = await request(app)
|
||||
.post("/api/companies/company-1/environments")
|
||||
@@ -2097,11 +1952,7 @@ describe("environment routes", () => {
|
||||
});
|
||||
|
||||
it("rejects persisted fake sandbox environments", async () => {
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor());
|
||||
|
||||
const res = await request(app)
|
||||
.post("/api/companies/company-1/environments")
|
||||
@@ -2134,11 +1985,7 @@ describe("environment routes", () => {
|
||||
};
|
||||
mockEnvironmentService.create.mockResolvedValue(environment);
|
||||
const pluginWorkerManager = {};
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
}, { pluginWorkerManager });
|
||||
const app = createApp(localBoardActor(), { pluginWorkerManager });
|
||||
|
||||
const res = await request(app)
|
||||
.post("/api/companies/company-1/environments")
|
||||
@@ -2223,11 +2070,7 @@ describe("environment routes", () => {
|
||||
},
|
||||
);
|
||||
const pluginWorkerManager = {};
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
}, { pluginWorkerManager });
|
||||
const app = createApp(localBoardActor(), { pluginWorkerManager });
|
||||
|
||||
const res = await request(app)
|
||||
.post("/api/companies/company-1/environments")
|
||||
@@ -2277,27 +2120,10 @@ describe("environment routes", () => {
|
||||
},
|
||||
pluginId: "plugin-secure",
|
||||
pluginKey: "acme.secure-sandbox-provider",
|
||||
driver: {
|
||||
driverKey: "secure-plugin",
|
||||
kind: "sandbox_provider",
|
||||
displayName: "Secure Sandbox",
|
||||
configSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
template: { type: "string" },
|
||||
apiKey: { type: "string", format: "secret-ref" },
|
||||
timeoutMs: { type: "number" },
|
||||
reuseLease: { type: "boolean" },
|
||||
},
|
||||
},
|
||||
},
|
||||
driver: createSecurePluginDriver(),
|
||||
});
|
||||
const pluginWorkerManager = {};
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
}, { pluginWorkerManager });
|
||||
const app = createApp(localBoardActor(), { pluginWorkerManager });
|
||||
|
||||
const res = await request(app)
|
||||
.post("/api/companies/company-1/environments")
|
||||
@@ -2369,27 +2195,10 @@ describe("environment routes", () => {
|
||||
normalizedConfig: { ...config },
|
||||
pluginId: "plugin-secure",
|
||||
pluginKey: "acme.secure-sandbox-provider",
|
||||
driver: {
|
||||
driverKey: "secure-plugin",
|
||||
kind: "sandbox_provider",
|
||||
displayName: "Secure Sandbox",
|
||||
configSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
template: { type: "string" },
|
||||
apiKey: { type: "string", format: "secret-ref" },
|
||||
timeoutMs: { type: "number" },
|
||||
reuseLease: { type: "boolean" },
|
||||
},
|
||||
},
|
||||
},
|
||||
driver: createSecurePluginDriver(),
|
||||
}));
|
||||
const pluginWorkerManager = {};
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
}, { pluginWorkerManager });
|
||||
const app = createApp(localBoardActor(), { pluginWorkerManager });
|
||||
|
||||
const res = await request(app)
|
||||
.post("/api/companies/company-1/environments")
|
||||
@@ -2452,27 +2261,10 @@ describe("environment routes", () => {
|
||||
},
|
||||
pluginId: "plugin-secure",
|
||||
pluginKey: "acme.secure-sandbox-provider",
|
||||
driver: {
|
||||
driverKey: "secure-plugin",
|
||||
kind: "sandbox_provider",
|
||||
displayName: "Secure Sandbox",
|
||||
configSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
template: { type: "string" },
|
||||
apiKey: { type: "string", format: "secret-ref" },
|
||||
timeoutMs: { type: "number" },
|
||||
reuseLease: { type: "boolean" },
|
||||
},
|
||||
},
|
||||
},
|
||||
driver: createSecurePluginDriver(),
|
||||
});
|
||||
const pluginWorkerManager = {};
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
}, { pluginWorkerManager });
|
||||
const app = createApp(localBoardActor(), { pluginWorkerManager });
|
||||
|
||||
const res = await request(app)
|
||||
.post("/api/companies/company-1/environments")
|
||||
@@ -2516,11 +2308,7 @@ describe("environment routes", () => {
|
||||
mockValidatePluginEnvironmentDriverConfig.mockResolvedValue(environment.config);
|
||||
mockEnvironmentService.create.mockResolvedValue(environment);
|
||||
const pluginWorkerManager = {};
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
}, { pluginWorkerManager });
|
||||
const app = createApp(localBoardActor(), { pluginWorkerManager });
|
||||
|
||||
const res = await request(app)
|
||||
.post("/api/companies/company-1/environments")
|
||||
@@ -2604,11 +2392,7 @@ describe("environment routes", () => {
|
||||
updatedAt: new Date("2026-04-16T05:05:00.000Z"),
|
||||
},
|
||||
]);
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor());
|
||||
|
||||
const res = await request(app).get(`/api/environments/${environment.id}/leases?status=active`);
|
||||
|
||||
@@ -2640,11 +2424,7 @@ describe("environment routes", () => {
|
||||
createdAt: new Date("2026-04-16T05:00:00.000Z"),
|
||||
updatedAt: new Date("2026-04-16T05:05:00.000Z"),
|
||||
});
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor());
|
||||
|
||||
const res = await request(app).get("/api/environment-leases/lease-1");
|
||||
|
||||
@@ -2677,11 +2457,7 @@ describe("environment routes", () => {
|
||||
...environment,
|
||||
status: "archived",
|
||||
});
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor());
|
||||
|
||||
const res = await request(app)
|
||||
.patch(`/api/environments/${environment.id}?companyId=company-1`)
|
||||
@@ -2736,11 +2512,7 @@ describe("environment routes", () => {
|
||||
driver: "local" as const,
|
||||
config: {},
|
||||
});
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor());
|
||||
|
||||
const res = await request(app)
|
||||
.patch(`/api/environments/${environment.id}?companyId=company-1`)
|
||||
@@ -2783,30 +2555,13 @@ describe("environment routes", () => {
|
||||
normalizedConfig: { ...config },
|
||||
pluginId: "plugin-secure",
|
||||
pluginKey: "acme.secure-sandbox-provider",
|
||||
driver: {
|
||||
driverKey: "secure-plugin",
|
||||
kind: "sandbox_provider",
|
||||
displayName: "Secure Sandbox",
|
||||
configSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
template: { type: "string" },
|
||||
apiKey: { type: "string", format: "secret-ref" },
|
||||
timeoutMs: { type: "number" },
|
||||
reuseLease: { type: "boolean" },
|
||||
},
|
||||
},
|
||||
},
|
||||
driver: createSecurePluginDriver(),
|
||||
}));
|
||||
// The environment's only binding still lives in another company. Before
|
||||
// bindings moved with the referenced secret, this state made every save
|
||||
// from the caller's company fail, with no route-level way out.
|
||||
mockSecretService.listBindingCompanyIdsForTarget.mockResolvedValue(["company-old"]);
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
}, { pluginWorkerManager: {} });
|
||||
const app = createApp(localBoardActor(), { pluginWorkerManager: {} });
|
||||
|
||||
const res = await request(app)
|
||||
.patch("/api/environments/env-sandbox?companyId=company-new")
|
||||
@@ -2856,11 +2611,7 @@ describe("environment routes", () => {
|
||||
mockSecretService.replaceSecretRefsForInstanceTarget.mockRejectedValue(
|
||||
unprocessable("Secret referenced at apiKey was not found", { code: "secret_missing" }),
|
||||
);
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
}, { pluginWorkerManager: {} });
|
||||
const app = createApp(localBoardActor(), { pluginWorkerManager: {} });
|
||||
|
||||
const res = await request(app)
|
||||
.patch("/api/environments/env-sandbox?companyId=company-1")
|
||||
@@ -2876,11 +2627,7 @@ describe("environment routes", () => {
|
||||
|
||||
it("requires explicit SSH config when switching from local to SSH", async () => {
|
||||
mockEnvironmentService.getById.mockResolvedValue(createEnvironment());
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor());
|
||||
|
||||
const res = await request(app)
|
||||
.patch("/api/environments/env-1?companyId=company-1")
|
||||
@@ -2895,11 +2642,7 @@ describe("environment routes", () => {
|
||||
|
||||
it("rejects switching an environment to the built-in fake sandbox provider", async () => {
|
||||
mockEnvironmentService.getById.mockResolvedValue(createEnvironment());
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor());
|
||||
|
||||
const res = await request(app)
|
||||
.patch("/api/environments/env-1?companyId=company-1")
|
||||
@@ -2918,11 +2661,7 @@ describe("environment routes", () => {
|
||||
|
||||
it("returns 404 when patching a missing environment", async () => {
|
||||
mockEnvironmentService.getById.mockResolvedValue(null);
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "local_implicit",
|
||||
});
|
||||
const app = createApp(localBoardActor());
|
||||
|
||||
const res = await request(app)
|
||||
.patch("/api/environments/missing-env")
|
||||
@@ -3200,20 +2939,7 @@ describe("environment routes", () => {
|
||||
},
|
||||
pluginId: "plugin-secure",
|
||||
pluginKey: "acme.secure-sandbox-provider",
|
||||
driver: {
|
||||
driverKey: "secure-plugin",
|
||||
kind: "sandbox_provider",
|
||||
displayName: "Secure Sandbox",
|
||||
configSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
template: { type: "string" },
|
||||
apiKey: { type: "string", format: "secret-ref" },
|
||||
timeoutMs: { type: "number" },
|
||||
reuseLease: { type: "boolean" },
|
||||
},
|
||||
},
|
||||
},
|
||||
driver: createSecurePluginDriver(),
|
||||
});
|
||||
mockProbeEnvironment.mockResolvedValue({
|
||||
ok: true,
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -154,8 +154,6 @@ describeEmbeddedPostgres("environmentService leases", () => {
|
||||
config: { config: { ...config, image: `registry.example.test/runtime@sha256:${"b".repeat(64)}` } },
|
||||
driver: { driver: "ssh", config: { host: "fixture.example.test", username: "fixture" } },
|
||||
};
|
||||
// Interleave the real probe UPDATE after the save is prepared but before its
|
||||
// real UPDATE executes. This reproduces the stale route-read/write window.
|
||||
const delayedWriteDb = {
|
||||
update(table: typeof environments) {
|
||||
const prepared = db.update(table);
|
||||
|
||||
@@ -292,6 +292,104 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
|
||||
await tempDb?.cleanup();
|
||||
});
|
||||
|
||||
function insertBlockedSourceTask(issueId: string, companyId: string, projectId: string) {
|
||||
return db.insert(issues).values({
|
||||
id: issueId,
|
||||
companyId,
|
||||
projectId,
|
||||
title: "Source task",
|
||||
status: "blocked",
|
||||
priority: "medium",
|
||||
});
|
||||
}
|
||||
|
||||
function insertBoardWorkspaceRecovery(companyId: string, issueId: string, fingerprint: string) {
|
||||
return db.insert(issueRecoveryActions).values({
|
||||
companyId,
|
||||
sourceIssueId: issueId,
|
||||
kind: "workspace_validation",
|
||||
status: "active",
|
||||
ownerType: "board",
|
||||
cause: "workspace_validation_failed",
|
||||
fingerprint,
|
||||
evidence: {
|
||||
workspaceValidation: {
|
||||
fingerprint,
|
||||
},
|
||||
},
|
||||
nextAction: "Repair the source issue workspace link.",
|
||||
});
|
||||
}
|
||||
|
||||
function insertMissingRecordedBranchWorkspace(executionWorkspaceId: string, companyId: string, projectId: string, issueId: string, worktreePath: string) {
|
||||
return db.insert(executionWorkspaces).values({
|
||||
id: executionWorkspaceId,
|
||||
companyId,
|
||||
projectId,
|
||||
sourceIssueId: issueId,
|
||||
mode: "isolated_workspace",
|
||||
strategyType: "git_worktree",
|
||||
name: "feature/never-created",
|
||||
status: "idle",
|
||||
providerType: "git_worktree",
|
||||
cwd: worktreePath,
|
||||
providerRef: worktreePath,
|
||||
branchName: "feature/never-created",
|
||||
baseRef: "main",
|
||||
});
|
||||
}
|
||||
|
||||
function insertRecordedBranchWorkspace(executionWorkspaceId: string, companyId: string, projectId: string, projectWorkspaceId: string, issueId: string, worktreePath: string) {
|
||||
return db.insert(executionWorkspaces).values({
|
||||
id: executionWorkspaceId,
|
||||
companyId,
|
||||
projectId,
|
||||
projectWorkspaceId,
|
||||
sourceIssueId: issueId,
|
||||
mode: "isolated_workspace",
|
||||
strategyType: "git_worktree",
|
||||
name: "feature/recorded",
|
||||
status: "active",
|
||||
providerType: "git_worktree",
|
||||
cwd: worktreePath,
|
||||
providerRef: worktreePath,
|
||||
branchName: "feature/recorded",
|
||||
baseRef: "main",
|
||||
});
|
||||
}
|
||||
|
||||
function insertRunningWorkspaceService(runtimeServiceId: string, companyId: string, projectId: string, executionWorkspaceId: string, issueId: string, worktreePath: string) {
|
||||
return db.insert(workspaceRuntimeServices).values({
|
||||
id: runtimeServiceId,
|
||||
companyId,
|
||||
projectId,
|
||||
executionWorkspaceId,
|
||||
issueId,
|
||||
scopeType: "execution_workspace",
|
||||
serviceName: "web",
|
||||
status: "running",
|
||||
lifecycle: "shared",
|
||||
command: "pnpm dev",
|
||||
cwd: worktreePath,
|
||||
provider: "local_process",
|
||||
healthStatus: "healthy",
|
||||
});
|
||||
}
|
||||
|
||||
function insertActiveCodexCoder(agentId: string, companyId: string) {
|
||||
return db.insert(agents).values({
|
||||
id: agentId,
|
||||
companyId,
|
||||
name: "Codex Coder",
|
||||
role: "engineer",
|
||||
status: "active",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
});
|
||||
}
|
||||
|
||||
async function seedTerminalWorkspace(options: {
|
||||
mergedPr?: boolean;
|
||||
activeRun?: boolean;
|
||||
@@ -2202,21 +2300,7 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
|
||||
branchName: "feature/recorded",
|
||||
baseRef: "main",
|
||||
});
|
||||
await db.insert(issueRecoveryActions).values({
|
||||
companyId,
|
||||
sourceIssueId: issueId,
|
||||
kind: "workspace_validation",
|
||||
status: "active",
|
||||
ownerType: "board",
|
||||
cause: "workspace_validation_failed",
|
||||
fingerprint,
|
||||
evidence: {
|
||||
workspaceValidation: {
|
||||
fingerprint,
|
||||
},
|
||||
},
|
||||
nextAction: "Repair the source issue workspace link.",
|
||||
});
|
||||
await insertBoardWorkspaceRecovery(companyId, issueId, fingerprint);
|
||||
|
||||
const result = await svc.reconcileExecutionWorkspaceBranch(executionWorkspaceId, {
|
||||
mode: "forward",
|
||||
@@ -2309,21 +2393,7 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
|
||||
status: "blocked",
|
||||
priority: "medium",
|
||||
});
|
||||
await db.insert(executionWorkspaces).values({
|
||||
id: executionWorkspaceId,
|
||||
companyId,
|
||||
projectId,
|
||||
sourceIssueId: issueId,
|
||||
mode: "isolated_workspace",
|
||||
strategyType: "git_worktree",
|
||||
name: "feature/never-created",
|
||||
status: "idle",
|
||||
providerType: "git_worktree",
|
||||
cwd: worktreePath,
|
||||
providerRef: worktreePath,
|
||||
branchName: "feature/never-created",
|
||||
baseRef: "main",
|
||||
});
|
||||
await insertMissingRecordedBranchWorkspace(executionWorkspaceId, companyId, projectId, issueId, worktreePath);
|
||||
|
||||
const result = await svc.reconcileExecutionWorkspaceBranch(executionWorkspaceId, {
|
||||
mode: "forward",
|
||||
@@ -2390,21 +2460,7 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
|
||||
status: "blocked",
|
||||
priority: "medium",
|
||||
});
|
||||
await db.insert(executionWorkspaces).values({
|
||||
id: executionWorkspaceId,
|
||||
companyId,
|
||||
projectId,
|
||||
sourceIssueId: issueId,
|
||||
mode: "isolated_workspace",
|
||||
strategyType: "git_worktree",
|
||||
name: "feature/never-created",
|
||||
status: "idle",
|
||||
providerType: "git_worktree",
|
||||
cwd: worktreePath,
|
||||
providerRef: worktreePath,
|
||||
branchName: "feature/never-created",
|
||||
baseRef: "main",
|
||||
});
|
||||
await insertMissingRecordedBranchWorkspace(executionWorkspaceId, companyId, projectId, issueId, worktreePath);
|
||||
|
||||
await expect(svc.reconcileExecutionWorkspaceBranch(executionWorkspaceId, {
|
||||
mode: "forward",
|
||||
@@ -2463,21 +2519,7 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
|
||||
status: "blocked",
|
||||
priority: "medium",
|
||||
});
|
||||
await db.insert(executionWorkspaces).values({
|
||||
id: executionWorkspaceId,
|
||||
companyId,
|
||||
projectId,
|
||||
sourceIssueId: issueId,
|
||||
mode: "isolated_workspace",
|
||||
strategyType: "git_worktree",
|
||||
name: "feature/never-created",
|
||||
status: "idle",
|
||||
providerType: "git_worktree",
|
||||
cwd: worktreePath,
|
||||
providerRef: worktreePath,
|
||||
branchName: "feature/never-created",
|
||||
baseRef: "main",
|
||||
});
|
||||
await insertMissingRecordedBranchWorkspace(executionWorkspaceId, companyId, projectId, issueId, worktreePath);
|
||||
|
||||
await expect(svc.reconcileExecutionWorkspaceBranch(executionWorkspaceId, {
|
||||
mode: "forward",
|
||||
@@ -2527,17 +2569,7 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
|
||||
issuePrefix: "PAP",
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
await db.insert(agents).values({
|
||||
id: agentId,
|
||||
companyId,
|
||||
name: "Codex Coder",
|
||||
role: "engineer",
|
||||
status: "active",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
});
|
||||
await insertActiveCodexCoder(agentId, companyId);
|
||||
await db.insert(projects).values({
|
||||
id: projectId,
|
||||
companyId,
|
||||
@@ -2563,37 +2595,8 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
|
||||
priority: "medium",
|
||||
assigneeAgentId: agentId,
|
||||
});
|
||||
await db.insert(executionWorkspaces).values({
|
||||
id: executionWorkspaceId,
|
||||
companyId,
|
||||
projectId,
|
||||
projectWorkspaceId,
|
||||
sourceIssueId: issueId,
|
||||
mode: "isolated_workspace",
|
||||
strategyType: "git_worktree",
|
||||
name: "feature/recorded",
|
||||
status: "active",
|
||||
providerType: "git_worktree",
|
||||
cwd: worktreePath,
|
||||
providerRef: worktreePath,
|
||||
branchName: "feature/recorded",
|
||||
baseRef: "main",
|
||||
});
|
||||
await db.insert(issueRecoveryActions).values({
|
||||
companyId,
|
||||
sourceIssueId: issueId,
|
||||
kind: "workspace_validation",
|
||||
status: "active",
|
||||
ownerType: "board",
|
||||
cause: "workspace_validation_failed",
|
||||
fingerprint,
|
||||
evidence: {
|
||||
workspaceValidation: {
|
||||
fingerprint,
|
||||
},
|
||||
},
|
||||
nextAction: "Repair the source issue workspace link.",
|
||||
});
|
||||
await insertRecordedBranchWorkspace(executionWorkspaceId, companyId, projectId, projectWorkspaceId, issueId, worktreePath);
|
||||
await insertBoardWorkspaceRecovery(companyId, issueId, fingerprint);
|
||||
|
||||
const result = await svc.reconcileExecutionWorkspaceBranch(executionWorkspaceId, {
|
||||
mode: "quarantine_restore",
|
||||
@@ -2717,21 +2720,7 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
|
||||
branchName: "feature/recorded",
|
||||
baseRef: "main",
|
||||
});
|
||||
await db.insert(workspaceRuntimeServices).values({
|
||||
id: runtimeServiceId,
|
||||
companyId,
|
||||
projectId,
|
||||
executionWorkspaceId,
|
||||
issueId,
|
||||
scopeType: "execution_workspace",
|
||||
serviceName: "web",
|
||||
status: "running",
|
||||
lifecycle: "shared",
|
||||
command: "pnpm dev",
|
||||
cwd: worktreePath,
|
||||
provider: "local_process",
|
||||
healthStatus: "healthy",
|
||||
});
|
||||
await insertRunningWorkspaceService(runtimeServiceId, companyId, projectId, executionWorkspaceId, issueId, worktreePath);
|
||||
|
||||
await expect(svc.reconcileExecutionWorkspaceBranch(executionWorkspaceId, {
|
||||
mode: "quarantine_restore",
|
||||
@@ -2875,37 +2864,8 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
|
||||
lastDecisionOutcome: null,
|
||||
},
|
||||
});
|
||||
await db.insert(executionWorkspaces).values({
|
||||
id: executionWorkspaceId,
|
||||
companyId,
|
||||
projectId,
|
||||
projectWorkspaceId,
|
||||
sourceIssueId: issueId,
|
||||
mode: "isolated_workspace",
|
||||
strategyType: "git_worktree",
|
||||
name: "feature/recorded",
|
||||
status: "active",
|
||||
providerType: "git_worktree",
|
||||
cwd: worktreePath,
|
||||
providerRef: worktreePath,
|
||||
branchName: "feature/recorded",
|
||||
baseRef: "main",
|
||||
});
|
||||
await db.insert(issueRecoveryActions).values({
|
||||
companyId,
|
||||
sourceIssueId: issueId,
|
||||
kind: "workspace_validation",
|
||||
status: "active",
|
||||
ownerType: "board",
|
||||
cause: "workspace_validation_failed",
|
||||
fingerprint,
|
||||
evidence: {
|
||||
workspaceValidation: {
|
||||
fingerprint,
|
||||
},
|
||||
},
|
||||
nextAction: "Repair the source issue workspace link.",
|
||||
});
|
||||
await insertRecordedBranchWorkspace(executionWorkspaceId, companyId, projectId, projectWorkspaceId, issueId, worktreePath);
|
||||
await insertBoardWorkspaceRecovery(companyId, issueId, fingerprint);
|
||||
|
||||
const result = await svc.reconcileExecutionWorkspaceBranch(executionWorkspaceId, {
|
||||
mode: "quarantine_restore",
|
||||
@@ -2986,17 +2946,7 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
|
||||
issuePrefix: "PAP",
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
await db.insert(agents).values({
|
||||
id: agentId,
|
||||
companyId,
|
||||
name: "Codex Coder",
|
||||
role: "engineer",
|
||||
status: "active",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
});
|
||||
await insertActiveCodexCoder(agentId, companyId);
|
||||
await db.insert(projects).values({
|
||||
id: projectId,
|
||||
companyId,
|
||||
@@ -3159,14 +3109,7 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
|
||||
name: "Branch reconcile",
|
||||
status: "in_progress",
|
||||
});
|
||||
await db.insert(issues).values({
|
||||
id: issueId,
|
||||
companyId,
|
||||
projectId,
|
||||
title: "Source task",
|
||||
status: "blocked",
|
||||
priority: "medium",
|
||||
});
|
||||
await insertBlockedSourceTask(issueId, companyId, projectId);
|
||||
await db.insert(executionWorkspaces).values({
|
||||
id: executionWorkspaceId,
|
||||
companyId,
|
||||
@@ -3244,14 +3187,7 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
|
||||
name: "Branch reconcile",
|
||||
status: "in_progress",
|
||||
});
|
||||
await db.insert(issues).values({
|
||||
id: issueId,
|
||||
companyId,
|
||||
projectId,
|
||||
title: "Source task",
|
||||
status: "blocked",
|
||||
priority: "medium",
|
||||
});
|
||||
await insertBlockedSourceTask(issueId, companyId, projectId);
|
||||
await db.insert(executionWorkspaces).values({
|
||||
id: executionWorkspaceId,
|
||||
companyId,
|
||||
@@ -3329,14 +3265,7 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
|
||||
name: "Branch reconcile",
|
||||
status: "in_progress",
|
||||
});
|
||||
await db.insert(issues).values({
|
||||
id: issueId,
|
||||
companyId,
|
||||
projectId,
|
||||
title: "Source task",
|
||||
status: "blocked",
|
||||
priority: "medium",
|
||||
});
|
||||
await insertBlockedSourceTask(issueId, companyId, projectId);
|
||||
await db.insert(executionWorkspaces).values({
|
||||
id: executionWorkspaceId,
|
||||
companyId,
|
||||
@@ -3433,14 +3362,7 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
|
||||
name: "Branch reconcile",
|
||||
status: "in_progress",
|
||||
});
|
||||
await db.insert(issues).values({
|
||||
id: issueId,
|
||||
companyId,
|
||||
projectId,
|
||||
title: "Source task",
|
||||
status: "blocked",
|
||||
priority: "medium",
|
||||
});
|
||||
await insertBlockedSourceTask(issueId, companyId, projectId);
|
||||
await db.insert(executionWorkspaces).values({
|
||||
id: executionWorkspaceId,
|
||||
companyId,
|
||||
@@ -3456,21 +3378,7 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
|
||||
branchName: "feature/recorded",
|
||||
baseRef: "main",
|
||||
});
|
||||
await db.insert(workspaceRuntimeServices).values({
|
||||
id: runtimeServiceId,
|
||||
companyId,
|
||||
projectId,
|
||||
executionWorkspaceId,
|
||||
issueId,
|
||||
scopeType: "execution_workspace",
|
||||
serviceName: "web",
|
||||
status: "running",
|
||||
lifecycle: "shared",
|
||||
command: "pnpm dev",
|
||||
cwd: worktreePath,
|
||||
provider: "local_process",
|
||||
healthStatus: "healthy",
|
||||
});
|
||||
await insertRunningWorkspaceService(runtimeServiceId, companyId, projectId, executionWorkspaceId, issueId, worktreePath);
|
||||
|
||||
await expect(svc.reconcileExecutionWorkspaceBranch(executionWorkspaceId, {
|
||||
mode: "override",
|
||||
@@ -3540,14 +3448,7 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
|
||||
name: "Branch reconcile",
|
||||
status: "in_progress",
|
||||
});
|
||||
await db.insert(issues).values({
|
||||
id: issueId,
|
||||
companyId,
|
||||
projectId,
|
||||
title: "Source task",
|
||||
status: "blocked",
|
||||
priority: "medium",
|
||||
});
|
||||
await insertBlockedSourceTask(issueId, companyId, projectId);
|
||||
await db.insert(executionWorkspaces).values({
|
||||
id: executionWorkspaceId,
|
||||
companyId,
|
||||
@@ -3669,14 +3570,7 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
|
||||
name: "Branch reconcile",
|
||||
status: "in_progress",
|
||||
});
|
||||
await db.insert(issues).values({
|
||||
id: issueId,
|
||||
companyId,
|
||||
projectId,
|
||||
title: "Source task",
|
||||
status: "blocked",
|
||||
priority: "medium",
|
||||
});
|
||||
await insertBlockedSourceTask(issueId, companyId, projectId);
|
||||
await db.insert(executionWorkspaces).values({
|
||||
id: executionWorkspaceId,
|
||||
companyId,
|
||||
@@ -3852,14 +3746,7 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => {
|
||||
name: "Branch reconcile",
|
||||
status: "in_progress",
|
||||
});
|
||||
await db.insert(issues).values({
|
||||
id: issueId,
|
||||
companyId,
|
||||
projectId,
|
||||
title: "Source task",
|
||||
status: "blocked",
|
||||
priority: "medium",
|
||||
});
|
||||
await insertBlockedSourceTask(issueId, companyId, projectId);
|
||||
await db.insert(executionWorkspaces).values({
|
||||
id: executionWorkspaceId,
|
||||
companyId,
|
||||
|
||||
@@ -65,7 +65,7 @@ describe("isPrivateWebhookHttpRequest", () => {
|
||||
describe("isSecretSensitiveHttpRequest", () => {
|
||||
it("protects Slack registration, installation, recovery, and all callback methods", () => {
|
||||
for (const suffix of ["registration", "install", "resume"]) expect(isSecretSensitiveHttpRequest("POST", `/api/chat-endpoints/id/slack/${suffix}`)).toBe(true);
|
||||
for (const method of ["GET", "POST", "DELETE"]) for (const path of ["/api/chat-slack/oauth/callback?code=canary", "https://paperclip.test/API/CHAT-SLACK/OAUTH/callback?code=canary"])
|
||||
for (const method of ["GET", "POST", "DELETE"]) for (const path of ["/api/chat-slack/oauth/callback?code=canary", "/api/chat-slack/managed/oauth/callback?claim=canary", "/api/chat-slack/managed/oauth/callback?code=canary", "/api/chat-endpoints/endpoint/slack/managed/authorize", "/api/chat-endpoints/endpoint/slack/managed/provision", "https://paperclip.test/API/CHAT-SLACK/OAUTH/callback?code=canary"])
|
||||
expect(isSecretSensitiveHttpRequest(method, path)).toBe(true);
|
||||
});
|
||||
it("identifies credential-bearing chat setup mutations", () => {
|
||||
|
||||
@@ -68,6 +68,15 @@ const mockDb = {
|
||||
select: vi.fn(() => ({ from: () => Promise.resolve(mockCompanyRows) })),
|
||||
};
|
||||
|
||||
function localAdminActor() {
|
||||
return {
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: true,
|
||||
};
|
||||
}
|
||||
|
||||
describe("instance settings routes", () => {
|
||||
const routeModules = hoistModuleGraph(registerModuleMocks, async () => {
|
||||
const [{ errorHandler }, { instanceSettingsRoutes }] = await Promise.all([
|
||||
@@ -234,12 +243,7 @@ describe("instance settings routes", () => {
|
||||
});
|
||||
|
||||
it("allows local board users to read and update experimental settings", async () => {
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: true,
|
||||
});
|
||||
const app = await createApp(localAdminActor());
|
||||
|
||||
const getRes = await request(app).get("/api/instance/settings/experimental");
|
||||
expect(getRes.status).toBe(200);
|
||||
@@ -273,12 +277,7 @@ describe("instance settings routes", () => {
|
||||
}, 10_000);
|
||||
|
||||
it("does not expose the retired liveness auto-recovery endpoints", async () => {
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: true,
|
||||
});
|
||||
const app = await createApp(localAdminActor());
|
||||
|
||||
await request(app)
|
||||
.post("/api/instance/settings/experimental/issue-graph-liveness-auto-recovery/preview")
|
||||
@@ -303,12 +302,7 @@ describe("instance settings routes", () => {
|
||||
});
|
||||
|
||||
it("accepts the instance-wide Streamlined UI preference", async () => {
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: true,
|
||||
});
|
||||
const app = await createApp(localAdminActor());
|
||||
|
||||
await request(app)
|
||||
.patch("/api/instance/settings/experimental")
|
||||
@@ -321,12 +315,7 @@ describe("instance settings routes", () => {
|
||||
});
|
||||
|
||||
it("strips server-managed worktree run execution fields before updating experimental settings", async () => {
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: true,
|
||||
});
|
||||
const app = await createApp(localAdminActor());
|
||||
|
||||
await request(app)
|
||||
.patch("/api/instance/settings/experimental")
|
||||
@@ -343,12 +332,7 @@ describe("instance settings routes", () => {
|
||||
});
|
||||
|
||||
it("allows local board users to read and update the instance default environment", async () => {
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: true,
|
||||
});
|
||||
const app = await createApp(localAdminActor());
|
||||
|
||||
const getRes = await request(app).get("/api/instance/settings");
|
||||
expect(getRes.status).toBe(200);
|
||||
@@ -379,12 +363,7 @@ describe("instance settings routes", () => {
|
||||
envVars: {},
|
||||
metadata: { managedByPaperclip: true, managedDefaultStamped: true },
|
||||
});
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: true,
|
||||
});
|
||||
const app = await createApp(localAdminActor());
|
||||
|
||||
const patchRes = await request(app)
|
||||
.patch("/api/instance/settings")
|
||||
@@ -419,12 +398,7 @@ describe("instance settings routes", () => {
|
||||
metadata: { managedByPaperclip: true, managedDefaultStamped: true },
|
||||
});
|
||||
mockEnvironmentService.update.mockRejectedValue(new Error("metadata write failed"));
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: true,
|
||||
});
|
||||
const app = await createApp(localAdminActor());
|
||||
|
||||
const patchRes = await request(app)
|
||||
.patch("/api/instance/settings")
|
||||
@@ -436,12 +410,7 @@ describe("instance settings routes", () => {
|
||||
|
||||
it("rejects unknown defaultEnvironmentId values with 422", async () => {
|
||||
mockEnvironmentService.getById.mockResolvedValue(null);
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: true,
|
||||
});
|
||||
const app = await createApp(localAdminActor());
|
||||
|
||||
const res = await request(app)
|
||||
.patch("/api/instance/settings")
|
||||
@@ -453,12 +422,7 @@ describe("instance settings routes", () => {
|
||||
});
|
||||
|
||||
it("allows local board users to update guarded dev-server auto-restart", async () => {
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: true,
|
||||
});
|
||||
const app = await createApp(localAdminActor());
|
||||
|
||||
await request(app)
|
||||
.patch("/api/instance/settings/experimental")
|
||||
@@ -473,12 +437,7 @@ describe("instance settings routes", () => {
|
||||
});
|
||||
|
||||
it("allows local board users to update external object detection", async () => {
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: true,
|
||||
});
|
||||
const app = await createApp(localAdminActor());
|
||||
|
||||
await request(app)
|
||||
.patch("/api/instance/settings/experimental")
|
||||
@@ -491,12 +450,7 @@ describe("instance settings routes", () => {
|
||||
});
|
||||
|
||||
it("allows local board users to update built-in agents", async () => {
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: true,
|
||||
});
|
||||
const app = await createApp(localAdminActor());
|
||||
|
||||
await request(app)
|
||||
.patch("/api/instance/settings/experimental")
|
||||
@@ -509,12 +463,7 @@ describe("instance settings routes", () => {
|
||||
});
|
||||
|
||||
it("allows local board users to update the goals sidebar link", async () => {
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: true,
|
||||
});
|
||||
const app = await createApp(localAdminActor());
|
||||
|
||||
await request(app)
|
||||
.patch("/api/instance/settings/experimental")
|
||||
@@ -527,12 +476,7 @@ describe("instance settings routes", () => {
|
||||
});
|
||||
|
||||
it("allows local board users to update the server info debug view", async () => {
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: true,
|
||||
});
|
||||
const app = await createApp(localAdminActor());
|
||||
|
||||
await request(app)
|
||||
.patch("/api/instance/settings/experimental")
|
||||
@@ -545,12 +489,7 @@ describe("instance settings routes", () => {
|
||||
});
|
||||
|
||||
it("allows local board users to update environment controls", async () => {
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: true,
|
||||
});
|
||||
const app = await createApp(localAdminActor());
|
||||
|
||||
await request(app)
|
||||
.patch("/api/instance/settings/experimental")
|
||||
@@ -582,12 +521,7 @@ describe("instance settings routes", () => {
|
||||
});
|
||||
|
||||
it("allows local board users to read and update general settings", async () => {
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: true,
|
||||
});
|
||||
const app = await createApp(localAdminActor());
|
||||
|
||||
const getRes = await request(app).get("/api/instance/settings/general");
|
||||
expect(getRes.status).toBe(200);
|
||||
|
||||
@@ -321,6 +321,56 @@ async function waitForWakeup(assertion: () => void) {
|
||||
await vi.waitFor(assertion);
|
||||
}
|
||||
|
||||
function createAgentReviewPolicy(reviewerAgentId: string) {
|
||||
return normalizePolicy({
|
||||
stages: [
|
||||
{
|
||||
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
||||
type: "review",
|
||||
participants: [{ type: "agent", agentId: reviewerAgentId }],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
function createReviewIssue(policy: NonNullable<Awaited<ReturnType<typeof normalizePolicy>>>, reviewerAgentId: string) {
|
||||
return {
|
||||
...makeIssue("todo"),
|
||||
status: "in_review",
|
||||
assigneeAgentId: reviewerAgentId,
|
||||
executionPolicy: policy,
|
||||
executionState: {
|
||||
status: "pending",
|
||||
currentStageId: policy.stages[0].id,
|
||||
currentStageIndex: 0,
|
||||
currentStageType: "review",
|
||||
currentParticipant: { type: "agent", agentId: reviewerAgentId },
|
||||
returnAssignee: {
|
||||
type: "agent",
|
||||
agentId: "22222222-2222-4222-8222-222222222222",
|
||||
},
|
||||
completedStageIds: [],
|
||||
lastDecisionId: null,
|
||||
lastDecisionOutcome: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createScheduledRetry() {
|
||||
return {
|
||||
runId: "retry-run-1",
|
||||
status: "scheduled_retry",
|
||||
agentId: "22222222-2222-4222-8222-222222222222",
|
||||
agentName: "CodexCoder",
|
||||
retryOfRunId: "source-run-1",
|
||||
scheduledRetryAt: new Date("2026-05-18T14:00:00.000Z"),
|
||||
scheduledRetryAttempt: 1,
|
||||
scheduledRetryReason: "transient_failure",
|
||||
error: null,
|
||||
errorCode: null,
|
||||
};
|
||||
}
|
||||
|
||||
describe("issue comment reopen routes", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
@@ -956,7 +1006,7 @@ describe("issue comment reopen routes", () => {
|
||||
|
||||
// Same guard on cancelled status — explicit resume must use `resume: true`,
|
||||
// a log-class self-comment with `reopen: true` is not a reopen signal.
|
||||
it("does not reopen via POST comment+reopen when the assignee agent is the actor on a cancelled issue", async () => {
|
||||
it("rejects bare agent reopen requests on cancelled issues before writing a comment", async () => {
|
||||
const assigneeAgentId = "22222222-2222-4222-8222-222222222222";
|
||||
mockIssueService.getById.mockResolvedValue(makeIssue("cancelled"));
|
||||
mockIssueService.update.mockImplementation(
|
||||
@@ -985,14 +1035,13 @@ describe("issue comment reopen routes", () => {
|
||||
}),
|
||||
)
|
||||
.post("/api/issues/11111111-1111-4111-8111-111111111111/comments")
|
||||
// Cancelled issues reject explicit resume entirely, so only reopen=true
|
||||
// is observable here — the guard is what keeps it from flipping back.
|
||||
.send({ body: "log line", reopen: true });
|
||||
|
||||
// Cancelled issues are rejected at assertExplicitResumeIntentAllowed for
|
||||
// agent actors with reopen=true (409). The guard runs after that, but
|
||||
// either way no reopen wakeup must fire and no status update to todo.
|
||||
expect([200, 201, 409]).toContain(res.status);
|
||||
expect(res.status).toBe(409);
|
||||
expect(res.body.error).toBe(
|
||||
"Cancelled issues must be restored through the dedicated restore flow",
|
||||
);
|
||||
expect(mockIssueService.addComment).not.toHaveBeenCalled();
|
||||
expect(mockIssueService.update).not.toHaveBeenCalledWith(
|
||||
"11111111-1111-4111-8111-111111111111",
|
||||
expect.objectContaining({ status: "todo" }),
|
||||
@@ -1142,18 +1191,7 @@ describe("issue comment reopen routes", () => {
|
||||
executionRunId: "retry-run-1",
|
||||
};
|
||||
mockIssueService.getById.mockResolvedValue(issue);
|
||||
mockIssueService.getCurrentScheduledRetry.mockResolvedValue({
|
||||
runId: "retry-run-1",
|
||||
status: "scheduled_retry",
|
||||
agentId: "22222222-2222-4222-8222-222222222222",
|
||||
agentName: "CodexCoder",
|
||||
retryOfRunId: "source-run-1",
|
||||
scheduledRetryAt: new Date("2026-05-18T14:00:00.000Z"),
|
||||
scheduledRetryAttempt: 1,
|
||||
scheduledRetryReason: "transient_failure",
|
||||
error: null,
|
||||
errorCode: null,
|
||||
});
|
||||
mockIssueService.getCurrentScheduledRetry.mockResolvedValue(createScheduledRetry());
|
||||
mockIssueService.update.mockImplementation(
|
||||
async (_id: string, patch: Record<string, unknown>) => ({
|
||||
...issue,
|
||||
@@ -1214,18 +1252,7 @@ describe("issue comment reopen routes", () => {
|
||||
executionRunId: "retry-run-1",
|
||||
};
|
||||
mockIssueService.getById.mockResolvedValue(issue);
|
||||
mockIssueService.getCurrentScheduledRetry.mockResolvedValue({
|
||||
runId: "retry-run-1",
|
||||
status: "scheduled_retry",
|
||||
agentId: "22222222-2222-4222-8222-222222222222",
|
||||
agentName: "CodexCoder",
|
||||
retryOfRunId: "source-run-1",
|
||||
scheduledRetryAt: new Date("2026-05-18T14:00:00.000Z"),
|
||||
scheduledRetryAttempt: 1,
|
||||
scheduledRetryReason: "transient_failure",
|
||||
error: null,
|
||||
errorCode: null,
|
||||
});
|
||||
mockIssueService.getCurrentScheduledRetry.mockResolvedValue(createScheduledRetry());
|
||||
mockHeartbeatService.cancelRun.mockRejectedValue(
|
||||
new Error("cancel failed"),
|
||||
);
|
||||
@@ -1890,18 +1917,7 @@ describe("issue comment reopen routes", () => {
|
||||
executionRunId: "retry-run-1",
|
||||
};
|
||||
mockIssueService.getById.mockResolvedValue(issue);
|
||||
mockIssueService.getCurrentScheduledRetry.mockResolvedValue({
|
||||
runId: "retry-run-1",
|
||||
status: "scheduled_retry",
|
||||
agentId: "22222222-2222-4222-8222-222222222222",
|
||||
agentName: "CodexCoder",
|
||||
retryOfRunId: "source-run-1",
|
||||
scheduledRetryAt: new Date("2026-05-18T14:00:00.000Z"),
|
||||
scheduledRetryAttempt: 1,
|
||||
scheduledRetryReason: "transient_failure",
|
||||
error: null,
|
||||
errorCode: null,
|
||||
});
|
||||
mockIssueService.getCurrentScheduledRetry.mockResolvedValue(createScheduledRetry());
|
||||
mockIssueService.update.mockImplementation(
|
||||
async (_id: string, patch: Record<string, unknown>) => ({
|
||||
...issue,
|
||||
@@ -1950,18 +1966,7 @@ describe("issue comment reopen routes", () => {
|
||||
executionRunId: "retry-run-1",
|
||||
};
|
||||
mockIssueService.getById.mockResolvedValue(issue);
|
||||
mockIssueService.getCurrentScheduledRetry.mockResolvedValue({
|
||||
runId: "retry-run-1",
|
||||
status: "scheduled_retry",
|
||||
agentId: "22222222-2222-4222-8222-222222222222",
|
||||
agentName: "CodexCoder",
|
||||
retryOfRunId: "source-run-1",
|
||||
scheduledRetryAt: new Date("2026-05-18T14:00:00.000Z"),
|
||||
scheduledRetryAttempt: 1,
|
||||
scheduledRetryReason: "transient_failure",
|
||||
error: null,
|
||||
errorCode: null,
|
||||
});
|
||||
mockIssueService.getCurrentScheduledRetry.mockResolvedValue(createScheduledRetry());
|
||||
mockHeartbeatService.cancelRun.mockRejectedValue(
|
||||
new Error("cancel failed"),
|
||||
);
|
||||
@@ -2635,8 +2640,8 @@ describe("issue comment reopen routes", () => {
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["invalid", "wrong agent", "wrong company"])(
|
||||
"rejects comment and PATCH writes with a %s run",
|
||||
it(
|
||||
"rejects comment and PATCH writes when influence validation rejects the run",
|
||||
async () => {
|
||||
mockIssueService.getById.mockResolvedValue(makeIssue("todo"));
|
||||
mockObserveCrossIssueInfluence.mockRejectedValue(
|
||||
@@ -2940,35 +2945,8 @@ describe("issue comment reopen routes", () => {
|
||||
|
||||
it("auto-approves a reviewer comment with the APPROVED review marker", async () => {
|
||||
const reviewerAgentId = "33333333-3333-4333-8333-333333333333";
|
||||
const policy = await normalizePolicy({
|
||||
stages: [
|
||||
{
|
||||
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
||||
type: "review",
|
||||
participants: [{ type: "agent", agentId: reviewerAgentId }],
|
||||
},
|
||||
],
|
||||
})!;
|
||||
const issue = {
|
||||
...makeIssue("todo"),
|
||||
status: "in_review",
|
||||
assigneeAgentId: reviewerAgentId,
|
||||
executionPolicy: policy,
|
||||
executionState: {
|
||||
status: "pending",
|
||||
currentStageId: policy.stages[0].id,
|
||||
currentStageIndex: 0,
|
||||
currentStageType: "review",
|
||||
currentParticipant: { type: "agent", agentId: reviewerAgentId },
|
||||
returnAssignee: {
|
||||
type: "agent",
|
||||
agentId: "22222222-2222-4222-8222-222222222222",
|
||||
},
|
||||
completedStageIds: [],
|
||||
lastDecisionId: null,
|
||||
lastDecisionOutcome: null,
|
||||
},
|
||||
};
|
||||
const policy = await createAgentReviewPolicy(reviewerAgentId)!;
|
||||
const issue = createReviewIssue(policy, reviewerAgentId);
|
||||
const reviewBody = "## Review: PAP-580 - APPROVED\n\nLooks good.";
|
||||
mockIssueService.getById.mockResolvedValue(issue);
|
||||
mockIssueService.addComment.mockResolvedValue({
|
||||
@@ -3032,35 +3010,8 @@ describe("issue comment reopen routes", () => {
|
||||
|
||||
it("auto-approves a reviewer comment with structured review metadata", async () => {
|
||||
const reviewerAgentId = "33333333-3333-4333-8333-333333333333";
|
||||
const policy = await normalizePolicy({
|
||||
stages: [
|
||||
{
|
||||
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
||||
type: "review",
|
||||
participants: [{ type: "agent", agentId: reviewerAgentId }],
|
||||
},
|
||||
],
|
||||
})!;
|
||||
const issue = {
|
||||
...makeIssue("todo"),
|
||||
status: "in_review",
|
||||
assigneeAgentId: reviewerAgentId,
|
||||
executionPolicy: policy,
|
||||
executionState: {
|
||||
status: "pending",
|
||||
currentStageId: policy.stages[0].id,
|
||||
currentStageIndex: 0,
|
||||
currentStageType: "review",
|
||||
currentParticipant: { type: "agent", agentId: reviewerAgentId },
|
||||
returnAssignee: {
|
||||
type: "agent",
|
||||
agentId: "22222222-2222-4222-8222-222222222222",
|
||||
},
|
||||
completedStageIds: [],
|
||||
lastDecisionId: null,
|
||||
lastDecisionOutcome: null,
|
||||
},
|
||||
};
|
||||
const policy = await createAgentReviewPolicy(reviewerAgentId)!;
|
||||
const issue = createReviewIssue(policy, reviewerAgentId);
|
||||
const reviewBody = "kind: review\ndecision: approved\nsummary: ship it";
|
||||
mockIssueService.getById.mockResolvedValue(issue);
|
||||
mockIssueService.addComment.mockResolvedValue({
|
||||
@@ -3125,15 +3076,7 @@ describe("issue comment reopen routes", () => {
|
||||
it("auto-approves a reviewer comment and wakes dependents when the final blocker resolves", async () => {
|
||||
const reviewerAgentId = "33333333-3333-4333-8333-333333333333";
|
||||
const dependentAgentId = "44444444-4444-4444-8444-444444444444";
|
||||
const policy = await normalizePolicy({
|
||||
stages: [
|
||||
{
|
||||
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
||||
type: "review",
|
||||
participants: [{ type: "agent", agentId: reviewerAgentId }],
|
||||
},
|
||||
],
|
||||
})!;
|
||||
const policy = await createAgentReviewPolicy(reviewerAgentId)!;
|
||||
const issue = {
|
||||
...makeIssue("todo"),
|
||||
status: "in_review",
|
||||
@@ -3220,15 +3163,7 @@ describe("issue comment reopen routes", () => {
|
||||
it("does not wake the returnAssignee with issue_commented when auto-approval reassigns the issue", async () => {
|
||||
const reviewerAgentId = "33333333-3333-4333-8333-333333333333";
|
||||
const returnAssigneeAgentId = "22222222-2222-4222-8222-222222222222";
|
||||
const policy = await normalizePolicy({
|
||||
stages: [
|
||||
{
|
||||
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
||||
type: "review",
|
||||
participants: [{ type: "agent", agentId: reviewerAgentId }],
|
||||
},
|
||||
],
|
||||
})!;
|
||||
const policy = await createAgentReviewPolicy(reviewerAgentId)!;
|
||||
const issue = {
|
||||
...makeIssue("todo"),
|
||||
status: "in_review",
|
||||
@@ -3298,35 +3233,8 @@ describe("issue comment reopen routes", () => {
|
||||
|
||||
it("does not auto-approve APPROVED comments from a non-review participant", async () => {
|
||||
const reviewerAgentId = "33333333-3333-4333-8333-333333333333";
|
||||
const policy = await normalizePolicy({
|
||||
stages: [
|
||||
{
|
||||
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
||||
type: "review",
|
||||
participants: [{ type: "agent", agentId: reviewerAgentId }],
|
||||
},
|
||||
],
|
||||
})!;
|
||||
const issue = {
|
||||
...makeIssue("todo"),
|
||||
status: "in_review",
|
||||
assigneeAgentId: reviewerAgentId,
|
||||
executionPolicy: policy,
|
||||
executionState: {
|
||||
status: "pending",
|
||||
currentStageId: policy.stages[0].id,
|
||||
currentStageIndex: 0,
|
||||
currentStageType: "review",
|
||||
currentParticipant: { type: "agent", agentId: reviewerAgentId },
|
||||
returnAssignee: {
|
||||
type: "agent",
|
||||
agentId: "22222222-2222-4222-8222-222222222222",
|
||||
},
|
||||
completedStageIds: [],
|
||||
lastDecisionId: null,
|
||||
lastDecisionOutcome: null,
|
||||
},
|
||||
};
|
||||
const policy = await createAgentReviewPolicy(reviewerAgentId)!;
|
||||
const issue = createReviewIssue(policy, reviewerAgentId);
|
||||
const reviewBody = "## Review: PAP-580 - APPROVED\n\nLooks good.";
|
||||
mockIssueService.getById.mockResolvedValue(issue);
|
||||
mockIssueService.addComment.mockResolvedValue({
|
||||
@@ -3422,35 +3330,8 @@ describe("issue comment reopen routes", () => {
|
||||
// Even though both `kind: review` and `decision: approved` appear, a blank line between
|
||||
// them means they are not on truly consecutive lines, so the strict regex must reject it.
|
||||
const reviewerAgentId = "33333333-3333-4333-8333-333333333333";
|
||||
const policy = await normalizePolicy({
|
||||
stages: [
|
||||
{
|
||||
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
||||
type: "review",
|
||||
participants: [{ type: "agent", agentId: reviewerAgentId }],
|
||||
},
|
||||
],
|
||||
})!;
|
||||
const issue = {
|
||||
...makeIssue("todo"),
|
||||
status: "in_review",
|
||||
assigneeAgentId: reviewerAgentId,
|
||||
executionPolicy: policy,
|
||||
executionState: {
|
||||
status: "pending",
|
||||
currentStageId: policy.stages[0].id,
|
||||
currentStageIndex: 0,
|
||||
currentStageType: "review",
|
||||
currentParticipant: { type: "agent", agentId: reviewerAgentId },
|
||||
returnAssignee: {
|
||||
type: "agent",
|
||||
agentId: "22222222-2222-4222-8222-222222222222",
|
||||
},
|
||||
completedStageIds: [],
|
||||
lastDecisionId: null,
|
||||
lastDecisionOutcome: null,
|
||||
},
|
||||
};
|
||||
const policy = await createAgentReviewPolicy(reviewerAgentId)!;
|
||||
const issue = createReviewIssue(policy, reviewerAgentId);
|
||||
const reviewBody = "kind: review\n\ndecision: approved";
|
||||
mockIssueService.getById.mockResolvedValue(issue);
|
||||
mockIssueService.addComment.mockResolvedValue({
|
||||
@@ -3483,35 +3364,8 @@ describe("issue comment reopen routes", () => {
|
||||
|
||||
it("does not auto-approve reviewer comments without an approval marker", async () => {
|
||||
const reviewerAgentId = "33333333-3333-4333-8333-333333333333";
|
||||
const policy = await normalizePolicy({
|
||||
stages: [
|
||||
{
|
||||
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
||||
type: "review",
|
||||
participants: [{ type: "agent", agentId: reviewerAgentId }],
|
||||
},
|
||||
],
|
||||
})!;
|
||||
const issue = {
|
||||
...makeIssue("todo"),
|
||||
status: "in_review",
|
||||
assigneeAgentId: reviewerAgentId,
|
||||
executionPolicy: policy,
|
||||
executionState: {
|
||||
status: "pending",
|
||||
currentStageId: policy.stages[0].id,
|
||||
currentStageIndex: 0,
|
||||
currentStageType: "review",
|
||||
currentParticipant: { type: "agent", agentId: reviewerAgentId },
|
||||
returnAssignee: {
|
||||
type: "agent",
|
||||
agentId: "22222222-2222-4222-8222-222222222222",
|
||||
},
|
||||
completedStageIds: [],
|
||||
lastDecisionId: null,
|
||||
lastDecisionOutcome: null,
|
||||
},
|
||||
};
|
||||
const policy = await createAgentReviewPolicy(reviewerAgentId)!;
|
||||
const issue = createReviewIssue(policy, reviewerAgentId);
|
||||
const reviewBody = "Looks good.";
|
||||
mockIssueService.getById.mockResolvedValue(issue);
|
||||
mockIssueService.addComment.mockResolvedValue({
|
||||
@@ -3545,15 +3399,7 @@ describe("issue comment reopen routes", () => {
|
||||
|
||||
it("does not auto-approve approval comments outside the in_review status", async () => {
|
||||
const reviewerAgentId = "33333333-3333-4333-8333-333333333333";
|
||||
const policy = await normalizePolicy({
|
||||
stages: [
|
||||
{
|
||||
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
||||
type: "review",
|
||||
participants: [{ type: "agent", agentId: reviewerAgentId }],
|
||||
},
|
||||
],
|
||||
})!;
|
||||
const policy = await createAgentReviewPolicy(reviewerAgentId)!;
|
||||
const issue = {
|
||||
...makeIssue("todo"),
|
||||
status: "in_progress",
|
||||
@@ -3633,35 +3479,8 @@ describe("issue comment reopen routes", () => {
|
||||
])("does not auto-approve negated approval phrasings ($name)", ({ body }) => {
|
||||
it("rejects the auto-approval transition and keeps the comment as a regular comment", async () => {
|
||||
const reviewerAgentId = "33333333-3333-4333-8333-333333333333";
|
||||
const policy = await normalizePolicy({
|
||||
stages: [
|
||||
{
|
||||
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
||||
type: "review",
|
||||
participants: [{ type: "agent", agentId: reviewerAgentId }],
|
||||
},
|
||||
],
|
||||
})!;
|
||||
const issue = {
|
||||
...makeIssue("todo"),
|
||||
status: "in_review",
|
||||
assigneeAgentId: reviewerAgentId,
|
||||
executionPolicy: policy,
|
||||
executionState: {
|
||||
status: "pending",
|
||||
currentStageId: policy.stages[0].id,
|
||||
currentStageIndex: 0,
|
||||
currentStageType: "review",
|
||||
currentParticipant: { type: "agent", agentId: reviewerAgentId },
|
||||
returnAssignee: {
|
||||
type: "agent",
|
||||
agentId: "22222222-2222-4222-8222-222222222222",
|
||||
},
|
||||
completedStageIds: [],
|
||||
lastDecisionId: null,
|
||||
lastDecisionOutcome: null,
|
||||
},
|
||||
};
|
||||
const policy = await createAgentReviewPolicy(reviewerAgentId)!;
|
||||
const issue = createReviewIssue(policy, reviewerAgentId);
|
||||
mockIssueService.getById.mockResolvedValue(issue);
|
||||
mockIssueService.addComment.mockResolvedValue({
|
||||
id: "comment-review-negated",
|
||||
@@ -3705,35 +3524,8 @@ describe("issue comment reopen routes", () => {
|
||||
])("auto-approves positive approval phrasings ($name)", ({ body }) => {
|
||||
it("triggers the auto-approval transition", async () => {
|
||||
const reviewerAgentId = "33333333-3333-4333-8333-333333333333";
|
||||
const policy = await normalizePolicy({
|
||||
stages: [
|
||||
{
|
||||
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
||||
type: "review",
|
||||
participants: [{ type: "agent", agentId: reviewerAgentId }],
|
||||
},
|
||||
],
|
||||
})!;
|
||||
const issue = {
|
||||
...makeIssue("todo"),
|
||||
status: "in_review",
|
||||
assigneeAgentId: reviewerAgentId,
|
||||
executionPolicy: policy,
|
||||
executionState: {
|
||||
status: "pending",
|
||||
currentStageId: policy.stages[0].id,
|
||||
currentStageIndex: 0,
|
||||
currentStageType: "review",
|
||||
currentParticipant: { type: "agent", agentId: reviewerAgentId },
|
||||
returnAssignee: {
|
||||
type: "agent",
|
||||
agentId: "22222222-2222-4222-8222-222222222222",
|
||||
},
|
||||
completedStageIds: [],
|
||||
lastDecisionId: null,
|
||||
lastDecisionOutcome: null,
|
||||
},
|
||||
};
|
||||
const policy = await createAgentReviewPolicy(reviewerAgentId)!;
|
||||
const issue = createReviewIssue(policy, reviewerAgentId);
|
||||
mockIssueService.getById.mockResolvedValue(issue);
|
||||
mockIssueService.addComment.mockResolvedValue({
|
||||
id: "comment-review-positive",
|
||||
@@ -3794,35 +3586,8 @@ describe("issue comment reopen routes", () => {
|
||||
|
||||
it("rolls back the comment when the auto-approval status transition fails", async () => {
|
||||
const reviewerAgentId = "33333333-3333-4333-8333-333333333333";
|
||||
const policy = await normalizePolicy({
|
||||
stages: [
|
||||
{
|
||||
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
||||
type: "review",
|
||||
participants: [{ type: "agent", agentId: reviewerAgentId }],
|
||||
},
|
||||
],
|
||||
})!;
|
||||
const issue = {
|
||||
...makeIssue("todo"),
|
||||
status: "in_review",
|
||||
assigneeAgentId: reviewerAgentId,
|
||||
executionPolicy: policy,
|
||||
executionState: {
|
||||
status: "pending",
|
||||
currentStageId: policy.stages[0].id,
|
||||
currentStageIndex: 0,
|
||||
currentStageType: "review",
|
||||
currentParticipant: { type: "agent", agentId: reviewerAgentId },
|
||||
returnAssignee: {
|
||||
type: "agent",
|
||||
agentId: "22222222-2222-4222-8222-222222222222",
|
||||
},
|
||||
completedStageIds: [],
|
||||
lastDecisionId: null,
|
||||
lastDecisionOutcome: null,
|
||||
},
|
||||
};
|
||||
const policy = await createAgentReviewPolicy(reviewerAgentId)!;
|
||||
const issue = createReviewIssue(policy, reviewerAgentId);
|
||||
const reviewBody = "## Review: PAP-580 - APPROVED\n\nLooks good.";
|
||||
mockIssueService.getById.mockResolvedValue(issue);
|
||||
mockIssueService.addComment.mockResolvedValue({
|
||||
@@ -3869,35 +3634,8 @@ describe("issue comment reopen routes", () => {
|
||||
|
||||
it("rolls back the auto-approval comment when the issue is concurrently deleted", async () => {
|
||||
const reviewerAgentId = "33333333-3333-4333-8333-333333333333";
|
||||
const policy = await normalizePolicy({
|
||||
stages: [
|
||||
{
|
||||
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
||||
type: "review",
|
||||
participants: [{ type: "agent", agentId: reviewerAgentId }],
|
||||
},
|
||||
],
|
||||
})!;
|
||||
const issue = {
|
||||
...makeIssue("todo"),
|
||||
status: "in_review",
|
||||
assigneeAgentId: reviewerAgentId,
|
||||
executionPolicy: policy,
|
||||
executionState: {
|
||||
status: "pending",
|
||||
currentStageId: policy.stages[0].id,
|
||||
currentStageIndex: 0,
|
||||
currentStageType: "review",
|
||||
currentParticipant: { type: "agent", agentId: reviewerAgentId },
|
||||
returnAssignee: {
|
||||
type: "agent",
|
||||
agentId: "22222222-2222-4222-8222-222222222222",
|
||||
},
|
||||
completedStageIds: [],
|
||||
lastDecisionId: null,
|
||||
lastDecisionOutcome: null,
|
||||
},
|
||||
};
|
||||
const policy = await createAgentReviewPolicy(reviewerAgentId)!;
|
||||
const issue = createReviewIssue(policy, reviewerAgentId);
|
||||
const reviewBody = "## Review: PAP-580 - APPROVED\n\nLooks good.";
|
||||
mockIssueService.getById.mockResolvedValue(issue);
|
||||
mockIssueService.addComment.mockResolvedValue({
|
||||
|
||||
@@ -81,17 +81,55 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
await tempDb?.cleanup();
|
||||
});
|
||||
|
||||
function interactionCompanyRow(companyId: string): typeof companies.$inferInsert {
|
||||
return {
|
||||
id: companyId,
|
||||
name: "Paperclip",
|
||||
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
};
|
||||
}
|
||||
|
||||
function parentIssueRow(issueId: string, companyId: string, goalId: string): typeof issues.$inferInsert {
|
||||
return {
|
||||
id: issueId,
|
||||
companyId,
|
||||
goalId,
|
||||
title: "Parent issue",
|
||||
status: "in_progress",
|
||||
priority: "medium",
|
||||
};
|
||||
}
|
||||
|
||||
function acceptIssueScope(issueId: string, companyId: string, goalId: string) {
|
||||
return {
|
||||
id: issueId,
|
||||
companyId,
|
||||
goalId,
|
||||
projectId: null,
|
||||
};
|
||||
}
|
||||
|
||||
function activeCodexAgentRow(agentId: string, companyId: string, name: string, role: string): typeof agents.$inferInsert {
|
||||
return {
|
||||
id: agentId,
|
||||
companyId,
|
||||
name,
|
||||
role,
|
||||
status: "active",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
};
|
||||
}
|
||||
|
||||
async function seedConfirmationIssue(title = "Comment supersede") {
|
||||
const companyId = randomUUID();
|
||||
const goalId = randomUUID();
|
||||
const issueId = randomUUID();
|
||||
|
||||
await db.insert(companies).values({
|
||||
id: companyId,
|
||||
name: "Paperclip",
|
||||
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
await db.insert(companies).values(interactionCompanyRow(companyId));
|
||||
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: false });
|
||||
await db.insert(goals).values({
|
||||
id: goalId,
|
||||
@@ -100,14 +138,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
level: "task",
|
||||
status: "active",
|
||||
});
|
||||
await db.insert(issues).values({
|
||||
id: issueId,
|
||||
companyId,
|
||||
goalId,
|
||||
title: "Parent issue",
|
||||
status: "in_progress",
|
||||
priority: "medium",
|
||||
});
|
||||
await db.insert(issues).values(parentIssueRow(issueId, companyId, goalId));
|
||||
|
||||
return { companyId, goalId, issueId };
|
||||
}
|
||||
@@ -174,17 +205,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
const { companyId, issueId } = await seedConfirmationIssue("Source question race");
|
||||
const agentId = randomUUID();
|
||||
const runId = randomUUID();
|
||||
await db.insert(agents).values({
|
||||
id: agentId,
|
||||
companyId,
|
||||
name: "Questioner",
|
||||
role: "engineer",
|
||||
status: "active",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
});
|
||||
await db.insert(agents).values(activeCodexAgentRow(agentId, companyId, "Questioner", "engineer"));
|
||||
await db.insert(heartbeatRuns).values({
|
||||
id: runId,
|
||||
companyId,
|
||||
@@ -596,17 +617,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
it("rejects an explicitly mismatched source-run issue or agent", async () => {
|
||||
const fixture = await seedSourceQuestionFixture({ paperclipWake: { comments: [] } });
|
||||
const otherAgentId = randomUUID();
|
||||
await db.insert(agents).values({
|
||||
id: otherAgentId,
|
||||
companyId: fixture.companyId,
|
||||
name: "Other questioner",
|
||||
role: "engineer",
|
||||
status: "active",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
});
|
||||
await db.insert(agents).values(activeCodexAgentRow(otherAgentId, fixture.companyId, "Other questioner", "engineer"));
|
||||
|
||||
// Simulate historical corruption to retain the service-level defense test.
|
||||
await db.transaction(async (tx) => {
|
||||
@@ -764,17 +775,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
const agentId = randomUUID();
|
||||
const firstRunId = randomUUID();
|
||||
const secondRunId = randomUUID();
|
||||
await db.insert(agents).values({
|
||||
id: agentId,
|
||||
companyId,
|
||||
name: "Researcher",
|
||||
role: "researcher",
|
||||
status: "active",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
});
|
||||
await db.insert(agents).values(activeCodexAgentRow(agentId, companyId, "Researcher", "researcher"));
|
||||
await db.update(issues).set({ assigneeAgentId: agentId, status: "in_progress" }).where(eq(issues.id, issueId));
|
||||
await db.insert(heartbeatRuns).values([
|
||||
{
|
||||
@@ -1168,12 +1169,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
const assigneeAgentId = randomUUID();
|
||||
const responsibleUserId = randomUUID();
|
||||
|
||||
await db.insert(companies).values({
|
||||
id: companyId,
|
||||
name: "Paperclip",
|
||||
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
await db.insert(companies).values(interactionCompanyRow(companyId));
|
||||
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: false });
|
||||
|
||||
await db.insert(goals).values({
|
||||
@@ -1183,17 +1179,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
level: "task",
|
||||
status: "active",
|
||||
});
|
||||
await db.insert(agents).values({
|
||||
id: assigneeAgentId,
|
||||
companyId,
|
||||
name: "CodexCoder",
|
||||
role: "engineer",
|
||||
status: "active",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
});
|
||||
await db.insert(agents).values(activeCodexAgentRow(assigneeAgentId, companyId, "CodexCoder", "engineer"));
|
||||
await db.insert(issues).values({
|
||||
id: issueId,
|
||||
companyId,
|
||||
@@ -1233,12 +1219,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
|
||||
expect(created.status).toBe("pending");
|
||||
|
||||
const accepted = await interactionsSvc.acceptSuggestedTasks({
|
||||
id: issueId,
|
||||
companyId,
|
||||
goalId,
|
||||
projectId: null,
|
||||
}, created.id, {}, {
|
||||
const accepted = await interactionsSvc.acceptSuggestedTasks(acceptIssueScope(issueId, companyId, goalId), created.id, {}, {
|
||||
userId: "local-board",
|
||||
});
|
||||
|
||||
@@ -1295,12 +1276,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
expect(listed).toHaveLength(1);
|
||||
expect(listed[0]?.status).toBe("accepted");
|
||||
|
||||
await expect(interactionsSvc.acceptSuggestedTasks({
|
||||
id: issueId,
|
||||
companyId,
|
||||
goalId,
|
||||
projectId: null,
|
||||
}, created.id, {}, {
|
||||
await expect(interactionsSvc.acceptSuggestedTasks(acceptIssueScope(issueId, companyId, goalId), created.id, {}, {
|
||||
userId: "local-board",
|
||||
})).rejects.toThrow("Interaction has already been resolved");
|
||||
|
||||
@@ -1313,12 +1289,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
const goalId = randomUUID();
|
||||
const issueId = randomUUID();
|
||||
|
||||
await db.insert(companies).values({
|
||||
id: companyId,
|
||||
name: "Paperclip",
|
||||
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
await db.insert(companies).values(interactionCompanyRow(companyId));
|
||||
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: false });
|
||||
|
||||
await db.insert(goals).values({
|
||||
@@ -1366,12 +1337,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
userId: "local-board",
|
||||
});
|
||||
|
||||
const accepted = await interactionsSvc.acceptSuggestedTasks({
|
||||
id: issueId,
|
||||
companyId,
|
||||
goalId,
|
||||
projectId: null,
|
||||
}, created.id, {
|
||||
const accepted = await interactionsSvc.acceptSuggestedTasks(acceptIssueScope(issueId, companyId, goalId), created.id, {
|
||||
selectedClientKeys: ["root"],
|
||||
}, {
|
||||
userId: "local-board",
|
||||
@@ -1395,12 +1361,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
const goalId = randomUUID();
|
||||
const issueId = randomUUID();
|
||||
|
||||
await db.insert(companies).values({
|
||||
id: companyId,
|
||||
name: "Paperclip",
|
||||
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
await db.insert(companies).values(interactionCompanyRow(companyId));
|
||||
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: false });
|
||||
|
||||
await db.insert(goals).values({
|
||||
@@ -1410,14 +1371,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
level: "task",
|
||||
status: "active",
|
||||
});
|
||||
await db.insert(issues).values({
|
||||
id: issueId,
|
||||
companyId,
|
||||
goalId,
|
||||
title: "Parent issue",
|
||||
status: "in_progress",
|
||||
priority: "medium",
|
||||
});
|
||||
await db.insert(issues).values(parentIssueRow(issueId, companyId, goalId));
|
||||
|
||||
const created = await interactionsSvc.create({
|
||||
id: issueId,
|
||||
@@ -1444,12 +1398,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
});
|
||||
|
||||
await expect(
|
||||
interactionsSvc.acceptSuggestedTasks({
|
||||
id: issueId,
|
||||
companyId,
|
||||
goalId,
|
||||
projectId: null,
|
||||
}, created.id, {
|
||||
interactionsSvc.acceptSuggestedTasks(acceptIssueScope(issueId, companyId, goalId), created.id, {
|
||||
selectedClientKeys: ["child"],
|
||||
}, {
|
||||
userId: "local-board",
|
||||
@@ -1462,12 +1411,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
const goalId = randomUUID();
|
||||
const issueId = randomUUID();
|
||||
|
||||
await db.insert(companies).values({
|
||||
id: companyId,
|
||||
name: "Paperclip",
|
||||
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
await db.insert(companies).values(interactionCompanyRow(companyId));
|
||||
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: false });
|
||||
|
||||
await db.insert(goals).values({
|
||||
@@ -1564,12 +1508,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
const goalId = randomUUID();
|
||||
const issueId = randomUUID();
|
||||
|
||||
await db.insert(companies).values({
|
||||
id: companyId,
|
||||
name: "Paperclip",
|
||||
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
await db.insert(companies).values(interactionCompanyRow(companyId));
|
||||
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: false });
|
||||
await db.insert(goals).values({
|
||||
id: goalId,
|
||||
@@ -1951,12 +1890,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
const agentId = randomUUID();
|
||||
const runId = randomUUID();
|
||||
|
||||
await db.insert(companies).values({
|
||||
id: companyId,
|
||||
name: "Paperclip",
|
||||
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
await db.insert(companies).values(interactionCompanyRow(companyId));
|
||||
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: false });
|
||||
|
||||
await db.insert(goals).values({
|
||||
@@ -1966,25 +1900,8 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
level: "task",
|
||||
status: "active",
|
||||
});
|
||||
await db.insert(agents).values({
|
||||
id: agentId,
|
||||
companyId,
|
||||
name: "CodexCoder",
|
||||
role: "engineer",
|
||||
status: "active",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
});
|
||||
await db.insert(issues).values({
|
||||
id: issueId,
|
||||
companyId,
|
||||
goalId,
|
||||
title: "Parent issue",
|
||||
status: "in_progress",
|
||||
priority: "medium",
|
||||
});
|
||||
await db.insert(agents).values(activeCodexAgentRow(agentId, companyId, "CodexCoder", "engineer"));
|
||||
await db.insert(issues).values(parentIssueRow(issueId, companyId, goalId));
|
||||
await db.insert(heartbeatRuns).values({
|
||||
id: runId,
|
||||
companyId,
|
||||
@@ -2039,28 +1956,8 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
const firstAgentId = randomUUID();
|
||||
const secondAgentId = randomUUID();
|
||||
await db.insert(agents).values([
|
||||
{
|
||||
id: firstAgentId,
|
||||
companyId,
|
||||
name: "First agent",
|
||||
role: "engineer",
|
||||
status: "active",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
},
|
||||
{
|
||||
id: secondAgentId,
|
||||
companyId,
|
||||
name: "Second agent",
|
||||
role: "engineer",
|
||||
status: "active",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
},
|
||||
activeCodexAgentRow(firstAgentId, companyId, "First agent", "engineer"),
|
||||
activeCodexAgentRow(secondAgentId, companyId, "Second agent", "engineer"),
|
||||
]);
|
||||
|
||||
const older = await interactionsSvc.create({ id: issueId, companyId }, {
|
||||
@@ -2117,28 +2014,8 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
const probingAgentId = randomUUID();
|
||||
const otherAgentId = randomUUID();
|
||||
await db.insert(agents).values([
|
||||
{
|
||||
id: probingAgentId,
|
||||
companyId,
|
||||
name: "Probing agent",
|
||||
role: "engineer",
|
||||
status: "active",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
},
|
||||
{
|
||||
id: otherAgentId,
|
||||
companyId,
|
||||
name: "Other agent",
|
||||
role: "engineer",
|
||||
status: "active",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
},
|
||||
activeCodexAgentRow(probingAgentId, companyId, "Probing agent", "engineer"),
|
||||
activeCodexAgentRow(otherAgentId, companyId, "Other agent", "engineer"),
|
||||
]);
|
||||
|
||||
const question = (prompt: string) => ({
|
||||
@@ -2198,23 +2075,8 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
const goalId = randomUUID();
|
||||
const issueId = randomUUID();
|
||||
const agentId = randomUUID();
|
||||
await db.insert(companies).values({
|
||||
id: companyId,
|
||||
name: "Paperclip",
|
||||
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
await db.insert(agents).values({
|
||||
id: agentId,
|
||||
companyId,
|
||||
name: "Chief of staff",
|
||||
role: "chief_of_staff",
|
||||
status: "active",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
});
|
||||
await db.insert(companies).values(interactionCompanyRow(companyId));
|
||||
await db.insert(agents).values(activeCodexAgentRow(agentId, companyId, "Chief of staff", "chief_of_staff"));
|
||||
await db.insert(goals).values({
|
||||
id: goalId,
|
||||
companyId,
|
||||
@@ -2291,28 +2153,8 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
const firstAgentId = randomUUID();
|
||||
const secondAgentId = randomUUID();
|
||||
await db.insert(agents).values([
|
||||
{
|
||||
id: firstAgentId,
|
||||
companyId,
|
||||
name: "First agent",
|
||||
role: "engineer",
|
||||
status: "active",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
},
|
||||
{
|
||||
id: secondAgentId,
|
||||
companyId,
|
||||
name: "Second agent",
|
||||
role: "engineer",
|
||||
status: "active",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
},
|
||||
activeCodexAgentRow(firstAgentId, companyId, "First agent", "engineer"),
|
||||
activeCodexAgentRow(secondAgentId, companyId, "Second agent", "engineer"),
|
||||
]);
|
||||
|
||||
const firstAgentIds = [randomUUID(), randomUUID(), randomUUID()];
|
||||
@@ -2411,12 +2253,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
const goalId = randomUUID();
|
||||
const issueId = randomUUID();
|
||||
|
||||
await db.insert(companies).values({
|
||||
id: companyId,
|
||||
name: "Paperclip",
|
||||
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
await db.insert(companies).values(interactionCompanyRow(companyId));
|
||||
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: false });
|
||||
await db.insert(goals).values({
|
||||
id: goalId,
|
||||
@@ -2425,14 +2262,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
level: "task",
|
||||
status: "active",
|
||||
});
|
||||
await db.insert(issues).values({
|
||||
id: issueId,
|
||||
companyId,
|
||||
goalId,
|
||||
title: "Parent issue",
|
||||
status: "in_progress",
|
||||
priority: "medium",
|
||||
});
|
||||
await db.insert(issues).values(parentIssueRow(issueId, companyId, goalId));
|
||||
|
||||
const created = await interactionsSvc.create({
|
||||
id: issueId,
|
||||
@@ -2454,12 +2284,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
expect(created.kind).toBe("request_confirmation");
|
||||
expect(created.status).toBe("pending");
|
||||
|
||||
const accepted = await interactionsSvc.acceptInteraction({
|
||||
id: issueId,
|
||||
companyId,
|
||||
goalId,
|
||||
projectId: null,
|
||||
}, created.id, {}, {
|
||||
const accepted = await interactionsSvc.acceptInteraction(acceptIssueScope(issueId, companyId, goalId), created.id, {}, {
|
||||
userId: "local-board",
|
||||
});
|
||||
|
||||
@@ -2551,17 +2376,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
const resolverAgentId = randomUUID();
|
||||
const resolverRunId = randomUUID();
|
||||
await db.update(issues).set({ status: "in_review" }).where(eq(issues.id, issueId));
|
||||
await db.insert(agents).values({
|
||||
id: resolverAgentId,
|
||||
companyId,
|
||||
name: "Review agent",
|
||||
role: "reviewer",
|
||||
status: "active",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
});
|
||||
await db.insert(agents).values(activeCodexAgentRow(resolverAgentId, companyId, "Review agent", "reviewer"));
|
||||
await db.insert(heartbeatRuns).values({
|
||||
id: resolverRunId,
|
||||
companyId,
|
||||
@@ -2579,12 +2394,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
});
|
||||
await recordReviewTransition({ companyId, issueId, interactionId: created.id });
|
||||
|
||||
const accepted = await interactionsSvc.acceptInteraction({
|
||||
id: issueId,
|
||||
companyId,
|
||||
goalId,
|
||||
projectId: null,
|
||||
}, created.id, {}, {
|
||||
const accepted = await interactionsSvc.acceptInteraction(acceptIssueScope(issueId, companyId, goalId), created.id, {}, {
|
||||
agentId: resolverAgentId,
|
||||
runId: resolverRunId,
|
||||
resolverPolicyRestriction: "anyone",
|
||||
@@ -2604,17 +2414,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
const { companyId, goalId, issueId } = await seedConfirmationIssue(`Locked ${action} policy`);
|
||||
const resolverAgentId = randomUUID();
|
||||
const resolverRunId = randomUUID();
|
||||
await db.insert(agents).values({
|
||||
id: resolverAgentId,
|
||||
companyId,
|
||||
name: "Review agent",
|
||||
role: "reviewer",
|
||||
status: "active",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
});
|
||||
await db.insert(agents).values(activeCodexAgentRow(resolverAgentId, companyId, "Review agent", "reviewer"));
|
||||
await db.insert(heartbeatRuns).values({
|
||||
id: resolverRunId,
|
||||
companyId,
|
||||
@@ -2705,17 +2505,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
const resolverAgentId = randomUUID();
|
||||
const resolverRunId = randomUUID();
|
||||
await db.update(issues).set({ status: "in_review" }).where(eq(issues.id, issueId));
|
||||
await db.insert(agents).values({
|
||||
id: resolverAgentId,
|
||||
companyId,
|
||||
name: "Review agent",
|
||||
role: "reviewer",
|
||||
status: "active",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
});
|
||||
await db.insert(agents).values(activeCodexAgentRow(resolverAgentId, companyId, "Review agent", "reviewer"));
|
||||
await db.insert(heartbeatRuns).values({
|
||||
id: resolverRunId,
|
||||
companyId,
|
||||
@@ -2806,12 +2596,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const accepted = await interactionsSvc.acceptInteraction({
|
||||
id: issueId,
|
||||
companyId,
|
||||
goalId,
|
||||
projectId: null,
|
||||
}, created.id, {
|
||||
const accepted = await interactionsSvc.acceptInteraction(acceptIssueScope(issueId, companyId, goalId), created.id, {
|
||||
selectedOptionIds: ["file-c", "file-a"],
|
||||
}, {
|
||||
userId: "local-board",
|
||||
@@ -2854,34 +2639,19 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
userId: "local-board",
|
||||
});
|
||||
|
||||
await expect(interactionsSvc.acceptInteraction({
|
||||
id: issueId,
|
||||
companyId,
|
||||
goalId,
|
||||
projectId: null,
|
||||
}, created.id, {
|
||||
await expect(interactionsSvc.acceptInteraction(acceptIssueScope(issueId, companyId, goalId), created.id, {
|
||||
selectedOptionIds: [],
|
||||
}, {
|
||||
userId: "local-board",
|
||||
})).rejects.toThrow("Select at least 1 checkbox confirmation option(s)");
|
||||
|
||||
await expect(interactionsSvc.acceptInteraction({
|
||||
id: issueId,
|
||||
companyId,
|
||||
goalId,
|
||||
projectId: null,
|
||||
}, created.id, {
|
||||
await expect(interactionsSvc.acceptInteraction(acceptIssueScope(issueId, companyId, goalId), created.id, {
|
||||
selectedOptionIds: ["missing"],
|
||||
}, {
|
||||
userId: "local-board",
|
||||
})).rejects.toThrow("Unknown checkbox confirmation optionId: missing");
|
||||
|
||||
await expect(interactionsSvc.acceptInteraction({
|
||||
id: issueId,
|
||||
companyId,
|
||||
goalId,
|
||||
projectId: null,
|
||||
}, created.id, {
|
||||
await expect(interactionsSvc.acceptInteraction(acceptIssueScope(issueId, companyId, goalId), created.id, {
|
||||
selectedOptionIds: ["one", "two", "three"],
|
||||
}, {
|
||||
userId: "local-board",
|
||||
@@ -3217,12 +2987,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
const issueId = randomUUID();
|
||||
const agentId = randomUUID();
|
||||
|
||||
await db.insert(companies).values({
|
||||
id: companyId,
|
||||
name: "Paperclip",
|
||||
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
await db.insert(companies).values(interactionCompanyRow(companyId));
|
||||
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: false });
|
||||
await db.insert(goals).values({
|
||||
id: goalId,
|
||||
@@ -3231,17 +2996,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
level: "task",
|
||||
status: "active",
|
||||
});
|
||||
await db.insert(agents).values({
|
||||
id: agentId,
|
||||
companyId,
|
||||
name: "Senior Product Engineer",
|
||||
role: "engineer",
|
||||
status: "active",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
});
|
||||
await db.insert(agents).values(activeCodexAgentRow(agentId, companyId, "Senior Product Engineer", "engineer"));
|
||||
await db.insert(issues).values({
|
||||
id: issueId,
|
||||
companyId,
|
||||
@@ -3268,12 +3023,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
agentId,
|
||||
});
|
||||
|
||||
const accepted = await interactionsSvc.acceptInteraction({
|
||||
id: issueId,
|
||||
companyId,
|
||||
goalId,
|
||||
projectId: null,
|
||||
}, created.id, {}, {
|
||||
const accepted = await interactionsSvc.acceptInteraction(acceptIssueScope(issueId, companyId, goalId), created.id, {}, {
|
||||
userId: "local-board",
|
||||
});
|
||||
|
||||
@@ -3315,12 +3065,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
agentId,
|
||||
});
|
||||
|
||||
const resumed = await interactionsSvc.acceptInteraction({
|
||||
id: issueId,
|
||||
companyId,
|
||||
goalId,
|
||||
projectId: null,
|
||||
}, agentOwnedConfirmation.id, {}, {
|
||||
const resumed = await interactionsSvc.acceptInteraction(acceptIssueScope(issueId, companyId, goalId), agentOwnedConfirmation.id, {}, {
|
||||
userId: "local-board",
|
||||
});
|
||||
|
||||
@@ -3358,12 +3103,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
agentId,
|
||||
});
|
||||
|
||||
const acceptedWhileActive = await interactionsSvc.acceptInteraction({
|
||||
id: issueId,
|
||||
companyId,
|
||||
goalId,
|
||||
projectId: null,
|
||||
}, activeConfirmation.id, {}, {
|
||||
const acceptedWhileActive = await interactionsSvc.acceptInteraction(acceptIssueScope(issueId, companyId, goalId), activeConfirmation.id, {}, {
|
||||
userId: "local-board",
|
||||
});
|
||||
|
||||
@@ -3380,17 +3120,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
it("atomically returns an accepted Plan-mode issue to its agent in Auto mode", async () => {
|
||||
const { companyId, goalId, issueId } = await seedConfirmationIssue("Accept a plan into Auto mode");
|
||||
const agentId = randomUUID();
|
||||
await db.insert(agents).values({
|
||||
id: agentId,
|
||||
companyId,
|
||||
name: "Plan owner",
|
||||
role: "engineer",
|
||||
status: "active",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
});
|
||||
await db.insert(agents).values(activeCodexAgentRow(agentId, companyId, "Plan owner", "engineer"));
|
||||
await db.update(issues).set({
|
||||
status: "in_review",
|
||||
workMode: "planning",
|
||||
@@ -3403,12 +3133,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
payload: { version: 1, prompt: "Accept this plan?", target },
|
||||
}, { agentId });
|
||||
|
||||
const accepted = await interactionsSvc.acceptInteraction({
|
||||
id: issueId,
|
||||
companyId,
|
||||
goalId,
|
||||
projectId: null,
|
||||
}, created.id, {}, { userId: "local-board" });
|
||||
const accepted = await interactionsSvc.acceptInteraction(acceptIssueScope(issueId, companyId, goalId), created.id, {}, { userId: "local-board" });
|
||||
|
||||
expect(accepted.interaction).toMatchObject({
|
||||
id: created.id,
|
||||
@@ -3433,17 +3158,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
it("keeps Plan mode for non-plan and checkbox confirmations", async () => {
|
||||
const { companyId, goalId, issueId } = await seedConfirmationIssue("Do not auto-transition other confirmations");
|
||||
const agentId = randomUUID();
|
||||
await db.insert(agents).values({
|
||||
id: agentId,
|
||||
companyId,
|
||||
name: "Plan owner",
|
||||
role: "engineer",
|
||||
status: "active",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
});
|
||||
await db.insert(agents).values(activeCodexAgentRow(agentId, companyId, "Plan owner", "engineer"));
|
||||
await db.update(issues).set({
|
||||
status: "in_review",
|
||||
workMode: "planning",
|
||||
@@ -3454,7 +3169,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
kind: "request_confirmation",
|
||||
payload: { version: 1, prompt: "Accept this unrelated decision?" },
|
||||
}, { agentId });
|
||||
await interactionsSvc.acceptInteraction({ id: issueId, companyId, goalId, projectId: null }, nonPlan.id, {}, {
|
||||
await interactionsSvc.acceptInteraction(acceptIssueScope(issueId, companyId, goalId), nonPlan.id, {}, {
|
||||
userId: "local-board",
|
||||
});
|
||||
await expect(db.select().from(issues).where(eq(issues.id, issueId)).then((rows) => rows[0]?.workMode))
|
||||
@@ -3471,7 +3186,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
target,
|
||||
},
|
||||
}, { agentId });
|
||||
await interactionsSvc.acceptInteraction({ id: issueId, companyId, goalId, projectId: null }, checkbox.id, {
|
||||
await interactionsSvc.acceptInteraction(acceptIssueScope(issueId, companyId, goalId), checkbox.id, {
|
||||
selectedOptionIds: ["phase-1"],
|
||||
}, { userId: "local-board" });
|
||||
await expect(db.select().from(issues).where(eq(issues.id, issueId)).then((rows) => rows[0]?.workMode))
|
||||
@@ -3487,7 +3202,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
payload: { version: 1, prompt: "Accept this plan?", target },
|
||||
}, { userId: "local-board" });
|
||||
|
||||
await interactionsSvc.acceptInteraction({ id: issueId, companyId, goalId, projectId: null }, created.id, {}, {
|
||||
await interactionsSvc.acceptInteraction(acceptIssueScope(issueId, companyId, goalId), created.id, {}, {
|
||||
userId: "local-board",
|
||||
});
|
||||
|
||||
@@ -3929,12 +3644,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
const revisionId = randomUUID();
|
||||
const nextRevisionId = randomUUID();
|
||||
|
||||
await db.insert(companies).values({
|
||||
id: companyId,
|
||||
name: "Paperclip",
|
||||
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
await db.insert(companies).values(interactionCompanyRow(companyId));
|
||||
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: false });
|
||||
await db.insert(goals).values({
|
||||
id: goalId,
|
||||
@@ -3943,14 +3653,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
level: "task",
|
||||
status: "active",
|
||||
});
|
||||
await db.insert(issues).values({
|
||||
id: issueId,
|
||||
companyId,
|
||||
goalId,
|
||||
title: "Parent issue",
|
||||
status: "in_progress",
|
||||
priority: "medium",
|
||||
});
|
||||
await db.insert(issues).values(parentIssueRow(issueId, companyId, goalId));
|
||||
await db.insert(documents).values({
|
||||
id: documentId,
|
||||
companyId,
|
||||
@@ -4013,12 +3716,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
latestRevisionNumber: 2,
|
||||
});
|
||||
|
||||
await expect(interactionsSvc.acceptInteraction({
|
||||
id: issueId,
|
||||
companyId,
|
||||
goalId,
|
||||
projectId: null,
|
||||
}, created.id, {}, {
|
||||
await expect(interactionsSvc.acceptInteraction(acceptIssueScope(issueId, companyId, goalId), created.id, {}, {
|
||||
userId: "local-board",
|
||||
})).rejects.toMatchObject({
|
||||
status: 409,
|
||||
@@ -4057,12 +3755,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
const revisionId = randomUUID();
|
||||
const nextRevisionId = randomUUID();
|
||||
|
||||
await db.insert(companies).values({
|
||||
id: companyId,
|
||||
name: "Paperclip",
|
||||
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
await db.insert(companies).values(interactionCompanyRow(companyId));
|
||||
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: false });
|
||||
await db.insert(goals).values({
|
||||
id: goalId,
|
||||
@@ -4071,14 +3764,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
level: "task",
|
||||
status: "active",
|
||||
});
|
||||
await db.insert(issues).values({
|
||||
id: issueId,
|
||||
companyId,
|
||||
goalId,
|
||||
title: "Parent issue",
|
||||
status: "in_progress",
|
||||
priority: "medium",
|
||||
});
|
||||
await db.insert(issues).values(parentIssueRow(issueId, companyId, goalId));
|
||||
await db.update(issues).set({ workMode: "planning" }).where(eq(issues.id, issueId));
|
||||
// Document is already at revision 2 — revision 1 is stale.
|
||||
await db.insert(documents).values({
|
||||
@@ -4172,12 +3858,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
userId: "local-board",
|
||||
});
|
||||
expect(created).toMatchObject({ status: "pending", kind: "request_confirmation" });
|
||||
await expect(interactionsSvc.acceptInteraction({
|
||||
id: issueId,
|
||||
companyId,
|
||||
goalId,
|
||||
projectId: null,
|
||||
}, created.id, {}, {
|
||||
await expect(interactionsSvc.acceptInteraction(acceptIssueScope(issueId, companyId, goalId), created.id, {}, {
|
||||
userId: "local-board",
|
||||
})).resolves.toMatchObject({
|
||||
interaction: { status: "accepted" },
|
||||
@@ -4196,12 +3877,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
const revisionId = randomUUID();
|
||||
const nextRevisionId = randomUUID();
|
||||
|
||||
await db.insert(companies).values({
|
||||
id: companyId,
|
||||
name: "Paperclip",
|
||||
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
await db.insert(companies).values(interactionCompanyRow(companyId));
|
||||
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: false });
|
||||
await db.insert(goals).values({
|
||||
id: goalId,
|
||||
@@ -4210,14 +3886,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
level: "task",
|
||||
status: "active",
|
||||
});
|
||||
await db.insert(issues).values({
|
||||
id: issueId,
|
||||
companyId,
|
||||
goalId,
|
||||
title: "Parent issue",
|
||||
status: "in_progress",
|
||||
priority: "medium",
|
||||
});
|
||||
await db.insert(issues).values(parentIssueRow(issueId, companyId, goalId));
|
||||
await db.insert(documents).values({
|
||||
id: documentId,
|
||||
companyId,
|
||||
@@ -4357,12 +4026,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
const foreignRunId = randomUUID();
|
||||
const kind = options?.kind ?? "request_confirmation";
|
||||
|
||||
await db.insert(companies).values({
|
||||
id: companyId,
|
||||
name: "Paperclip",
|
||||
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
await db.insert(companies).values(interactionCompanyRow(companyId));
|
||||
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: false });
|
||||
await db.insert(projects).values({
|
||||
id: projectId,
|
||||
@@ -4379,17 +4043,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
visibility: "default",
|
||||
isPrimary: true,
|
||||
});
|
||||
await db.insert(agents).values({
|
||||
id: agentId,
|
||||
companyId,
|
||||
name: "CodexCoder",
|
||||
role: "engineer",
|
||||
status: "active",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
});
|
||||
await db.insert(agents).values(activeCodexAgentRow(agentId, companyId, "CodexCoder", "engineer"));
|
||||
const sourceRunStatus = options?.sourceRunStatus ?? "succeeded";
|
||||
const sourceRunTerminal = sourceRunStatus !== "running";
|
||||
await db.insert(heartbeatRuns).values([
|
||||
@@ -4596,7 +4250,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
});
|
||||
|
||||
const accepted = await interactionsSvc.acceptInteraction(
|
||||
{ id: issueId, companyId, goalId, projectId: null },
|
||||
acceptIssueScope(issueId, companyId, goalId),
|
||||
interactionId,
|
||||
{},
|
||||
{ userId: "local-board" },
|
||||
@@ -4624,7 +4278,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
|
||||
await expect(
|
||||
interactionsSvc.acceptInteraction(
|
||||
{ id: issueId, companyId, goalId, projectId: null },
|
||||
acceptIssueScope(issueId, companyId, goalId),
|
||||
interactionId,
|
||||
{},
|
||||
{ userId: "local-board" },
|
||||
@@ -4654,7 +4308,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
});
|
||||
|
||||
const accepted = await interactionsSvc.acceptInteraction(
|
||||
{ id: issueId, companyId, goalId, projectId: null },
|
||||
acceptIssueScope(issueId, companyId, goalId),
|
||||
interactionId,
|
||||
{},
|
||||
{ userId: "local-board" },
|
||||
@@ -4692,7 +4346,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
});
|
||||
|
||||
const accepted = await interactionsSvc.acceptInteraction(
|
||||
{ id: issueId, companyId, goalId, projectId: null },
|
||||
acceptIssueScope(issueId, companyId, goalId),
|
||||
interactionId,
|
||||
{},
|
||||
{ userId: "local-board" },
|
||||
@@ -4722,7 +4376,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
});
|
||||
|
||||
const accepted = await interactionsSvc.acceptInteraction(
|
||||
{ id: issueId, companyId, goalId, projectId: null },
|
||||
acceptIssueScope(issueId, companyId, goalId),
|
||||
interactionId,
|
||||
{},
|
||||
{ userId: "local-board" },
|
||||
@@ -4752,7 +4406,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
|
||||
await expect(
|
||||
interactionsSvc.acceptInteraction(
|
||||
{ id: issueId, companyId, goalId, projectId: null },
|
||||
acceptIssueScope(issueId, companyId, goalId),
|
||||
interactionId,
|
||||
{},
|
||||
{ userId: "local-board" },
|
||||
@@ -4780,7 +4434,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
});
|
||||
|
||||
const accepted = await interactionsSvc.acceptInteraction(
|
||||
{ id: issueId, companyId, goalId, projectId: null },
|
||||
acceptIssueScope(issueId, companyId, goalId),
|
||||
interactionId,
|
||||
{},
|
||||
{ userId: "local-board" },
|
||||
@@ -4815,7 +4469,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
});
|
||||
|
||||
const accepted = await interactionsSvc.acceptInteraction(
|
||||
{ id: issueId, companyId, goalId, projectId: null },
|
||||
acceptIssueScope(issueId, companyId, goalId),
|
||||
interactionId,
|
||||
{ selectedOptionIds: ["file-b"] },
|
||||
{ userId: "local-board" },
|
||||
@@ -4846,7 +4500,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
|
||||
await expect(
|
||||
interactionsSvc.acceptInteraction(
|
||||
{ id: issueId, companyId, goalId, projectId: null },
|
||||
acceptIssueScope(issueId, companyId, goalId),
|
||||
interactionId,
|
||||
{ selectedOptionIds: ["file-a"] },
|
||||
{ userId: "local-board" },
|
||||
@@ -4869,7 +4523,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
});
|
||||
|
||||
const accepted = await interactionsSvc.acceptInteraction(
|
||||
{ id: issueId, companyId, goalId, projectId: null },
|
||||
acceptIssueScope(issueId, companyId, goalId),
|
||||
interactionId,
|
||||
{ selectedOptionIds: ["file-a"] },
|
||||
{ userId: "local-board" },
|
||||
@@ -4896,7 +4550,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
});
|
||||
|
||||
const accepted = await interactionsSvc.acceptInteraction(
|
||||
{ id: issueId, companyId, goalId, projectId: null },
|
||||
acceptIssueScope(issueId, companyId, goalId),
|
||||
interactionId,
|
||||
{ selectedOptionIds: ["file-a"] },
|
||||
{ userId: "local-board" },
|
||||
@@ -4946,7 +4600,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
||||
});
|
||||
|
||||
const accepted = await interactionsSvc.acceptInteraction(
|
||||
{ id: issueId, companyId, goalId, projectId: null },
|
||||
acceptIssueScope(issueId, companyId, goalId),
|
||||
created.id,
|
||||
{},
|
||||
{ userId: "local-board" },
|
||||
|
||||
@@ -276,8 +276,6 @@ describe("issue update comment wakeups", () => {
|
||||
mockIssueService.getWakeableParentAfterChildCompletion.mockResolvedValue(null);
|
||||
mockIssueService.getCurrentScheduledRetry.mockResolvedValue(null);
|
||||
mockIssueService.listReviewAttention.mockResolvedValue(new Map());
|
||||
// A fresh route graph is fixture preparation, not request execution. Keep
|
||||
// per-case module resets while charging the cold transform to hookTimeout.
|
||||
routeFixture = await loadRouteFixture();
|
||||
});
|
||||
|
||||
|
||||
@@ -6,6 +6,11 @@ import {
|
||||
stripSecretBearingUrlParts,
|
||||
} from "../middleware/redact-sensitive.js";
|
||||
|
||||
it("redacts Slack manager grants and sealed OAuth handoffs", () => {
|
||||
const result = redactSensitive({ managerToken: "manager-canary", refreshToken: "refresh-canary", sealed: { ct: "sealed-canary" }, handoff: { token: "handoff-canary" }, claim_id: "claim-canary" });
|
||||
expect(JSON.stringify(result)).not.toContain("canary");
|
||||
});
|
||||
|
||||
describe("redactSensitive", () => {
|
||||
it("redacts a plaintext password field on a sign-in body", () => {
|
||||
const body = {
|
||||
|
||||
Loaded 100 of 449 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user