@sweny-ai/core 0.1.170 → 0.1.172
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/dist/agent-env.d.ts +2 -1
- package/dist/agent-env.js +9 -3
- package/dist/browser.d.ts +2 -0
- package/dist/browser.js +3 -0
- package/dist/budget.js +5 -1
- package/dist/cli/check.js +12 -12
- package/dist/cli/comment-output.d.ts +4 -2
- package/dist/cli/comment-output.js +16 -8
- package/dist/cli/config.js +24 -24
- package/dist/cli/e2e.js +17 -17
- package/dist/cli/main.js +42 -26
- package/dist/cli/marketplace.js +3 -3
- package/dist/cli/new.js +11 -8
- package/dist/cli/output.d.ts +3 -2
- package/dist/cli/output.js +39 -37
- package/dist/cli/progress.d.ts +33 -0
- package/dist/cli/progress.js +69 -0
- package/dist/cli/publish.js +2 -2
- package/dist/cli/record.d.ts +66 -0
- package/dist/cli/record.js +344 -0
- package/dist/cli/renderer.d.ts +4 -0
- package/dist/cli/renderer.js +20 -13
- package/dist/cli/run-output.d.ts +13 -6
- package/dist/cli/run-output.js +36 -26
- package/dist/cli/runs.js +17 -11
- package/dist/cli/setup.js +7 -7
- package/dist/cli/style.d.ts +26 -0
- package/dist/cli/style.js +48 -0
- package/dist/cli/terminal.d.ts +47 -0
- package/dist/cli/terminal.js +123 -0
- package/dist/cli/ticket.d.ts +89 -0
- package/dist/cli/ticket.js +218 -0
- package/dist/cli/try.d.ts +19 -8
- package/dist/cli/try.js +97 -65
- package/dist/cli/upgrade.js +1 -1
- package/dist/executor.d.ts +8 -1
- package/dist/executor.js +20 -2
- package/dist/git-credentials.d.ts +106 -0
- package/dist/git-credentials.js +322 -0
- package/dist/harness/acp.js +3 -0
- package/dist/harness/claude-code.d.ts +7 -0
- package/dist/harness/claude-code.js +44 -3
- package/dist/harness/codex.js +3 -0
- package/dist/harness/pi.js +3 -0
- package/dist/harness/policy.d.ts +5 -0
- package/dist/harness/policy.js +14 -0
- package/dist/harness/sandbox-wrapper.d.ts +9 -2
- package/dist/harness/sandbox-wrapper.js +14 -4
- package/dist/harness/types.d.ts +10 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/mermaid.d.ts +10 -5
- package/dist/mermaid.js +11 -13
- package/dist/safe-outputs.js +5 -0
- package/dist/skills/git-push.d.ts +62 -0
- package/dist/skills/git-push.js +132 -0
- package/dist/skills/github.d.ts +17 -0
- package/dist/skills/github.js +23 -0
- package/dist/theme.d.ts +185 -0
- package/dist/theme.js +183 -0
- package/dist/workflows/browser.js +2 -2
- package/dist/workflows/implement.yml +2 -1
- package/dist/workflows/triage.yml +2 -1
- package/package.json +6 -1
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
* host network except through that proxy.
|
|
20
20
|
* - filesystem: the workspace (cwd) and a scratch HOME are writable; the rest
|
|
21
21
|
* of the filesystem is read-only; the operator's credential files are
|
|
22
|
-
* unreadable
|
|
22
|
+
* unreadable, and so is a git credential the checkout persisted when the
|
|
23
|
+
* node is read-only or staged (#473, `denyRead`). Sibling homes under the same configured scratch root are
|
|
23
24
|
* hidden; only this run's HOME is exposed. Dry runs (`readOnly`) leave the
|
|
24
25
|
* workspace read-only too. All concurrent credential-bearing adapters must
|
|
25
26
|
* use the same scratch root; different roots are not mutually isolated.
|
|
@@ -126,10 +127,14 @@ export function buildSrtSettings(req, opts) {
|
|
|
126
127
|
// must stay hidden too. srt allowRead carves out only our own HOME.
|
|
127
128
|
if (opts.isolationRoot)
|
|
128
129
|
denyRead.push(opts.isolationRoot);
|
|
130
|
+
// #473: persisted git credentials. Also unwritable, so a writable workspace
|
|
131
|
+
// cannot rename one out from under its read deny.
|
|
132
|
+
const masked = [...new Set((req.denyRead ?? []).map(real))].filter((p) => exists(p));
|
|
133
|
+
denyRead.push(...masked.filter((p) => !denyRead.includes(p)));
|
|
129
134
|
const allowWrite = req.readOnly ? [opts.home] : [real(req.cwd), opts.home];
|
|
130
135
|
return {
|
|
131
136
|
network: { allowedDomains, deniedDomains: [], strictAllowlist: true, allowLocalBinding: false },
|
|
132
|
-
filesystem: { denyRead, allowRead: opts.isolationRoot ? [opts.home] : [], allowWrite, denyWrite:
|
|
137
|
+
filesystem: { denyRead, allowRead: opts.isolationRoot ? [opts.home] : [], allowWrite, denyWrite: masked },
|
|
133
138
|
};
|
|
134
139
|
}
|
|
135
140
|
/** Env for the wrapped process: the adapter's env with HOME, XDG and temp dirs moved into the scratch HOME. */
|
|
@@ -149,7 +154,7 @@ export function scratchEnv(env, home) {
|
|
|
149
154
|
export class SrtSandboxWrapper {
|
|
150
155
|
opts;
|
|
151
156
|
backend = "srt";
|
|
152
|
-
provides = { sandbox: true, egress: true, readOnlyMount: true };
|
|
157
|
+
provides = { sandbox: true, egress: true, readOnlyMount: true, readDeny: true };
|
|
153
158
|
constructor(opts) {
|
|
154
159
|
this.opts = opts;
|
|
155
160
|
}
|
|
@@ -335,7 +340,12 @@ export async function prepareAgentSpawn(opts) {
|
|
|
335
340
|
...parseList(env.SWENY_SANDBOX_ALLOWED_DOMAINS),
|
|
336
341
|
...(opts.harnessEgress ?? []),
|
|
337
342
|
];
|
|
338
|
-
const wrapped = await wrapper.wrap({
|
|
343
|
+
const wrapped = await wrapper.wrap({
|
|
344
|
+
...opts.spawn,
|
|
345
|
+
egress,
|
|
346
|
+
readOnly: policy.readOnly,
|
|
347
|
+
...(policy.gitCredentials && policy.gitCredentials.length > 0 ? { denyRead: policy.gitCredentials } : {}),
|
|
348
|
+
});
|
|
339
349
|
const { home, cleanup, ...spawn } = wrapped;
|
|
340
350
|
return { degraded: gate.degraded, spawn, wrappedBy: wrapper.backend, home, cleanup };
|
|
341
351
|
}
|
package/dist/harness/types.d.ts
CHANGED
|
@@ -97,6 +97,14 @@ export interface NodePolicy {
|
|
|
97
97
|
* adapters, which know whether the run is sandboxed.
|
|
98
98
|
*/
|
|
99
99
|
stagedWrite?: boolean;
|
|
100
|
+
/**
|
|
101
|
+
* Files holding a persisted git credential (#473: the token
|
|
102
|
+
* `actions/checkout` writes by default) that this node's agent must not
|
|
103
|
+
* read. Set by adapters for a read-only node or a staged write node when
|
|
104
|
+
* the checkout has one (see git-credentials.ts). The process wrapper denies
|
|
105
|
+
* reads of them; without it, reported, and refused under `strict`.
|
|
106
|
+
*/
|
|
107
|
+
gitCredentials?: string[];
|
|
100
108
|
}
|
|
101
109
|
/** Process-level wrappers the host provides (OS sandbox, egress proxy, read-only mount). */
|
|
102
110
|
export interface PolicyWrappers {
|
|
@@ -104,6 +112,8 @@ export interface PolicyWrappers {
|
|
|
104
112
|
sandbox?: boolean;
|
|
105
113
|
egress?: boolean;
|
|
106
114
|
readOnlyMount?: boolean;
|
|
115
|
+
/** The wrapper can make named files unreadable to the agent (srt `denyRead`). */
|
|
116
|
+
readDeny?: boolean;
|
|
107
117
|
}
|
|
108
118
|
export interface PolicyGateResult {
|
|
109
119
|
/** Every opinion this run could not honor natively. Empty for Claude Code. */
|
package/dist/index.d.ts
CHANGED
|
@@ -61,5 +61,7 @@ export { resolveTemplates, loadAdditionalContext, loadTemplate } from "./templat
|
|
|
61
61
|
export type { Templates } from "./templates.js";
|
|
62
62
|
export { toMermaid, toMermaidBlock } from "./mermaid.js";
|
|
63
63
|
export type { MermaidOptions, NodeStatus } from "./mermaid.js";
|
|
64
|
+
export { SWENY_TAGLINE, PALETTE, ROLE_COLORS, SURFACE_COLORS, GLYPHS, SPINNER_FRAMES, SPINNER_INTERVAL_MS, TICKET_BOX, MERMAID_CLASS_DEFS, MERMAID_EDGE_STYLES, } from "./theme.js";
|
|
65
|
+
export type { ColorRole, GlyphKey, GlyphSet, TicketBox, DiagramNodeStatus } from "./theme.js";
|
|
64
66
|
export { loadConfigFile } from "./cli/config-file.js";
|
|
65
67
|
export type { FileConfig } from "./cli/config-file.js";
|
package/dist/index.js
CHANGED
|
@@ -62,5 +62,7 @@ export { buildWorkflow, refineWorkflow } from "./workflow-builder.js";
|
|
|
62
62
|
export { resolveTemplates, loadAdditionalContext, loadTemplate } from "./templates.js";
|
|
63
63
|
// Mermaid export
|
|
64
64
|
export { toMermaid, toMermaidBlock } from "./mermaid.js";
|
|
65
|
+
// Visual tokens (#479): palette, color roles, glyphs, diagram classDefs. Browser-safe.
|
|
66
|
+
export { SWENY_TAGLINE, PALETTE, ROLE_COLORS, SURFACE_COLORS, GLYPHS, SPINNER_FRAMES, SPINNER_INTERVAL_MS, TICKET_BOX, MERMAID_CLASS_DEFS, MERMAID_EDGE_STYLES, } from "./theme.js";
|
|
65
67
|
// Config file
|
|
66
68
|
export { loadConfigFile } from "./cli/config-file.js";
|
package/dist/mermaid.d.ts
CHANGED
|
@@ -10,11 +10,16 @@
|
|
|
10
10
|
* to highlight which edges were taken and show loop iterations.
|
|
11
11
|
*/
|
|
12
12
|
import type { Workflow, ExecutionTrace } from "./types.js";
|
|
13
|
-
|
|
13
|
+
import { type DiagramNodeStatus } from "./theme.js";
|
|
14
|
+
/**
|
|
15
|
+
* A node's state in a diagram. `pending` is applied automatically to every
|
|
16
|
+
* node missing from a non-empty `state` map (the run never reached it).
|
|
17
|
+
*/
|
|
18
|
+
export type NodeStatus = DiagramNodeStatus;
|
|
14
19
|
export interface MermaidOptions {
|
|
15
|
-
/** Execution state
|
|
20
|
+
/** Execution state. With a non-empty map, nodes missing from it render as `pending`. */
|
|
16
21
|
state?: Record<string, NodeStatus>;
|
|
17
|
-
/** Execution trace
|
|
22
|
+
/** Execution trace: highlights taken and not-taken edges and loop counts */
|
|
18
23
|
trace?: ExecutionTrace;
|
|
19
24
|
/** Graph direction: TB (top-bottom) or LR (left-right). Default: TB */
|
|
20
25
|
direction?: "TB" | "LR";
|
|
@@ -22,8 +27,8 @@ export interface MermaidOptions {
|
|
|
22
27
|
title?: string;
|
|
23
28
|
/**
|
|
24
29
|
* Override the `classDef` style body (the part after the class name) for
|
|
25
|
-
* specific statuses
|
|
26
|
-
*
|
|
30
|
+
* specific statuses. Unset statuses keep the brand defaults
|
|
31
|
+
* (`MERMAID_CLASS_DEFS` in theme.ts). SWEny's own surfaces pass none.
|
|
27
32
|
*/
|
|
28
33
|
classDefs?: Partial<Record<NodeStatus, string>>;
|
|
29
34
|
}
|
package/dist/mermaid.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* to highlight which edges were taken and show loop iterations.
|
|
11
11
|
*/
|
|
12
12
|
import { whenLabel } from "./when.js";
|
|
13
|
+
import { MERMAID_CLASS_DEFS, MERMAID_EDGE_STYLES } from "./theme.js";
|
|
13
14
|
/**
|
|
14
15
|
* Convert a Workflow to a Mermaid flowchart diagram string.
|
|
15
16
|
*
|
|
@@ -91,7 +92,7 @@ export function toMermaid(workflow, options = {}) {
|
|
|
91
92
|
edgeTakenCounts.set(key, (edgeTakenCounts.get(key) ?? 0) + 1);
|
|
92
93
|
}
|
|
93
94
|
}
|
|
94
|
-
// Edges
|
|
95
|
+
// Edges: track index for linkStyle directives
|
|
95
96
|
const takenIndices = [];
|
|
96
97
|
const notTakenIndices = [];
|
|
97
98
|
let edgeIndex = 0;
|
|
@@ -129,32 +130,29 @@ export function toMermaid(workflow, options = {}) {
|
|
|
129
130
|
}
|
|
130
131
|
edgeIndex++;
|
|
131
132
|
}
|
|
132
|
-
// Node status styling
|
|
133
|
+
// Node status styling (brand classDefs from theme.ts)
|
|
133
134
|
const statusNodes = groupByStatus(state);
|
|
134
135
|
if (statusNodes.size > 0) {
|
|
136
|
+
const unreached = Object.keys(workflow.nodes).filter((id) => !(id in state));
|
|
137
|
+
if (unreached.length > 0)
|
|
138
|
+
statusNodes.set("pending", [...(statusNodes.get("pending") ?? []), ...unreached]);
|
|
135
139
|
lines.push("");
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
success: "fill:#22c55e,stroke:#16a34a,color:#fff,stroke-width:2px",
|
|
139
|
-
failed: "fill:#ef4444,stroke:#dc2626,color:#fff,stroke-width:2px",
|
|
140
|
-
skipped: "fill:#6b7280,stroke:#4b5563,color:#fff,stroke-dasharray:5 5",
|
|
141
|
-
};
|
|
142
|
-
for (const status of ["current", "success", "failed", "skipped"]) {
|
|
143
|
-
lines.push(` classDef ${status} ${classDefs[status] ?? defaults[status]}`);
|
|
140
|
+
for (const status of ["current", "success", "failed", "skipped", "pending"]) {
|
|
141
|
+
lines.push(` classDef ${status} ${classDefs[status] ?? MERMAID_CLASS_DEFS[status]}`);
|
|
144
142
|
}
|
|
145
143
|
for (const [status, ids] of statusNodes) {
|
|
146
144
|
const sanitized = ids.map(aliasFor).join(",");
|
|
147
145
|
lines.push(` class ${sanitized} ${status}`);
|
|
148
146
|
}
|
|
149
147
|
}
|
|
150
|
-
// Edge styling
|
|
148
|
+
// Edge styling: taken edges bold brand blue, not-taken edges dashed slate
|
|
151
149
|
if (trace) {
|
|
152
150
|
lines.push("");
|
|
153
151
|
if (takenIndices.length > 0) {
|
|
154
|
-
lines.push(` linkStyle ${takenIndices.join(",")}
|
|
152
|
+
lines.push(` linkStyle ${takenIndices.join(",")} ${MERMAID_EDGE_STYLES.taken}`);
|
|
155
153
|
}
|
|
156
154
|
if (notTakenIndices.length > 0) {
|
|
157
|
-
lines.push(` linkStyle ${notTakenIndices.join(",")}
|
|
155
|
+
lines.push(` linkStyle ${notTakenIndices.join(",")} ${MERMAID_EDGE_STYLES.notTaken}`);
|
|
158
156
|
}
|
|
159
157
|
}
|
|
160
158
|
return lines.join("\n");
|
package/dist/safe-outputs.js
CHANGED
|
@@ -428,6 +428,11 @@ export async function applySafeOutputs(o) {
|
|
|
428
428
|
refuse("close needs a pinned issue", at);
|
|
429
429
|
continue;
|
|
430
430
|
}
|
|
431
|
+
// ...and the repository (or Linear team) must come from the declaration or the run, never from the agent's intent.
|
|
432
|
+
if (asked === "close" && !pinned) {
|
|
433
|
+
refuse("close needs a pinned repository", at);
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
431
436
|
state = asked;
|
|
432
437
|
}
|
|
433
438
|
if (via === "github" && issueRef !== undefined && !/^[1-9][0-9]*$/.test(issueRef)) {
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sweny-side branch push for `github_create_pr` (#473).
|
|
3
|
+
*
|
|
4
|
+
* With `actions/checkout`'s recommended `persist-credentials: false` the
|
|
5
|
+
* checkout holds no token, so the agent's own `git push` in the create_pr node
|
|
6
|
+
* has nothing to push with. sweny pushes the PR's head branch itself, from
|
|
7
|
+
* the sweny process, with the github skill's token (`GITHUB_TOKEN`), right
|
|
8
|
+
* before it opens the PR. The agent never holds the token.
|
|
9
|
+
*
|
|
10
|
+
* Guard rails: only a local branch that exists, only to `origin` when it is
|
|
11
|
+
* the PR's own repo, never force, never the base branch or the remote's
|
|
12
|
+
* default branch. The token goes to git through command-scope config in the
|
|
13
|
+
* child env (`GIT_CONFIG_*`), never argv, as an `http.<server>/.extraheader`
|
|
14
|
+
* that first resets any persisted one (so the job token is never sent twice).
|
|
15
|
+
* A push that fails is logged and the PR is still requested: the agent may
|
|
16
|
+
* already have pushed (persisted credentials, an `agent_env` grant).
|
|
17
|
+
*/
|
|
18
|
+
export interface GitRunResult {
|
|
19
|
+
code: number;
|
|
20
|
+
stdout: string;
|
|
21
|
+
stderr: string;
|
|
22
|
+
}
|
|
23
|
+
export type GitRunner = (args: string[], opts: {
|
|
24
|
+
cwd: string;
|
|
25
|
+
env: NodeJS.ProcessEnv;
|
|
26
|
+
}) => Promise<GitRunResult>;
|
|
27
|
+
export interface PushHeadOptions {
|
|
28
|
+
/** `owner/repo` the PR is for. */
|
|
29
|
+
repo: string;
|
|
30
|
+
/** The PR's head branch. */
|
|
31
|
+
head: string;
|
|
32
|
+
/** The PR's base branch. */
|
|
33
|
+
base: string;
|
|
34
|
+
/** The github skill's token. Without it, git uses whatever credential the checkout has. */
|
|
35
|
+
token?: string;
|
|
36
|
+
/** The checkout. Default: `process.cwd()`. */
|
|
37
|
+
cwd?: string;
|
|
38
|
+
env?: NodeJS.ProcessEnv;
|
|
39
|
+
/** Test seam. */
|
|
40
|
+
git?: GitRunner;
|
|
41
|
+
}
|
|
42
|
+
export interface PushHeadResult {
|
|
43
|
+
pushed: boolean;
|
|
44
|
+
/** A push ran (and `pushed` says whether it worked); false when a guard skipped it. */
|
|
45
|
+
attempted: boolean;
|
|
46
|
+
/** Why nothing was pushed, or why the push failed. Never carries the token. */
|
|
47
|
+
reason?: string;
|
|
48
|
+
}
|
|
49
|
+
/** `owner/repo` from a GitHub remote URL (https, ssh or scp form), lowercased, or undefined. */
|
|
50
|
+
export declare function repoFromRemote(url: string): {
|
|
51
|
+
host: string;
|
|
52
|
+
repo: string;
|
|
53
|
+
https: boolean;
|
|
54
|
+
} | undefined;
|
|
55
|
+
/** A branch name sweny will pass to git: plain ref characters, no option or traversal forms. */
|
|
56
|
+
export declare function isSafeBranch(name: string): boolean;
|
|
57
|
+
/** The command-scope git config that authenticates one push to `server` with `token`. */
|
|
58
|
+
export declare function pushAuthConfig(server: string, token: string, env: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
59
|
+
/**
|
|
60
|
+
* Push the PR's head branch to `origin` from the sweny process. Never throws.
|
|
61
|
+
*/
|
|
62
|
+
export declare function pushHeadBranch(opts: PushHeadOptions): Promise<PushHeadResult>;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sweny-side branch push for `github_create_pr` (#473).
|
|
3
|
+
*
|
|
4
|
+
* With `actions/checkout`'s recommended `persist-credentials: false` the
|
|
5
|
+
* checkout holds no token, so the agent's own `git push` in the create_pr node
|
|
6
|
+
* has nothing to push with. sweny pushes the PR's head branch itself, from
|
|
7
|
+
* the sweny process, with the github skill's token (`GITHUB_TOKEN`), right
|
|
8
|
+
* before it opens the PR. The agent never holds the token.
|
|
9
|
+
*
|
|
10
|
+
* Guard rails: only a local branch that exists, only to `origin` when it is
|
|
11
|
+
* the PR's own repo, never force, never the base branch or the remote's
|
|
12
|
+
* default branch. The token goes to git through command-scope config in the
|
|
13
|
+
* child env (`GIT_CONFIG_*`), never argv, as an `http.<server>/.extraheader`
|
|
14
|
+
* that first resets any persisted one (so the job token is never sent twice).
|
|
15
|
+
* A push that fails is logged and the PR is still requested: the agent may
|
|
16
|
+
* already have pushed (persisted credentials, an `agent_env` grant).
|
|
17
|
+
*/
|
|
18
|
+
import { execFile } from "node:child_process";
|
|
19
|
+
import { setBranchPusher } from "./github.js";
|
|
20
|
+
const defaultGit = (args, opts) => new Promise((resolve) => {
|
|
21
|
+
execFile("git", args, { cwd: opts.cwd, env: opts.env, timeout: 120_000 }, (err, stdout, stderr) => {
|
|
22
|
+
const code = err
|
|
23
|
+
? typeof err.code === "number"
|
|
24
|
+
? err.code
|
|
25
|
+
: 1
|
|
26
|
+
: 0;
|
|
27
|
+
resolve({ code, stdout: String(stdout ?? ""), stderr: String(stderr ?? "") });
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
/** `owner/repo` from a GitHub remote URL (https, ssh or scp form), lowercased, or undefined. */
|
|
31
|
+
export function repoFromRemote(url) {
|
|
32
|
+
const u = url.trim();
|
|
33
|
+
const https = /^https?:\/\/(?:[^@/]*@)?([^/:]+)(?::\d+)?\/(.+?)(?:\.git)?\/?$/i.exec(u);
|
|
34
|
+
if (https)
|
|
35
|
+
return { host: https[1].toLowerCase(), repo: https[2].toLowerCase(), https: true };
|
|
36
|
+
const ssh = /^ssh:\/\/(?:[^@/]*@)?([^/:]+)(?::\d+)?\/(.+?)(?:\.git)?\/?$/i.exec(u);
|
|
37
|
+
if (ssh)
|
|
38
|
+
return { host: ssh[1].toLowerCase(), repo: ssh[2].toLowerCase(), https: false };
|
|
39
|
+
const scp = /^(?:[^@/]+@)?([^/:]+):(.+?)(?:\.git)?\/?$/.exec(u);
|
|
40
|
+
if (scp)
|
|
41
|
+
return { host: scp[1].toLowerCase(), repo: scp[2].toLowerCase(), https: false };
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
/** A branch name sweny will pass to git: plain ref characters, no option or traversal forms. */
|
|
45
|
+
export function isSafeBranch(name) {
|
|
46
|
+
return (/^[A-Za-z0-9._/-]+$/.test(name) &&
|
|
47
|
+
!name.startsWith("-") &&
|
|
48
|
+
!name.startsWith("/") &&
|
|
49
|
+
!name.endsWith("/") &&
|
|
50
|
+
!name.endsWith(".lock") &&
|
|
51
|
+
!name.includes("..") &&
|
|
52
|
+
!name.includes("//"));
|
|
53
|
+
}
|
|
54
|
+
/** The command-scope git config that authenticates one push to `server` with `token`. */
|
|
55
|
+
export function pushAuthConfig(server, token, env) {
|
|
56
|
+
const out = { ...env };
|
|
57
|
+
const base = Number.parseInt(out.GIT_CONFIG_COUNT ?? "0", 10);
|
|
58
|
+
let n = Number.isFinite(base) && base > 0 ? base : 0;
|
|
59
|
+
const key = `http.${server.replace(/\/+$/, "")}/.extraheader`;
|
|
60
|
+
const basic = Buffer.from(`x-access-token:${token}`).toString("base64");
|
|
61
|
+
// An empty value resets the list, so a header the checkout persisted is not sent too.
|
|
62
|
+
for (const value of ["", `AUTHORIZATION: basic ${basic}`]) {
|
|
63
|
+
out[`GIT_CONFIG_KEY_${n}`] = key;
|
|
64
|
+
out[`GIT_CONFIG_VALUE_${n}`] = value;
|
|
65
|
+
n++;
|
|
66
|
+
}
|
|
67
|
+
out.GIT_CONFIG_COUNT = String(n);
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Push the PR's head branch to `origin` from the sweny process. Never throws.
|
|
72
|
+
*/
|
|
73
|
+
export async function pushHeadBranch(opts) {
|
|
74
|
+
const git = opts.git ?? defaultGit;
|
|
75
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
76
|
+
const env = { ...(opts.env ?? process.env), GIT_TERMINAL_PROMPT: "0" };
|
|
77
|
+
const { head, base } = opts;
|
|
78
|
+
if (!isSafeBranch(head))
|
|
79
|
+
return { pushed: false, attempted: false, reason: `head "${head}" is not a plain branch name` };
|
|
80
|
+
if (head === base)
|
|
81
|
+
return { pushed: false, attempted: false, reason: "head is the base branch" };
|
|
82
|
+
try {
|
|
83
|
+
const local = await git(["rev-parse", "--verify", "--quiet", `refs/heads/${head}^{commit}`], { cwd, env });
|
|
84
|
+
if (local.code !== 0)
|
|
85
|
+
return { pushed: false, attempted: false, reason: `no local branch ${head}` };
|
|
86
|
+
const remote = await git(["remote", "get-url", "origin"], { cwd, env });
|
|
87
|
+
if (remote.code !== 0)
|
|
88
|
+
return { pushed: false, attempted: false, reason: "no origin remote" };
|
|
89
|
+
const parsed = repoFromRemote(remote.stdout);
|
|
90
|
+
if (!parsed || parsed.repo !== opts.repo.toLowerCase()) {
|
|
91
|
+
return { pushed: false, attempted: false, reason: `origin is not ${opts.repo}` };
|
|
92
|
+
}
|
|
93
|
+
const originHead = await git(["symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"], { cwd, env });
|
|
94
|
+
const defaultBranch = originHead.code === 0 ? originHead.stdout.trim().replace(/^origin\//, "") : undefined;
|
|
95
|
+
if (head === defaultBranch || head === "main" || head === "master") {
|
|
96
|
+
return { pushed: false, attempted: false, reason: `head ${head} is the default branch` };
|
|
97
|
+
}
|
|
98
|
+
const server = (env.GITHUB_SERVER_URL || "https://github.com").replace(/\/+$/, "");
|
|
99
|
+
let serverHost;
|
|
100
|
+
try {
|
|
101
|
+
serverHost = new URL(server).host.toLowerCase();
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
serverHost = "github.com";
|
|
105
|
+
}
|
|
106
|
+
const pushEnv = opts.token && parsed.https && parsed.host === serverHost ? pushAuthConfig(server, opts.token, env) : env;
|
|
107
|
+
const r = await git(["push", "--porcelain", "origin", `refs/heads/${head}:refs/heads/${head}`], {
|
|
108
|
+
cwd,
|
|
109
|
+
env: pushEnv,
|
|
110
|
+
});
|
|
111
|
+
if (r.code !== 0) {
|
|
112
|
+
const why = (r.stderr || r.stdout).trim().split("\n").at(-1) ?? "";
|
|
113
|
+
return { pushed: false, attempted: true, reason: `git push failed: ${redact(why, opts.token)}` };
|
|
114
|
+
}
|
|
115
|
+
return { pushed: true, attempted: true };
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
return {
|
|
119
|
+
pushed: false,
|
|
120
|
+
attempted: false,
|
|
121
|
+
reason: redact(err instanceof Error ? err.message : String(err), opts.token),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function redact(text, token) {
|
|
126
|
+
if (!token)
|
|
127
|
+
return text;
|
|
128
|
+
const basic = Buffer.from(`x-access-token:${token}`).toString("base64");
|
|
129
|
+
return text.split(token).join("***").split(basic).join("***");
|
|
130
|
+
}
|
|
131
|
+
// The Node entry imports this module (executor.ts), which arms the push in `github_create_pr`.
|
|
132
|
+
setBranchPusher(pushHeadBranch);
|
package/dist/skills/github.d.ts
CHANGED
|
@@ -5,6 +5,23 @@
|
|
|
5
5
|
* ~800 lines → ~120 lines
|
|
6
6
|
*/
|
|
7
7
|
import type { Skill } from "../types.js";
|
|
8
|
+
/**
|
|
9
|
+
* Pushes a PR's head branch from the sweny process (#473). The Node entry
|
|
10
|
+
* registers it (skills/git-push.ts, imported by the executor); this module
|
|
11
|
+
* stays browser-safe, and without a pusher `github_create_pr` only calls the API.
|
|
12
|
+
*/
|
|
13
|
+
export type BranchPusher = (opts: {
|
|
14
|
+
repo: string;
|
|
15
|
+
head: string;
|
|
16
|
+
base: string;
|
|
17
|
+
token?: string;
|
|
18
|
+
}) => Promise<{
|
|
19
|
+
pushed: boolean;
|
|
20
|
+
attempted: boolean;
|
|
21
|
+
reason?: string;
|
|
22
|
+
}>;
|
|
23
|
+
/** Register (or clear, with `undefined`) the sweny-side branch pusher. */
|
|
24
|
+
export declare function setBranchPusher(fn: BranchPusher | undefined): void;
|
|
8
25
|
/**
|
|
9
26
|
* Encode an LLM-chosen repo file path for safe interpolation into a
|
|
10
27
|
* GitHub API request path.
|
package/dist/skills/github.js
CHANGED
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
* Replaces: source-control/github.ts + issue-tracking/github-issues.ts
|
|
5
5
|
* ~800 lines → ~120 lines
|
|
6
6
|
*/
|
|
7
|
+
let branchPusher;
|
|
8
|
+
/** Register (or clear, with `undefined`) the sweny-side branch pusher. */
|
|
9
|
+
export function setBranchPusher(fn) {
|
|
10
|
+
branchPusher = fn;
|
|
11
|
+
}
|
|
7
12
|
class GitHubApiError extends Error {
|
|
8
13
|
status;
|
|
9
14
|
body;
|
|
@@ -178,6 +183,24 @@ export const github = {
|
|
|
178
183
|
handler: async (input, ctx) => {
|
|
179
184
|
let pr;
|
|
180
185
|
let reused = false;
|
|
186
|
+
// #473: push the head branch from sweny with this skill's token, so the
|
|
187
|
+
// recommended `persist-credentials: false` checkout still ships the
|
|
188
|
+
// branch and the agent never holds a write token. Skipped unless the
|
|
189
|
+
// branch exists locally and origin is this repo; never forced.
|
|
190
|
+
if (branchPusher) {
|
|
191
|
+
const push = await branchPusher({
|
|
192
|
+
repo: input.repo,
|
|
193
|
+
head: input.head,
|
|
194
|
+
base: input.base ?? "main",
|
|
195
|
+
token: ctx.config.GITHUB_TOKEN,
|
|
196
|
+
});
|
|
197
|
+
if (push.pushed)
|
|
198
|
+
ctx.logger?.info?.(` github_create_pr: pushed ${input.head} to origin`);
|
|
199
|
+
else if (push.attempted)
|
|
200
|
+
ctx.logger?.warn?.(` github_create_pr: ${push.reason}; requesting the PR anyway`);
|
|
201
|
+
else
|
|
202
|
+
ctx.logger?.debug?.(` github_create_pr: no sweny-side push (${push.reason})`);
|
|
203
|
+
}
|
|
181
204
|
try {
|
|
182
205
|
pr = (await gh(`/repos/${input.repo}/pulls`, ctx, {
|
|
183
206
|
method: "POST",
|
package/dist/theme.d.ts
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SWEny visual tokens: one palette, one glyph set, one meaning per color.
|
|
3
|
+
*
|
|
4
|
+
* The single source for the CLI, the PR comment, the Mermaid diagrams, and
|
|
5
|
+
* Studio. Values come from docs/brand-guide.md. Browser-safe on purpose: no
|
|
6
|
+
* imports, no `process`, no chalk. Terminal painters live in `cli/style.ts`
|
|
7
|
+
* and are built from these tokens.
|
|
8
|
+
*
|
|
9
|
+
* Import from `@sweny-ai/core`, `@sweny-ai/core/browser`, or
|
|
10
|
+
* `@sweny-ai/core/theme`.
|
|
11
|
+
*/
|
|
12
|
+
/** The product line, as on the README and docs. */
|
|
13
|
+
export declare const SWENY_TAGLINE = "Workflows for coding agents. One set of rules, a receipt for every run.";
|
|
14
|
+
/** Raw brand palette (Tailwind names in comments). Prefer `ROLE_COLORS` in UI code. */
|
|
15
|
+
export declare const PALETTE: {
|
|
16
|
+
readonly blue300: "#93c5fd";
|
|
17
|
+
readonly blue400: "#60a5fa";
|
|
18
|
+
readonly blue500: "#3b82f6";
|
|
19
|
+
readonly blue600: "#2563eb";
|
|
20
|
+
readonly blue700: "#1d4ed8";
|
|
21
|
+
readonly blue900: "#1e3a5f";
|
|
22
|
+
readonly green400: "#4ade80";
|
|
23
|
+
readonly green600: "#16a34a";
|
|
24
|
+
readonly yellow400: "#facc15";
|
|
25
|
+
readonly yellow600: "#ca8a04";
|
|
26
|
+
readonly red400: "#f87171";
|
|
27
|
+
readonly red600: "#dc2626";
|
|
28
|
+
readonly red800: "#991b1b";
|
|
29
|
+
readonly slate50: "#f8fafc";
|
|
30
|
+
readonly slate100: "#f1f5f9";
|
|
31
|
+
readonly slate200: "#e2e8f0";
|
|
32
|
+
readonly slate400: "#94a3b8";
|
|
33
|
+
readonly slate500: "#64748b";
|
|
34
|
+
readonly slate600: "#475569";
|
|
35
|
+
readonly slate700: "#334155";
|
|
36
|
+
readonly slate800: "#1e293b";
|
|
37
|
+
readonly slate900: "#0f172a";
|
|
38
|
+
readonly surface: "#162032";
|
|
39
|
+
readonly white: "#ffffff";
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* One color per meaning. Every surface (terminal, PR comment, diagram,
|
|
43
|
+
* Studio) maps a meaning to the same token:
|
|
44
|
+
*
|
|
45
|
+
* - `brand` the SWEny mark, the policy stamp, the running spinner, current work
|
|
46
|
+
* - `success` a passed node or run
|
|
47
|
+
* - `warning` something ran weaker than asked (unscoped env, no sandbox, degraded)
|
|
48
|
+
* - `error` a failed node or run, a budget stop
|
|
49
|
+
* - `info` links and neutral highlights
|
|
50
|
+
* - `muted` labels, frames, pending and skipped work
|
|
51
|
+
*/
|
|
52
|
+
export declare const ROLE_COLORS: {
|
|
53
|
+
readonly brand: "#3b82f6";
|
|
54
|
+
readonly brandStrong: "#2563eb";
|
|
55
|
+
readonly brandDeep: "#1d4ed8";
|
|
56
|
+
readonly success: "#4ade80";
|
|
57
|
+
readonly warning: "#facc15";
|
|
58
|
+
readonly error: "#f87171";
|
|
59
|
+
readonly info: "#60a5fa";
|
|
60
|
+
readonly muted: "#64748b";
|
|
61
|
+
};
|
|
62
|
+
export type ColorRole = keyof typeof ROLE_COLORS;
|
|
63
|
+
/** Surface colors for dark and light UIs (Studio, docs). */
|
|
64
|
+
export declare const SURFACE_COLORS: {
|
|
65
|
+
readonly dark: {
|
|
66
|
+
readonly background: "#1e293b";
|
|
67
|
+
readonly surface: "#162032";
|
|
68
|
+
readonly border: "#334155";
|
|
69
|
+
readonly primary: "#3b82f6";
|
|
70
|
+
readonly text: "#f1f5f9";
|
|
71
|
+
readonly textSecondary: "#94a3b8";
|
|
72
|
+
readonly textMuted: "#64748b";
|
|
73
|
+
};
|
|
74
|
+
readonly light: {
|
|
75
|
+
readonly background: "#f8fafc";
|
|
76
|
+
readonly surface: "#ffffff";
|
|
77
|
+
readonly border: "#e2e8f0";
|
|
78
|
+
readonly primary: "#2563eb";
|
|
79
|
+
readonly text: "#0f172a";
|
|
80
|
+
readonly textSecondary: "#64748b";
|
|
81
|
+
readonly textMuted: "#94a3b8";
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* The glyph set. One glyph per meaning, everywhere:
|
|
86
|
+
*
|
|
87
|
+
* | key | unicode | ascii | meaning |
|
|
88
|
+
* |----------|---------|-------|--------------------------------------|
|
|
89
|
+
* | success | ✓ | + | node or run passed |
|
|
90
|
+
* | failure | ✗ | x | node or run failed |
|
|
91
|
+
* | skipped | − | - | node skipped |
|
|
92
|
+
* | pending | ○ | o | node not reached yet |
|
|
93
|
+
* | running | ● | * | node running (static views) |
|
|
94
|
+
* | warning | ⚠ | ! | a warning line |
|
|
95
|
+
* | brand | ▲ | ^ | the SWEny mark |
|
|
96
|
+
* | detail | ↳ | > | a detail line under a node |
|
|
97
|
+
* | sep | · | | | separator between inline facts |
|
|
98
|
+
* | ellipsis | … | ... | work in progress, truncated text |
|
|
99
|
+
* | arrow | → | -> | a route taken, a change from a to b |
|
|
100
|
+
*
|
|
101
|
+
* Every unicode glyph above is a single terminal column.
|
|
102
|
+
*/
|
|
103
|
+
export declare const GLYPHS: {
|
|
104
|
+
readonly unicode: {
|
|
105
|
+
readonly success: "✓";
|
|
106
|
+
readonly failure: "✗";
|
|
107
|
+
readonly skipped: "−";
|
|
108
|
+
readonly pending: "○";
|
|
109
|
+
readonly running: "●";
|
|
110
|
+
readonly warning: "⚠";
|
|
111
|
+
readonly brand: "▲";
|
|
112
|
+
readonly detail: "↳";
|
|
113
|
+
readonly sep: "·";
|
|
114
|
+
readonly ellipsis: "…";
|
|
115
|
+
readonly arrow: "→";
|
|
116
|
+
};
|
|
117
|
+
readonly ascii: {
|
|
118
|
+
readonly success: "+";
|
|
119
|
+
readonly failure: "x";
|
|
120
|
+
readonly skipped: "-";
|
|
121
|
+
readonly pending: "o";
|
|
122
|
+
readonly running: "*";
|
|
123
|
+
readonly warning: "!";
|
|
124
|
+
readonly brand: "^";
|
|
125
|
+
readonly detail: ">";
|
|
126
|
+
readonly sep: "|";
|
|
127
|
+
readonly ellipsis: "...";
|
|
128
|
+
readonly arrow: "->";
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
export type GlyphKey = keyof typeof GLYPHS.unicode;
|
|
132
|
+
export type GlyphSet = {
|
|
133
|
+
readonly [K in GlyphKey]: string;
|
|
134
|
+
};
|
|
135
|
+
/** Spinner frames for live work: braille dots, or a plain ASCII bar. */
|
|
136
|
+
export declare const SPINNER_FRAMES: {
|
|
137
|
+
readonly unicode: readonly ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
138
|
+
readonly ascii: readonly ["|", "/", "-", "\\"];
|
|
139
|
+
};
|
|
140
|
+
/** Spinner frame interval in ms. */
|
|
141
|
+
export declare const SPINNER_INTERVAL_MS = 80;
|
|
142
|
+
/**
|
|
143
|
+
* Box drawing for the receipt ticket. The frame is perforated (dashed
|
|
144
|
+
* edges); the tear line separates the stub that carries the policy stamp.
|
|
145
|
+
*/
|
|
146
|
+
export declare const TICKET_BOX: {
|
|
147
|
+
readonly unicode: {
|
|
148
|
+
readonly topLeft: "╭";
|
|
149
|
+
readonly topRight: "╮";
|
|
150
|
+
readonly bottomLeft: "╰";
|
|
151
|
+
readonly bottomRight: "╯";
|
|
152
|
+
readonly horizontal: "┄";
|
|
153
|
+
readonly vertical: "┆";
|
|
154
|
+
readonly tearLeft: "├";
|
|
155
|
+
readonly tearRight: "┤";
|
|
156
|
+
readonly tear: "╌";
|
|
157
|
+
};
|
|
158
|
+
readonly ascii: {
|
|
159
|
+
readonly topLeft: "+";
|
|
160
|
+
readonly topRight: "+";
|
|
161
|
+
readonly bottomLeft: "+";
|
|
162
|
+
readonly bottomRight: "+";
|
|
163
|
+
readonly horizontal: "-";
|
|
164
|
+
readonly vertical: ":";
|
|
165
|
+
readonly tearLeft: "+";
|
|
166
|
+
readonly tearRight: "+";
|
|
167
|
+
readonly tear: "- ";
|
|
168
|
+
};
|
|
169
|
+
};
|
|
170
|
+
export type TicketBox = {
|
|
171
|
+
readonly [K in keyof typeof TICKET_BOX.unicode]: string;
|
|
172
|
+
};
|
|
173
|
+
/** Node states a diagram can show. `pending` is any node the run never reached. */
|
|
174
|
+
export type DiagramNodeStatus = "current" | "success" | "failed" | "skipped" | "pending";
|
|
175
|
+
/**
|
|
176
|
+
* Mermaid `classDef` bodies by node status. Readable on GitHub light and dark:
|
|
177
|
+
* filled nodes carry white text, pending is an outline in slate-500 (legible
|
|
178
|
+
* on both backgrounds).
|
|
179
|
+
*/
|
|
180
|
+
export declare const MERMAID_CLASS_DEFS: Readonly<Record<DiagramNodeStatus, string>>;
|
|
181
|
+
/** Mermaid `linkStyle` bodies for edges a traced run took and did not take. */
|
|
182
|
+
export declare const MERMAID_EDGE_STYLES: {
|
|
183
|
+
readonly taken: "stroke:#2563eb,stroke-width:3px";
|
|
184
|
+
readonly notTaken: "stroke:#64748b,stroke-width:1px,stroke-dasharray:5 5";
|
|
185
|
+
};
|