@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.
Files changed (64) hide show
  1. package/dist/agent-env.d.ts +2 -1
  2. package/dist/agent-env.js +9 -3
  3. package/dist/browser.d.ts +2 -0
  4. package/dist/browser.js +3 -0
  5. package/dist/budget.js +5 -1
  6. package/dist/cli/check.js +12 -12
  7. package/dist/cli/comment-output.d.ts +4 -2
  8. package/dist/cli/comment-output.js +16 -8
  9. package/dist/cli/config.js +24 -24
  10. package/dist/cli/e2e.js +17 -17
  11. package/dist/cli/main.js +42 -26
  12. package/dist/cli/marketplace.js +3 -3
  13. package/dist/cli/new.js +11 -8
  14. package/dist/cli/output.d.ts +3 -2
  15. package/dist/cli/output.js +39 -37
  16. package/dist/cli/progress.d.ts +33 -0
  17. package/dist/cli/progress.js +69 -0
  18. package/dist/cli/publish.js +2 -2
  19. package/dist/cli/record.d.ts +66 -0
  20. package/dist/cli/record.js +344 -0
  21. package/dist/cli/renderer.d.ts +4 -0
  22. package/dist/cli/renderer.js +20 -13
  23. package/dist/cli/run-output.d.ts +13 -6
  24. package/dist/cli/run-output.js +36 -26
  25. package/dist/cli/runs.js +17 -11
  26. package/dist/cli/setup.js +7 -7
  27. package/dist/cli/style.d.ts +26 -0
  28. package/dist/cli/style.js +48 -0
  29. package/dist/cli/terminal.d.ts +47 -0
  30. package/dist/cli/terminal.js +123 -0
  31. package/dist/cli/ticket.d.ts +89 -0
  32. package/dist/cli/ticket.js +218 -0
  33. package/dist/cli/try.d.ts +19 -8
  34. package/dist/cli/try.js +97 -65
  35. package/dist/cli/upgrade.js +1 -1
  36. package/dist/executor.d.ts +8 -1
  37. package/dist/executor.js +20 -2
  38. package/dist/git-credentials.d.ts +106 -0
  39. package/dist/git-credentials.js +322 -0
  40. package/dist/harness/acp.js +3 -0
  41. package/dist/harness/claude-code.d.ts +7 -0
  42. package/dist/harness/claude-code.js +44 -3
  43. package/dist/harness/codex.js +3 -0
  44. package/dist/harness/pi.js +3 -0
  45. package/dist/harness/policy.d.ts +5 -0
  46. package/dist/harness/policy.js +14 -0
  47. package/dist/harness/sandbox-wrapper.d.ts +9 -2
  48. package/dist/harness/sandbox-wrapper.js +14 -4
  49. package/dist/harness/types.d.ts +10 -0
  50. package/dist/index.d.ts +2 -0
  51. package/dist/index.js +2 -0
  52. package/dist/mermaid.d.ts +10 -5
  53. package/dist/mermaid.js +11 -13
  54. package/dist/safe-outputs.js +5 -0
  55. package/dist/skills/git-push.d.ts +62 -0
  56. package/dist/skills/git-push.js +132 -0
  57. package/dist/skills/github.d.ts +17 -0
  58. package/dist/skills/github.js +23 -0
  59. package/dist/theme.d.ts +185 -0
  60. package/dist/theme.js +183 -0
  61. package/dist/workflows/browser.js +2 -2
  62. package/dist/workflows/implement.yml +2 -1
  63. package/dist/workflows/triage.yml +2 -1
  64. 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. Sibling homes under the same configured scratch root are
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({ ...opts.spawn, egress, readOnly: policy.readOnly });
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
  }
@@ -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
- export type NodeStatus = "current" | "success" | "failed" | "skipped";
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 — nodes not in this map render as default (pending) */
20
+ /** Execution state. With a non-empty map, nodes missing from it render as `pending`. */
16
21
  state?: Record<string, NodeStatus>;
17
- /** Execution trace — highlights taken/not-taken edges and loop counts */
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, e.g. `{ success: "fill:#2563eb,stroke:#1d4ed8,color:#fff" }`.
26
- * Unset statuses keep the defaults.
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 — track index for linkStyle directives
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 defaults = {
137
- current: "fill:#3b82f6,stroke:#2563eb,color:#fff,stroke-width:2px",
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 — taken edges bold green, not-taken edges dashed gray
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(",")} stroke:#22c55e,stroke-width:3px`);
152
+ lines.push(` linkStyle ${takenIndices.join(",")} ${MERMAID_EDGE_STYLES.taken}`);
155
153
  }
156
154
  if (notTakenIndices.length > 0) {
157
- lines.push(` linkStyle ${notTakenIndices.join(",")} stroke:#6b7280,stroke-width:1px,stroke-dasharray:5 5`);
155
+ lines.push(` linkStyle ${notTakenIndices.join(",")} ${MERMAID_EDGE_STYLES.notTaken}`);
158
156
  }
159
157
  }
160
158
  return lines.join("\n");
@@ -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);
@@ -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.
@@ -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",
@@ -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
+ };