@cursor/july 0.1.95 → 0.1.96
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +3 -4
- package/dist/channels/deployments/types.d.ts +1 -1
- package/dist/channels/deployments/types.d.ts.map +1 -1
- package/dist/channels/deployments/types.js +1 -0
- package/dist/channels/origin/api.d.ts +59 -13
- package/dist/channels/origin/api.d.ts.map +1 -1
- package/dist/channels/origin/api.js +168 -27
- package/dist/channels/origin/binding.d.ts +2 -2
- package/dist/channels/origin/checks.d.ts +6 -6
- package/dist/channels/origin/checks.js +6 -6
- package/dist/channels/origin/cursor-account.d.ts +5 -3
- package/dist/channels/origin/cursor-account.d.ts.map +1 -1
- package/dist/channels/origin/cursor-account.js +5 -3
- package/dist/channels/slack/api.d.ts +9 -0
- package/dist/channels/slack/api.d.ts.map +1 -1
- package/dist/channels/slack/api.js +36 -5
- package/dist/channels/slack/defaults.d.ts.map +1 -1
- package/dist/channels/slack/defaults.js +17 -46
- package/dist/channels/slack/dispatch.d.ts +7 -1
- package/dist/channels/slack/dispatch.d.ts.map +1 -1
- package/dist/channels/slack/dispatch.js +47 -23
- package/dist/channels/slack/inbound.d.ts +8 -0
- package/dist/channels/slack/inbound.d.ts.map +1 -1
- package/dist/channels/slack/inbound.js +32 -0
- package/dist/channels/slack/live-delivery.d.ts +26 -29
- package/dist/channels/slack/live-delivery.d.ts.map +1 -1
- package/dist/channels/slack/live-delivery.js +172 -73
- package/dist/channels/slack/manifest.d.ts.map +1 -1
- package/dist/channels/slack/manifest.js +5 -1
- package/dist/channels/slack/nudge-carrier.d.ts +2 -0
- package/dist/channels/slack/nudge-carrier.d.ts.map +1 -1
- package/dist/channels/slack/nudge-carrier.js +48 -1
- package/dist/channels/slack/slack-channel.d.ts.map +1 -1
- package/dist/channels/slack/slack-channel.js +2 -2
- package/dist/docs/404.html +2 -2
- package/dist/docs/ab.html +4 -4
- package/dist/docs/assets/{app.BBj0klBO.js → app.jXleCmOc.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.DFoGuETX.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.CUEXpL78.js → VPLocalSearchBox.8LhyglIT.js} +1 -1
- package/dist/docs/assets/chunks/{theme.DabmQnia.js → theme.9ged-pr2.js} +2 -2
- package/dist/docs/assets/index.md.CI93Ltlb.js +5 -0
- package/dist/docs/assets/{index.md.BoHaMdeZ.lean.js → index.md.CI93Ltlb.lean.js} +1 -1
- package/dist/docs/assets/{reference_cli.md.BsMOxDvh.js → reference_cli.md.CCnG-nJx.js} +2 -1
- package/dist/docs/assets/templates_code-wiki.md.CDYZIBFN.js +2 -0
- package/dist/docs/assets/templates_code-wiki.md.CDYZIBFN.lean.js +1 -0
- package/dist/docs/building-with-agents.html +4 -4
- package/dist/docs/concepts.html +4 -4
- package/dist/docs/deployment.html +4 -4
- package/dist/docs/evals.html +4 -4
- package/dist/docs/guides/agent-to-agent.html +4 -4
- package/dist/docs/guides/cloud-runtime.html +4 -4
- package/dist/docs/guides/convert-automation.html +4 -4
- package/dist/docs/guides/github.html +4 -4
- package/dist/docs/guides/human-in-the-loop.html +4 -4
- package/dist/docs/guides/mcp-oauth.html +4 -4
- package/dist/docs/guides/opentelemetry.html +4 -4
- package/dist/docs/guides/slack.html +4 -4
- package/dist/docs/guides/webhooks.html +4 -4
- package/dist/docs/hashmap.json +1 -1
- package/dist/docs/hillclimbing.html +4 -4
- package/dist/docs/index.html +6 -6
- package/dist/docs/index.md +3 -1
- package/dist/docs/llms-full.txt +102 -3
- package/dist/docs/llms.txt +1 -0
- package/dist/docs/quickstart.html +4 -4
- package/dist/docs/reference/agent-config.html +4 -4
- package/dist/docs/reference/artifacts.html +4 -4
- package/dist/docs/reference/channels.html +4 -4
- package/dist/docs/reference/cli.html +7 -6
- package/dist/docs/reference/cli.md +3 -2
- package/dist/docs/reference/connections.html +4 -4
- package/dist/docs/reference/hooks.html +4 -4
- package/dist/docs/reference/http-api.html +4 -4
- package/dist/docs/reference/instructions.html +4 -4
- package/dist/docs/reference/playground.html +4 -4
- package/dist/docs/reference/project-layout.html +4 -4
- package/dist/docs/reference/prompt.html +4 -4
- package/dist/docs/reference/schedules.html +4 -4
- package/dist/docs/reference/sessions.html +4 -4
- package/dist/docs/reference/skills.html +4 -4
- package/dist/docs/reference/subagents.html +4 -4
- package/dist/docs/reference/tools.html +4 -4
- package/dist/docs/scaffolding-agents.html +4 -4
- package/dist/docs/storage.html +4 -4
- package/dist/docs/templates/agentic-owners.html +4 -4
- package/dist/docs/templates/code-wiki.html +28 -0
- package/dist/docs/templates/code-wiki.md +91 -0
- package/dist/docs/templates/demo.html +5 -5
- package/dist/docs/templates/pr-autofixer.html +4 -4
- package/dist/docs/templates/security-help.html +4 -4
- package/dist/docs/templates/security-reviewer.html +5 -5
- package/dist/docs/templates/triage.html +4 -4
- package/dist/docs/troubleshooting.html +4 -4
- package/dist/files-backends/cursor-hosted.d.ts +26 -4
- package/dist/files-backends/cursor-hosted.d.ts.map +1 -1
- package/dist/files-backends/cursor-hosted.js +32 -4
- package/dist/internal/advertise-tools.d.ts +2 -0
- package/dist/internal/advertise-tools.d.ts.map +1 -1
- package/dist/internal/advertise-tools.js +15 -1
- package/dist/internal/bounded.d.ts +7 -0
- package/dist/internal/bounded.d.ts.map +1 -0
- package/dist/internal/bounded.js +32 -0
- package/dist/internal/cli-deploy.d.ts.map +1 -1
- package/dist/internal/cli-deploy.js +7 -1
- package/dist/internal/conversation-mirror.d.ts +11 -8
- package/dist/internal/conversation-mirror.d.ts.map +1 -1
- package/dist/internal/conversation-mirror.js +116 -55
- package/dist/internal/cursor/backend-client.d.ts.map +1 -1
- package/dist/internal/cursor/backend-client.js +22 -48
- package/dist/internal/cursor/session-token.d.ts +45 -0
- package/dist/internal/cursor/session-token.d.ts.map +1 -0
- package/dist/internal/cursor/session-token.js +99 -0
- package/dist/internal/cursor-agent-template.d.ts +1 -1
- package/dist/internal/cursor-agent-template.d.ts.map +1 -1
- package/dist/internal/cursor-agent-template.js +1 -0
- package/dist/internal/framework-file-storage.d.ts +61 -0
- package/dist/internal/framework-file-storage.d.ts.map +1 -0
- package/dist/internal/framework-file-storage.js +204 -0
- package/dist/internal/framework-storage-selection.d.ts +11 -5
- package/dist/internal/framework-storage-selection.d.ts.map +1 -1
- package/dist/internal/framework-storage-selection.js +38 -11
- package/dist/internal/hosted-delivery.d.ts +7 -0
- package/dist/internal/hosted-delivery.d.ts.map +1 -1
- package/dist/internal/hosted-delivery.js +4 -0
- package/dist/internal/init-project.d.ts.map +1 -1
- package/dist/internal/init-project.js +2 -0
- package/dist/internal/install-cursor-skills.d.ts.map +1 -1
- package/dist/internal/install-cursor-skills.js +1 -3
- package/dist/internal/managed-delivery.d.ts +80 -0
- package/dist/internal/managed-delivery.d.ts.map +1 -0
- package/dist/internal/managed-delivery.js +86 -0
- package/dist/internal/runtime-dispatch-runner.d.ts +5 -1
- package/dist/internal/runtime-dispatch-runner.d.ts.map +1 -1
- package/dist/internal/runtime-dispatch-runner.js +7 -0
- package/dist/internal/sdk-runner.d.ts +11 -0
- package/dist/internal/sdk-runner.d.ts.map +1 -1
- package/dist/internal/sdk-runner.js +39 -8
- package/dist/internal/server.js +1 -0
- package/dist/internal/session-engine.d.ts +13 -0
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +62 -3
- package/dist/internal/steering.d.ts +15 -0
- package/dist/internal/steering.d.ts.map +1 -0
- package/dist/internal/steering.js +18 -0
- package/dist/playground/assets/index-B1DDvBeC.css +1 -0
- package/dist/playground/assets/{index-CZA1uEWA.js → index-Cx9CclK_.js} +51 -51
- package/dist/playground/index.html +2 -2
- package/dist/storage-backends/cursor-hosted.d.ts +8 -5
- package/dist/storage-backends/cursor-hosted.d.ts.map +1 -1
- package/dist/storage-backends/cursor-hosted.js +4 -3
- package/dist/types.d.ts +2 -1
- package/dist/types.d.ts.map +1 -1
- package/docs/README.md +3 -1
- package/docs/reference/cli.md +3 -2
- package/docs/templates/code-wiki.md +96 -0
- package/package.json +2 -2
- package/skills/ab/SKILL.md +13 -97
- package/skills/create-agent/SKILL.md +123 -223
- package/skills/debug/SKILL.md +34 -68
- package/skills/evals/SKILL.md +55 -180
- package/skills/framework-map/SKILL.md +83 -145
- package/skills/github/SKILL.md +53 -111
- package/skills/hillclimb/SKILL.md +40 -163
- package/skills/mcp-auth/SKILL.md +48 -107
- package/skills/otel/SKILL.md +22 -68
- package/skills/setup-slack/SKILL.md +77 -208
- package/src/channels/deployments/types.ts +1 -0
- package/src/channels/origin/api.ts +242 -30
- package/src/channels/origin/binding.ts +2 -2
- package/src/channels/origin/checks.ts +6 -6
- package/src/channels/origin/cursor-account.ts +5 -3
- package/src/channels/slack/api.ts +41 -5
- package/src/channels/slack/defaults.ts +15 -49
- package/src/channels/slack/dispatch.ts +56 -22
- package/src/channels/slack/inbound.ts +43 -0
- package/src/channels/slack/live-delivery.ts +235 -114
- package/src/channels/slack/manifest.ts +5 -1
- package/src/channels/slack/nudge-carrier.ts +48 -1
- package/src/channels/slack/slack-channel.ts +2 -0
- package/src/files-backends/cursor-hosted.ts +40 -7
- package/src/internal/advertise-tools.ts +18 -2
- package/src/internal/bounded.ts +28 -0
- package/src/internal/cli-deploy.ts +12 -0
- package/src/internal/conversation-mirror.ts +145 -69
- package/src/internal/cursor/backend-client.ts +34 -58
- package/src/internal/cursor/session-token.ts +115 -0
- package/src/internal/cursor-agent-template.ts +1 -0
- package/src/internal/framework-file-storage.ts +222 -0
- package/src/internal/framework-storage-selection.ts +51 -11
- package/src/internal/hosted-delivery.ts +10 -0
- package/src/internal/init-project.ts +2 -0
- package/src/internal/install-cursor-skills.ts +1 -4
- package/src/internal/managed-delivery.ts +125 -0
- package/src/internal/runtime-dispatch-runner.ts +10 -0
- package/src/internal/sdk-runner.ts +56 -8
- package/src/internal/server.ts +2 -0
- package/src/internal/session-engine.ts +64 -4
- package/src/internal/steering.ts +17 -0
- package/src/storage-backends/cursor-hosted.ts +9 -7
- package/src/types.ts +2 -1
- package/templates/code-wiki/README.md +34 -0
- package/templates/code-wiki/agent/agent.ts +8 -0
- package/templates/code-wiki/agent/channels/github.ts +144 -0
- package/templates/code-wiki/agent/instructions.md +39 -0
- package/templates/code-wiki/agent/lib/repos.ts +5 -0
- package/templates/code-wiki/agent/lib/wiki.ts +22 -0
- package/templates/code-wiki/agent/tools/apply_wiki.ts +219 -0
- package/templates/code-wiki/evals/evals.config.ts +5 -0
- package/templates/code-wiki/evals/wiki.eval.ts +132 -0
- package/templates/code-wiki/init.json +25 -0
- package/templates/code-wiki/package.json +18 -0
- package/templates/code-wiki/tsconfig.json +12 -0
- package/dist/docs/assets/chunks/@localSearchIndexroot.oqPawjiD.js +0 -1
- package/dist/docs/assets/index.md.BoHaMdeZ.js +0 -5
- package/dist/internal/deployment-scm-event-stream.d.ts +0 -116
- package/dist/internal/deployment-scm-event-stream.d.ts.map +0 -1
- package/dist/internal/deployment-scm-event-stream.js +0 -474
- package/dist/playground/assets/index-CF7hUDnQ.css +0 -1
- package/src/internal/deployment-scm-event-stream.ts +0 -593
- /package/dist/docs/assets/{reference_cli.md.BsMOxDvh.lean.js → reference_cli.md.CCnG-nJx.lean.js} +0 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Envelope-carried installation config for managed-product deliveries
|
|
3
|
+
* (plan 0002 Stack H1; envelope shape from Stack B1).
|
|
4
|
+
*
|
|
5
|
+
* On the v2 architecture a managed product (security-reviewer, …) is many
|
|
6
|
+
* single-tenant deployments: the pre-signal trigger gate matches the
|
|
7
|
+
* customer's automation trigger before a workflow starts and stamps the
|
|
8
|
+
* matched automation uuid + an opaque installation-config reference on the
|
|
9
|
+
* attributed session envelope. The delivery activity forwards the matched
|
|
10
|
+
* installation-config document to the fork **inside the webhook payload it
|
|
11
|
+
* POSTs to the channel**, under {@link MANAGED_DELIVERY_PAYLOAD_KEY} — the
|
|
12
|
+
* same embed-in-payload pattern the legacy deployment event stream used
|
|
13
|
+
* for its source marker.
|
|
14
|
+
*
|
|
15
|
+
* This module is the one contract both sides share: the control-plane
|
|
16
|
+
* delivery path embeds with {@link withManagedDeliveryConfig}; the agent's
|
|
17
|
+
* channels detect and read with {@link parseManagedDeliveryConfig}. The
|
|
18
|
+
* document mirrors the sync layer's `ManagedAutomationConfigDoc` **policy**
|
|
19
|
+
* fields that "ride the delivery envelope to the fork" (posting mode,
|
|
20
|
+
* managed config, trigger snapshots for the channel's staleness guard);
|
|
21
|
+
* readers ignore unknown fields so a newer sync can add material without
|
|
22
|
+
* breaking older forks. The automation's MCP server refs deliberately do
|
|
23
|
+
* NOT ride here: they travel on the hosted-admission envelope's
|
|
24
|
+
* `managedAutomation` slice (plan 0002 G2, `hosted-delivery-protocol.ts` /
|
|
25
|
+
* `hosted-managed-automation.ts`) so one mechanism owns MCP attach.
|
|
26
|
+
*
|
|
27
|
+
* INTERNAL: first-party host plumbing shared by agents in `factory/*`,
|
|
28
|
+
* consumed via relative source imports. Deliberately excluded from the
|
|
29
|
+
* published `@cursor/july` npm package (see the `files` whitelist).
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Key inside the delivered webhook payload under which the delivery
|
|
34
|
+
* activity embeds the matched installation config. Chosen to never
|
|
35
|
+
* collide with a provider webhook field (GitHub and Origin payloads have
|
|
36
|
+
* no `cursor*` keys).
|
|
37
|
+
*/
|
|
38
|
+
export const MANAGED_DELIVERY_PAYLOAD_KEY = "cursorManagedDelivery" as const;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The matched installation-config document as it rides the delivery to
|
|
42
|
+
* the fork. Required: the matched automation uuid (B1 attribution).
|
|
43
|
+
* Everything else is optional; absent fields keep the channel's default
|
|
44
|
+
* posture (post, all modules enabled, no staleness guard).
|
|
45
|
+
*/
|
|
46
|
+
export interface ManagedDeliveryInstallationConfig {
|
|
47
|
+
/** Source managed automation uuid the pre-signal gate matched. */
|
|
48
|
+
matchedAutomationUuid: string;
|
|
49
|
+
/** Opaque `<installationId>@<resourceVersion>` config reference. */
|
|
50
|
+
installationConfigRef?: string;
|
|
51
|
+
managedType?: string;
|
|
52
|
+
name?: string;
|
|
53
|
+
/**
|
|
54
|
+
* Whether product output (PR comments, …) posts for this installation,
|
|
55
|
+
* or the run stays check/shadow-only. Channels post only on `"post"`
|
|
56
|
+
* (or when the field is absent) — any other value fails closed to
|
|
57
|
+
* shadow, so a newer mode name never posts by accident.
|
|
58
|
+
*/
|
|
59
|
+
postingMode?: string;
|
|
60
|
+
managedConfig?: Record<string, unknown>;
|
|
61
|
+
/**
|
|
62
|
+
* The automation's git triggers as `GitTrigger.toJson()` snapshots.
|
|
63
|
+
* The channel re-checks the delivery against them (staleness guard:
|
|
64
|
+
* the config may have changed between admission and delivery).
|
|
65
|
+
*/
|
|
66
|
+
triggerSnapshots?: unknown[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
70
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function nonEmptyString(value: unknown): string | undefined {
|
|
74
|
+
if (typeof value !== "string") {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
const trimmed = value.trim();
|
|
78
|
+
return trimmed === "" ? undefined : trimmed;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Read the embedded installation config out of a delivered webhook
|
|
83
|
+
* payload. Returns undefined for payloads without the key (legacy
|
|
84
|
+
* deliveries, host relay, fixtures) and for documents missing the
|
|
85
|
+
* required matched automation uuid — the channel then runs its legacy
|
|
86
|
+
* path unchanged. Malformed optional fields are dropped, not fatal.
|
|
87
|
+
*/
|
|
88
|
+
export function parseManagedDeliveryConfig(
|
|
89
|
+
payload: unknown
|
|
90
|
+
): ManagedDeliveryInstallationConfig | undefined {
|
|
91
|
+
if (!isRecord(payload)) {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
const doc = payload[MANAGED_DELIVERY_PAYLOAD_KEY];
|
|
95
|
+
if (!isRecord(doc)) {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
const matchedAutomationUuid = nonEmptyString(doc.matchedAutomationUuid);
|
|
99
|
+
if (matchedAutomationUuid === undefined) {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
matchedAutomationUuid,
|
|
104
|
+
installationConfigRef: nonEmptyString(doc.installationConfigRef),
|
|
105
|
+
managedType: nonEmptyString(doc.managedType),
|
|
106
|
+
name: nonEmptyString(doc.name),
|
|
107
|
+
postingMode: nonEmptyString(doc.postingMode),
|
|
108
|
+
managedConfig: isRecord(doc.managedConfig) ? doc.managedConfig : undefined,
|
|
109
|
+
triggerSnapshots: Array.isArray(doc.triggerSnapshots)
|
|
110
|
+
? doc.triggerSnapshots
|
|
111
|
+
: undefined,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Embed one matched installation config into the webhook payload the
|
|
117
|
+
* delivery activity POSTs to the channel. Pure; used by the control-plane
|
|
118
|
+
* delivery path and by channel tests building fixture envelopes.
|
|
119
|
+
*/
|
|
120
|
+
export function withManagedDeliveryConfig(
|
|
121
|
+
payload: Record<string, unknown>,
|
|
122
|
+
config: ManagedDeliveryInstallationConfig
|
|
123
|
+
): Record<string, unknown> {
|
|
124
|
+
return { ...payload, [MANAGED_DELIVERY_PAYLOAD_KEY]: config };
|
|
125
|
+
}
|
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
AgentRunner,
|
|
14
14
|
PrewarmOutcome,
|
|
15
15
|
RunnerPrewarmRequest,
|
|
16
|
+
SteerAckOutcome,
|
|
16
17
|
} from "./sdk-runner.js";
|
|
17
18
|
|
|
18
19
|
export interface RuntimeDispatchingRunnerOptions {
|
|
@@ -45,6 +46,15 @@ export class RuntimeDispatchingRunner implements AgentRunner {
|
|
|
45
46
|
return this.options.sdkRunner.runTurn(request);
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
async steer(request: {
|
|
50
|
+
sessionId: string;
|
|
51
|
+
text: string;
|
|
52
|
+
}): Promise<SteerAckOutcome> {
|
|
53
|
+
return (
|
|
54
|
+
(await this.options.sdkRunner.steer?.(request)) ?? "revert_to_followup"
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
48
58
|
async prewarm(request: RunnerPrewarmRequest): Promise<PrewarmOutcome> {
|
|
49
59
|
// Prewarming is a local-harness concern; the engine only calls it for
|
|
50
60
|
// local-runtime agents.
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
createAgentPlatform,
|
|
17
17
|
type LocalAgentStore,
|
|
18
18
|
type ModelSelection,
|
|
19
|
+
type Run,
|
|
19
20
|
type RunResult,
|
|
20
21
|
type SDKAgent,
|
|
21
22
|
} from "@cursor/sdk";
|
|
@@ -31,10 +32,18 @@ import {
|
|
|
31
32
|
import { mapSdkTokenUsage, usageTokenSum } from "./cloud-turn-cost.js";
|
|
32
33
|
import type { ConversationMirror } from "./conversation-mirror.js";
|
|
33
34
|
import { resolveApiKeySync } from "./cursor/credentials.js";
|
|
35
|
+
import { describeError } from "./describe-error.js";
|
|
36
|
+
|
|
37
|
+
/** Mirrors @cursor/sdk's SteerAckOutcome; the published dist may predate it. */
|
|
38
|
+
export type SteerAckOutcome = "complete_delivered" | "revert_to_followup";
|
|
34
39
|
|
|
35
40
|
/** Internal seam between the session engine and the Cursor SDK harness. */
|
|
36
41
|
export interface AgentRunner {
|
|
37
42
|
runTurn(request: RunnerTurnRequest): Promise<TurnOutcome>;
|
|
43
|
+
steer?(request: {
|
|
44
|
+
sessionId: string;
|
|
45
|
+
text: string;
|
|
46
|
+
}): Promise<SteerAckOutcome>;
|
|
38
47
|
/**
|
|
39
48
|
* Resolve a local workspace before any session needs it, so the first turn
|
|
40
49
|
* is not the one that pays for it. Best effort, hence the reported outcome:
|
|
@@ -193,6 +202,7 @@ function retryReason(
|
|
|
193
202
|
|
|
194
203
|
export class CursorSdkRunner implements AgentRunner {
|
|
195
204
|
private readonly agents = new Map<string, CachedAgentHandle>();
|
|
205
|
+
private readonly activeRuns = new Map<string, Run>();
|
|
196
206
|
private readonly stores = new Map<string, Promise<LocalAgentStore>>();
|
|
197
207
|
private readonly prewarmReleases: Array<() => Promise<void>> = [];
|
|
198
208
|
|
|
@@ -322,15 +332,9 @@ export class CursorSdkRunner implements AgentRunner {
|
|
|
322
332
|
) {
|
|
323
333
|
await request.onRequestId(run.requestId);
|
|
324
334
|
}
|
|
335
|
+
this.activeRuns.set(request.sessionId, run);
|
|
325
336
|
try {
|
|
326
337
|
const result = await run.wait();
|
|
327
|
-
// Mirror the conversation before replying: on hosted v2 the pod can
|
|
328
|
-
// be parked as soon as the execution reply lands, so a
|
|
329
|
-
// fire-and-forget write would race VM teardown. Failed turns still
|
|
330
|
-
// checkpoint the user message, so they persist too.
|
|
331
|
-
if (request.runtime === "local") {
|
|
332
|
-
await this.persistConversation(request, agent.agentId);
|
|
333
|
-
}
|
|
334
338
|
let outcome = toTurnOutcome(result);
|
|
335
339
|
if (outcome.status === "error") {
|
|
336
340
|
// An error result usually means the handle's transport is poisoned
|
|
@@ -353,7 +357,25 @@ export class CursorSdkRunner implements AgentRunner {
|
|
|
353
357
|
}
|
|
354
358
|
return outcome;
|
|
355
359
|
} finally {
|
|
360
|
+
if (this.activeRuns.get(request.sessionId) === run) {
|
|
361
|
+
this.activeRuns.delete(request.sessionId);
|
|
362
|
+
}
|
|
356
363
|
signal?.removeEventListener("abort", onAbort);
|
|
364
|
+
// Mirror the conversation before replying: on hosted v2 the pod can
|
|
365
|
+
// be parked as soon as the execution reply lands, so a
|
|
366
|
+
// fire-and-forget write would race VM teardown. In the `finally` so
|
|
367
|
+
// a `wait()` that THROWS (transport death mid-turn, an abort with
|
|
368
|
+
// no follow-up) still persists the checkpoints the turn already
|
|
369
|
+
// wrote — the store is local SQLite, safe to read on the throw
|
|
370
|
+
// path, and persistConversation swallows its own errors. Guarded
|
|
371
|
+
// here (not just inside the helper) so unmirrored turns keep their
|
|
372
|
+
// exact post-wait sequencing.
|
|
373
|
+
if (
|
|
374
|
+
request.runtime === "local" &&
|
|
375
|
+
this.options.conversationMirror !== undefined
|
|
376
|
+
) {
|
|
377
|
+
await this.persistConversation(request, agent.agentId);
|
|
378
|
+
}
|
|
357
379
|
}
|
|
358
380
|
} catch (error) {
|
|
359
381
|
// Drop the cached agent so a dead Connect/HTTP2 session cannot poison
|
|
@@ -363,6 +385,28 @@ export class CursorSdkRunner implements AgentRunner {
|
|
|
363
385
|
}
|
|
364
386
|
}
|
|
365
387
|
|
|
388
|
+
async steer(request: {
|
|
389
|
+
sessionId: string;
|
|
390
|
+
text: string;
|
|
391
|
+
}): Promise<SteerAckOutcome> {
|
|
392
|
+
const run = this.activeRuns.get(request.sessionId) as
|
|
393
|
+
| (Run & {
|
|
394
|
+
steer?: (text: string) => Promise<SteerAckOutcome>;
|
|
395
|
+
})
|
|
396
|
+
| undefined;
|
|
397
|
+
if (run?.steer === undefined) {
|
|
398
|
+
return "revert_to_followup";
|
|
399
|
+
}
|
|
400
|
+
try {
|
|
401
|
+
return await run.steer(request.text);
|
|
402
|
+
} catch (error) {
|
|
403
|
+
this.log(
|
|
404
|
+
`[agent-sdk] session ${request.sessionId}: steer failed: ${describeError(error)}`
|
|
405
|
+
);
|
|
406
|
+
return "revert_to_followup";
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
366
410
|
/**
|
|
367
411
|
* Build the local executor for a workspace before any session asks for it.
|
|
368
412
|
* The warmed executor is reference counted, so the lease is held until
|
|
@@ -484,7 +528,11 @@ export class CursorSdkRunner implements AgentRunner {
|
|
|
484
528
|
private async buildLocalOptions(
|
|
485
529
|
request: RunnerPrewarmRequest
|
|
486
530
|
): Promise<AgentOptions> {
|
|
487
|
-
|
|
531
|
+
// Not resolveStore: an injected store is used synchronously (no
|
|
532
|
+
// microtask, no thenable assimilation), exactly as before the mirror
|
|
533
|
+
// existed.
|
|
534
|
+
const store =
|
|
535
|
+
this.options.store ?? (await this.getStore(request.runnerStateDir));
|
|
488
536
|
return {
|
|
489
537
|
...this.buildSharedOptions(request),
|
|
490
538
|
// The built-in tool allowlist rides AgentOptions, not `local`, and is
|
package/src/internal/server.ts
CHANGED
|
@@ -1286,6 +1286,8 @@ function mountAgentRoutes(
|
|
|
1286
1286
|
channelId,
|
|
1287
1287
|
hostedSessionId,
|
|
1288
1288
|
}),
|
|
1289
|
+
drainSessionDurability: (sessionId) =>
|
|
1290
|
+
engine.drainSessionDurability(sessionId),
|
|
1289
1291
|
});
|
|
1290
1292
|
}
|
|
1291
1293
|
// Playground lists/opens Slack (and other channel) sessions across
|
|
@@ -136,10 +136,11 @@ import {
|
|
|
136
136
|
SCHEDULE_CHANNEL_PREFIX,
|
|
137
137
|
} from "./prompt-context.js";
|
|
138
138
|
import { assertCloudCanHonorAccountServersFilters } from "./resolved-connections.js";
|
|
139
|
-
import type { AgentRunner } from "./sdk-runner.js";
|
|
139
|
+
import type { AgentRunner, SteerAckOutcome } from "./sdk-runner.js";
|
|
140
140
|
import { sessionExternalLinks } from "./session-links.js";
|
|
141
141
|
import { SessionStore } from "./session-store.js";
|
|
142
142
|
import { syncSkillsToStore } from "./skills-sync.js";
|
|
143
|
+
import { steeringEnabled } from "./steering.js";
|
|
143
144
|
import { StorageCoordinator } from "./storage-coordinator.js";
|
|
144
145
|
import type { FrameworkSessionStore } from "./storage-roles.js";
|
|
145
146
|
import { decideToolCall, resolveEffect } from "./tool-policy.js";
|
|
@@ -698,6 +699,9 @@ export class SessionEngine {
|
|
|
698
699
|
) {
|
|
699
700
|
return;
|
|
700
701
|
}
|
|
702
|
+
if (steeringEnabled()) {
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
701
705
|
try {
|
|
702
706
|
await materializeSlackNudgeCarrier({
|
|
703
707
|
harnessCwd,
|
|
@@ -1359,18 +1363,52 @@ export class SessionEngine {
|
|
|
1359
1363
|
});
|
|
1360
1364
|
}
|
|
1361
1365
|
|
|
1366
|
+
private async steerIntoRunningTurn(
|
|
1367
|
+
sessionId: string,
|
|
1368
|
+
message: string
|
|
1369
|
+
): Promise<SteerAckOutcome> {
|
|
1370
|
+
if (this.runner.steer === undefined) {
|
|
1371
|
+
return "revert_to_followup";
|
|
1372
|
+
}
|
|
1373
|
+
try {
|
|
1374
|
+
return await this.runner.steer({ sessionId, text: message });
|
|
1375
|
+
} catch (error) {
|
|
1376
|
+
this.logger(
|
|
1377
|
+
`[session] steer failed for ${sessionId}: ${describeError(error)}`
|
|
1378
|
+
);
|
|
1379
|
+
return "revert_to_followup";
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1362
1383
|
private async admitTurn(
|
|
1363
1384
|
sessionId: string,
|
|
1364
1385
|
message: string,
|
|
1365
1386
|
options: AdmitTurnOptions = {}
|
|
1366
1387
|
): Promise<boolean> {
|
|
1367
1388
|
const admission = options.admission ?? "preempt";
|
|
1389
|
+
if (
|
|
1390
|
+
admission === "coalesce" &&
|
|
1391
|
+
steeringEnabled() &&
|
|
1392
|
+
this.isTurnRunning(sessionId)
|
|
1393
|
+
) {
|
|
1394
|
+
const toolCtx = this.activeToolContexts.get(sessionId);
|
|
1395
|
+
const steerOutcome = await this.steerIntoRunningTurn(sessionId, message);
|
|
1396
|
+
if (steerOutcome === "complete_delivered") {
|
|
1397
|
+
this.logger(`[session] steered into running turn ${sessionId}`);
|
|
1398
|
+
if (toolCtx !== undefined) {
|
|
1399
|
+
toolCtx.emit({ type: "message.received", data: { text: message } });
|
|
1400
|
+
toolCtx.emit({ type: "nudge.delivered", data: {} });
|
|
1401
|
+
}
|
|
1402
|
+
return true;
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
// Re-checked after the steer: a turn that ended during it falls through
|
|
1406
|
+
// and this message starts the next turn itself. Enqueueing here instead
|
|
1407
|
+
// would orphan it — the completion drain already ran.
|
|
1368
1408
|
if (admission === "coalesce" && this.isTurnRunning(sessionId)) {
|
|
1369
1409
|
this.enqueueCoalesced(sessionId, {
|
|
1370
1410
|
message,
|
|
1371
|
-
|
|
1372
|
-
? {}
|
|
1373
|
-
: { sourceTs: options.sourceTs }),
|
|
1411
|
+
sourceTs: options.sourceTs,
|
|
1374
1412
|
});
|
|
1375
1413
|
this.logger(`[session] coalesce enqueue ${sessionId}`);
|
|
1376
1414
|
return true;
|
|
@@ -3394,6 +3432,28 @@ export class SessionEngine {
|
|
|
3394
3432
|
// Session handles
|
|
3395
3433
|
// ==========================================================================
|
|
3396
3434
|
|
|
3435
|
+
/**
|
|
3436
|
+
* Wait until everything this session has already emitted is durably
|
|
3437
|
+
* mirrored: its dispatch chain first (the chain's tail runs the turn-end
|
|
3438
|
+
* storage flush, so the record carries the boundary event's handler
|
|
3439
|
+
* mutations), then the storage delivery queue. Hosted v2 awaits this
|
|
3440
|
+
* before the execution reply — the pod can be parked the moment the
|
|
3441
|
+
* reply lands, and an unflushed session record would strand a
|
|
3442
|
+
* conversation whose blobs persisted but whose record (which selects the
|
|
3443
|
+
* conversation to hydrate) never reached the sink. No-op without a
|
|
3444
|
+
* storage mirror.
|
|
3445
|
+
*/
|
|
3446
|
+
async drainSessionDurability(sessionId: string): Promise<void> {
|
|
3447
|
+
if (this.storage === undefined) {
|
|
3448
|
+
return;
|
|
3449
|
+
}
|
|
3450
|
+
const chain = this.dispatchChains.get(sessionId);
|
|
3451
|
+
if (chain !== undefined) {
|
|
3452
|
+
await chain.catch(() => {});
|
|
3453
|
+
}
|
|
3454
|
+
await this.storage.whenIdle();
|
|
3455
|
+
}
|
|
3456
|
+
|
|
3397
3457
|
async waitForCompletion(sessionId: string): Promise<TurnOutcome> {
|
|
3398
3458
|
// Drain admission first so pendingTurns → activeTurns is visible before we
|
|
3399
3459
|
// decide the session is idle (reminders use this for queueBehindTurn).
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deploy-time switch between the two mid-turn follow-up carriers.
|
|
3
|
+
*
|
|
4
|
+
* With ENABLE_STEERING set ("1"/"true"), a Slack follow-up arriving mid-turn
|
|
5
|
+
* is steered into the running turn via Run.steer and the nudge-carrier file
|
|
6
|
+
* store is left uninstalled. Unset (the default), the pre-steering behavior
|
|
7
|
+
* is fully preserved: the carrier is materialized and follow-ups ride the
|
|
8
|
+
* nudge store to the running turn's next tool boundary.
|
|
9
|
+
*
|
|
10
|
+
* Read per call rather than cached so tests can flip it, and because this is
|
|
11
|
+
* a rollout switch: once steering has proven out in production the flag and
|
|
12
|
+
* the carrier both go away.
|
|
13
|
+
*/
|
|
14
|
+
export function steeringEnabled(): boolean {
|
|
15
|
+
const value = process.env.ENABLE_STEERING?.trim().toLowerCase();
|
|
16
|
+
return value === "1" || value === "true";
|
|
17
|
+
}
|
|
@@ -74,13 +74,16 @@ export interface CursorHostedStorageOptions {
|
|
|
74
74
|
* `https://api.cursor.com`).
|
|
75
75
|
*/
|
|
76
76
|
baseUrl?: string;
|
|
77
|
+
/** Bearer credential (default: `CURSOR_API_KEY`). */
|
|
78
|
+
apiKey?: string;
|
|
77
79
|
/**
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
80
|
+
* Lazy credential, re-resolved on every call and preferred over
|
|
81
|
+
* {@link apiKey}: hosted freeze snapshots listen unsigned and bind the
|
|
82
|
+
* key file after serve start, so a value captured at construct time
|
|
83
|
+
* would be permanently absent. Used by the platform-selected v2
|
|
84
|
+
* defaults; kept separate so `apiKey` stays a plain string for authors.
|
|
82
85
|
*/
|
|
83
|
-
|
|
86
|
+
resolveApiKey?: () => string | undefined;
|
|
84
87
|
/** Injected for tests. */
|
|
85
88
|
fetch?: typeof fetch;
|
|
86
89
|
}
|
|
@@ -185,8 +188,7 @@ function toCasResult(response: KvCasResponse): KvCasResult {
|
|
|
185
188
|
}
|
|
186
189
|
|
|
187
190
|
function resolveApiKey(options: CursorHostedStorageOptions): string {
|
|
188
|
-
const configured =
|
|
189
|
-
typeof options.apiKey === "function" ? options.apiKey() : options.apiKey;
|
|
191
|
+
const configured = options.resolveApiKey?.() ?? options.apiKey;
|
|
190
192
|
const key = (configured ?? process.env.CURSOR_API_KEY)?.trim();
|
|
191
193
|
if (key === undefined || key === "") {
|
|
192
194
|
throw new Error(
|
package/src/types.ts
CHANGED
|
@@ -1262,7 +1262,8 @@ export type ConnectionConfig = (
|
|
|
1262
1262
|
* Local runtime only, like all server tools: a `runtime: "cloud"` agent
|
|
1263
1263
|
* fails discovery, and hybrid per-send cloud sessions refuse the turn.
|
|
1264
1264
|
* The listing runs once per turn; point this at endpoints that can
|
|
1265
|
-
* answer `tools/list` cheaply.
|
|
1265
|
+
* answer `tools/list` cheaply. Tools whose MCP `annotations.title` is
|
|
1266
|
+
* `do-not-call` are omitted (vendor host-UI / MCP Apps helpers).
|
|
1266
1267
|
*/
|
|
1267
1268
|
advertiseTools?: boolean;
|
|
1268
1269
|
/**
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Code wiki
|
|
2
|
+
|
|
3
|
+
Keeps `wiki/` pages current after a pull request merges. Opens a
|
|
4
|
+
follow-up PR against that merge's base when a durable fact changed.
|
|
5
|
+
Skips chores. Same-base updates stack on `wiki/<base>`.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx @cursor/july init ./code-wiki --template code-wiki
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`init` asks which repos to watch. Skip the prompt with
|
|
12
|
+
`--var repos=owner/repo`.
|
|
13
|
+
|
|
14
|
+
## Run
|
|
15
|
+
|
|
16
|
+
Connect GitHub in Cursor for those repos. Then sign the host in:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
agent-sdk login
|
|
20
|
+
agent-sdk dev
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Events and API calls use your Cursor GitHub connection. Replay a merged PR:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
agent-sdk github replay https://github.com/owner/repo/pull/1 --dir . --action closed
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Files
|
|
30
|
+
|
|
31
|
+
- `agent/lib/repos.ts`: watched repos
|
|
32
|
+
- `agent/lib/wiki.ts`: wiki path rules
|
|
33
|
+
- `agent/instructions.md`: what counts as a wiki update
|
|
34
|
+
- `evals/wiki.eval.ts`: skip and update smoke cases
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { HostGitHubClient } from "@cursor/july";
|
|
2
|
+
import {
|
|
3
|
+
defaultGitHubAuth,
|
|
4
|
+
githubChannel,
|
|
5
|
+
} from "@cursor/july/channels/github";
|
|
6
|
+
import { REPOS, watches } from "../lib/repos.js";
|
|
7
|
+
import { WIKI_DIR, isWikiPath } from "../lib/wiki.js";
|
|
8
|
+
|
|
9
|
+
const MAX_DIFF_CHARS = 150_000;
|
|
10
|
+
const MAX_WIKI_PAGES = 40;
|
|
11
|
+
const LIST_FILES_CAP = 3000;
|
|
12
|
+
|
|
13
|
+
type Octokit = Awaited<ReturnType<HostGitHubClient["getOctokit"]>>;
|
|
14
|
+
|
|
15
|
+
export default githubChannel({
|
|
16
|
+
cursorAccount: { repos: REPOS, permissions: "contents-write" },
|
|
17
|
+
deliverReplies: false,
|
|
18
|
+
onComment: () => null,
|
|
19
|
+
onPullRequest: async (ctx, pr) => {
|
|
20
|
+
if (!watches(ctx.repository.fullName)) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
if (pr.action !== "closed" || !pr.merged) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
if (pr.headRef.startsWith(`${WIKI_DIR}/`)) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const octokit = await ctx.github.getOctokit();
|
|
30
|
+
const pull = {
|
|
31
|
+
owner: ctx.repository.owner,
|
|
32
|
+
repo: ctx.repository.name,
|
|
33
|
+
pull_number: pr.number,
|
|
34
|
+
};
|
|
35
|
+
const listed = await octokit.paginate(octokit.rest.pulls.listFiles, {
|
|
36
|
+
...pull,
|
|
37
|
+
per_page: 100,
|
|
38
|
+
});
|
|
39
|
+
const files = listed.map((file) => file.filename);
|
|
40
|
+
if (
|
|
41
|
+
files.length > 0 &&
|
|
42
|
+
files.length < LIST_FILES_CAP &&
|
|
43
|
+
files.every(isWikiPath)
|
|
44
|
+
) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
auth: defaultGitHubAuth(ctx),
|
|
49
|
+
context: [
|
|
50
|
+
"This pull request merged. Read pr/files.txt, pr/diff.patch, and wiki/.",
|
|
51
|
+
],
|
|
52
|
+
async workspaceFiles() {
|
|
53
|
+
const [diff, wiki] = await Promise.all([
|
|
54
|
+
octokit.rest.pulls.get({ ...pull, mediaType: { format: "diff" } }),
|
|
55
|
+
loadWiki(octokit, pull.owner, pull.repo, pr.baseRef),
|
|
56
|
+
]);
|
|
57
|
+
const text = String(diff.data);
|
|
58
|
+
return {
|
|
59
|
+
"pr/base.ref": `${pr.baseRef}\n`,
|
|
60
|
+
"pr/files.txt": `${files.join("\n")}\n`,
|
|
61
|
+
"pr/diff.patch":
|
|
62
|
+
text.length > MAX_DIFF_CHARS
|
|
63
|
+
? `${text.slice(0, MAX_DIFF_CHARS)}\n…[truncated]\n`
|
|
64
|
+
: text,
|
|
65
|
+
...wiki,
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
async function loadWiki(
|
|
73
|
+
octokit: Octokit,
|
|
74
|
+
owner: string,
|
|
75
|
+
repo: string,
|
|
76
|
+
ref: string
|
|
77
|
+
): Promise<Record<string, string>> {
|
|
78
|
+
const listed = await readContents(octokit, {
|
|
79
|
+
owner,
|
|
80
|
+
repo,
|
|
81
|
+
path: WIKI_DIR,
|
|
82
|
+
ref,
|
|
83
|
+
});
|
|
84
|
+
if (!Array.isArray(listed)) {
|
|
85
|
+
return { "wiki/pages.txt": "" };
|
|
86
|
+
}
|
|
87
|
+
const markdown = listed.filter(
|
|
88
|
+
(item) => item.type === "file" && item.name.endsWith(".md")
|
|
89
|
+
);
|
|
90
|
+
const truncated = markdown.length > MAX_WIKI_PAGES;
|
|
91
|
+
const files = markdown.slice(0, MAX_WIKI_PAGES);
|
|
92
|
+
const pages: Record<string, string> = {};
|
|
93
|
+
await Promise.all(
|
|
94
|
+
files.map(async (file) => {
|
|
95
|
+
const body = await readContents(octokit, {
|
|
96
|
+
owner,
|
|
97
|
+
repo,
|
|
98
|
+
path: `${WIKI_DIR}/${file.name}`,
|
|
99
|
+
ref,
|
|
100
|
+
});
|
|
101
|
+
if (
|
|
102
|
+
body === undefined ||
|
|
103
|
+
Array.isArray(body) ||
|
|
104
|
+
body.encoding !== "base64" ||
|
|
105
|
+
typeof body.content !== "string"
|
|
106
|
+
) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
pages[`${WIKI_DIR}/${file.name}`] = Buffer.from(
|
|
110
|
+
body.content,
|
|
111
|
+
"base64"
|
|
112
|
+
).toString("utf8");
|
|
113
|
+
})
|
|
114
|
+
);
|
|
115
|
+
const names = Object.keys(pages).sort();
|
|
116
|
+
pages["wiki/pages.txt"] = truncated
|
|
117
|
+
? `${names.join("\n")}\n…[truncated]\n`
|
|
118
|
+
: `${names.join("\n")}\n`;
|
|
119
|
+
return pages;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function readContents(
|
|
123
|
+
octokit: Octokit,
|
|
124
|
+
args: { owner: string; repo: string; path: string; ref: string }
|
|
125
|
+
) {
|
|
126
|
+
try {
|
|
127
|
+
const { data } = await octokit.rest.repos.getContent(args);
|
|
128
|
+
return data;
|
|
129
|
+
} catch (error) {
|
|
130
|
+
if (isNotFound(error)) {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function isNotFound(error: unknown): boolean {
|
|
138
|
+
return (
|
|
139
|
+
typeof error === "object" &&
|
|
140
|
+
error !== null &&
|
|
141
|
+
"status" in error &&
|
|
142
|
+
error.status === 404
|
|
143
|
+
);
|
|
144
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Code wiki
|
|
2
|
+
|
|
3
|
+
You keep the in-repo wiki current after a pull request merges.
|
|
4
|
+
|
|
5
|
+
## Read
|
|
6
|
+
|
|
7
|
+
The workspace has `pr/files.txt`, `pr/diff.patch`, and `wiki/`.
|
|
8
|
+
`wiki/pages.txt` lists existing pages. Missing pages mean the wiki is
|
|
9
|
+
empty. Read the file list and the pages that own the change. Use the
|
|
10
|
+
diff only to test a fact.
|
|
11
|
+
|
|
12
|
+
## Decide
|
|
13
|
+
|
|
14
|
+
Update only when the merge changes a durable fact someone would look
|
|
15
|
+
up later: how a system works, a public or operator contract, a default
|
|
16
|
+
or ownership change, a removed feature.
|
|
17
|
+
|
|
18
|
+
An empty wiki is not a license to document the whole repo. Create a
|
|
19
|
+
page only for a durable contract in this merge.
|
|
20
|
+
|
|
21
|
+
Skip lint, format, lockfiles, dependency pins, tests-only, cherry-picks,
|
|
22
|
+
backports, telemetry-only changes, one-off bug fixes that leave the
|
|
23
|
+
described behavior the same, knobs the wiki never mentioned, and facts
|
|
24
|
+
the matching page already states.
|
|
25
|
+
|
|
26
|
+
Do not write a changelog or a PR list.
|
|
27
|
+
|
|
28
|
+
## Write
|
|
29
|
+
|
|
30
|
+
Call `apply_wiki` once.
|
|
31
|
+
|
|
32
|
+
- Skip: `{ verdict: "skip", reason }` with one line.
|
|
33
|
+
- Update: `{ verdict: "update", reason, pages }` with full page bodies.
|
|
34
|
+
|
|
35
|
+
Pages stay under `wiki/`, flat, `*.md`. Keep `_index.md` in sync when
|
|
36
|
+
you add a page. Each catalog line names the page and the paths it owns.
|
|
37
|
+
Short declarative sentences. Edit only the stale or missing facts.
|
|
38
|
+
|
|
39
|
+
Reply with `skip` or `update` and the reason.
|