@sweny-ai/core 0.1.176 → 0.1.178

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 (49) hide show
  1. package/dist/agent-env.d.ts +9 -0
  2. package/dist/agent-env.js +4 -0
  3. package/dist/cli/comment-output.js +4 -5
  4. package/dist/cli/config-file.d.ts +37 -2
  5. package/dist/cli/config-file.js +68 -3
  6. package/dist/cli/final-output.d.ts +4 -1
  7. package/dist/cli/final-output.js +6 -8
  8. package/dist/cli/main.d.ts +2 -1
  9. package/dist/cli/main.js +18 -10
  10. package/dist/cli/resume.js +1 -0
  11. package/dist/cli/run-history.d.ts +3 -7
  12. package/dist/cli/run-history.js +5 -12
  13. package/dist/cli/run-output.d.ts +16 -10
  14. package/dist/cli/run-output.js +30 -16
  15. package/dist/cli/ticket.js +3 -2
  16. package/dist/cli/try.js +2 -3
  17. package/dist/decider.d.ts +176 -64
  18. package/dist/decider.js +475 -155
  19. package/dist/executor.d.ts +55 -7
  20. package/dist/executor.js +511 -99
  21. package/dist/harness/acp.js +2 -2
  22. package/dist/harness/claude-code.js +6 -4
  23. package/dist/harness/codex.js +2 -2
  24. package/dist/harness/pi.js +2 -2
  25. package/dist/index.d.ts +2 -2
  26. package/dist/index.js +2 -2
  27. package/dist/journal.d.ts +34 -5
  28. package/dist/journal.js +42 -13
  29. package/dist/safe-file.d.ts +74 -0
  30. package/dist/safe-file.js +211 -0
  31. package/dist/safe-outputs.d.ts +7 -1
  32. package/dist/safe-outputs.js +7 -1
  33. package/dist/schema.d.ts +189 -241
  34. package/dist/schema.js +56 -36
  35. package/dist/skills/git-push.d.ts +99 -10
  36. package/dist/skills/git-push.js +413 -52
  37. package/dist/skills/github.d.ts +19 -15
  38. package/dist/skills/github.js +91 -15
  39. package/dist/startup-env.d.ts +30 -0
  40. package/dist/startup-env.js +48 -0
  41. package/dist/types.d.ts +86 -4
  42. package/dist/types.js +22 -0
  43. package/dist/when.d.ts +8 -1
  44. package/dist/when.js +8 -1
  45. package/dist/workflow-builder.js +3 -1
  46. package/dist/workflows/browser.js +2 -2
  47. package/dist/workflows/implement.yml +37 -5
  48. package/dist/workflows/triage.yml +40 -6
  49. package/package.json +1 -1
package/dist/schema.js CHANGED
@@ -7,7 +7,7 @@
7
7
  * parsing untrusted input (JSON/YAML import, Studio editor).
8
8
  */
9
9
  import { z } from "zod";
10
- import { AUTHOR_ASSOCIATIONS, CONTEXT_MODES, EVALUATOR_KINDS, EVAL_POLICIES, MCP_TRANSPORTS, NODE_ACCESS, NODE_ON_FAIL, REQUIRES_ON_FAIL, SAFE_OUTPUT_APPLIERS, SAFE_OUTPUT_EXPIRES_PATTERN, SAFE_OUTPUT_MAX_CEILING, SAFE_OUTPUT_STATES, SAFE_OUTPUT_TYPES, SKILL_CATEGORIES, SKILL_ID_MAX_LENGTH, SKILL_ID_PATTERN, TOOL_ACCESS, TOOL_CLASSES, WORKFLOW_TYPES, } from "./types.js";
10
+ import { AUTHOR_ASSOCIATIONS, CONTEXT_MODES, DECIDER_CONFIDENCE_FLOOR, DECIDER_MARGIN_FLOOR, DECIDER_MIN_CONFIDENCE, DECIDER_MIN_MARGIN, DECIDER_MODE_REMOVED, DECIDER_PROVIDER_MOVED, EVALUATOR_KINDS, EVAL_POLICIES, MCP_TRANSPORTS, NODE_ACCESS, NODE_ON_FAIL, REQUIRES_ON_FAIL, ROUTE_BY, SAFE_OUTPUT_APPLIERS, SAFE_OUTPUT_EXPIRES_PATTERN, SAFE_OUTPUT_MAX_CEILING, SAFE_OUTPUT_STATES, SAFE_OUTPUT_TYPES, SKILL_CATEGORIES, SKILL_ID_MAX_LENGTH, SKILL_ID_PATTERN, TOOL_ACCESS, TOOL_CLASSES, WORKFLOW_TYPES, } from "./types.js";
11
11
  import { sourceZ } from "./sources.js";
12
12
  import { resolveNodePermissions } from "./node-policy.js";
13
13
  import { workflowInputsZ, WORKFLOW_INPUT_TYPES } from "./inputs.js";
@@ -280,26 +280,22 @@ export const budgetZ = z
280
280
  message: "budget must declare at least one of tokens, cost_usd",
281
281
  });
282
282
  /**
283
- * Decision model for route choices (#357). `shadow` needs a provider; the
284
- * state only ever goes to that `base_url`, there is no fallback. The key is
285
- * named by env var, never inlined.
283
+ * Decision model thresholds (#357) for the nodes that set `route_by:
284
+ * decider`. Thresholds may be raised, and lowered no further than the floors.
285
+ * The provider (endpoint, model, key) comes only from operator config.
286
+ *
287
+ * Shapes from the shadow-mode release fail with an explanation instead of
288
+ * silently changing routing: `mode` ({@link DECIDER_MODE_REMOVED}) and
289
+ * `provider` ({@link DECIDER_PROVIDER_MOVED}).
286
290
  */
287
291
  export const deciderZ = z
288
292
  .object({
289
- mode: z.enum(["off", "shadow"]),
290
- provider: z
291
- .object({
292
- base_url: z.string().url(),
293
- model: z.string().min(1),
294
- api_key_env: z.string().min(1).optional(),
295
- })
296
- .strict()
297
- .optional(),
293
+ mode: z.custom(() => false, { message: DECIDER_MODE_REMOVED }).optional(),
294
+ provider: z.custom(() => false, { message: DECIDER_PROVIDER_MOVED }).optional(),
295
+ min_confidence: z.number().min(DECIDER_CONFIDENCE_FLOOR).max(1).optional(),
296
+ min_margin: z.number().min(DECIDER_MARGIN_FLOOR).max(1).optional(),
298
297
  })
299
- .strict()
300
- .refine((d) => d.mode === "off" || d.provider !== undefined, {
301
- message: "decider.provider is required when mode is shadow",
302
- });
298
+ .strict();
303
299
  /** One typed write intent a node may emit (#365). */
304
300
  export const safeOutputDeclarationZ = z
305
301
  .object({
@@ -342,6 +338,7 @@ export const nodeZ = z
342
338
  tools: nodeToolsZ.optional(),
343
339
  fail_soft: z.boolean().optional(),
344
340
  on_fail: z.enum(NODE_ON_FAIL).optional(),
341
+ route_by: z.enum(ROUTE_BY).optional(),
345
342
  permissions: nodePermissionsZ.optional(),
346
343
  outputs: z.array(safeOutputDeclarationZ).min(1).optional(),
347
344
  rules: nodeSourcesZ.optional(),
@@ -361,6 +358,7 @@ export const edgeZ = z
361
358
  // Natural language (the harness picks the edge) or `{ expr }`, a
362
359
  // deterministic expression sweny evaluates itself (#461). One or the other.
363
360
  when: z.union([z.string(), z.object({ expr: z.string().min(1).max(WHEN_EXPR_MAX_LENGTH) }).strict()]).optional(),
361
+ description: z.string().min(1).optional(),
364
362
  max_iterations: z.number().int().min(1).max(EDGE_MAX_ITERATIONS_CEILING).optional(),
365
363
  })
366
364
  .strict();
@@ -478,6 +476,22 @@ export function validateWorkflow(workflow, knownSkills) {
478
476
  else
479
477
  m.set(k, [v]);
480
478
  };
479
+ // One edge per (from, to) pair (#357). A route picks a target node, so two
480
+ // edges to the same target would make their conditions ambiguous (which
481
+ // one's max_iterations counts, which one's condition the router saw). Write
482
+ // one edge whose condition covers both cases instead.
483
+ const seenPairs = new Set();
484
+ for (const edge of workflow.edges) {
485
+ const key = JSON.stringify([edge.from, edge.to]);
486
+ if (seenPairs.has(key)) {
487
+ errors.push({
488
+ code: "DUPLICATE_EDGE",
489
+ message: `More than one edge from "${edge.from}" to "${edge.to}"; merge them into one edge (combine the conditions with OR)`,
490
+ nodeId: edge.from,
491
+ });
492
+ }
493
+ seenPairs.add(key);
494
+ }
481
495
  // Edge targets must exist
482
496
  for (const edge of workflow.edges) {
483
497
  if (!nodeIds.has(edge.from)) {
@@ -1000,28 +1014,24 @@ export const workflowJsonSchema = {
1000
1014
  },
1001
1015
  Decider: {
1002
1016
  type: "object",
1003
- required: ["mode"],
1004
1017
  additionalProperties: false,
1005
- description: "Decision model for route choices. 'shadow' asks it alongside the agent and logs whether they agree; the route is always the agent's. The workflow state goes only to provider.base_url, with no fallback.",
1018
+ description: "Decision model thresholds for the nodes that set route_by: decider. The provider (URL, model, key) comes only from operator config (.sweny.yml or SWENY_DECIDER_* env), never from the workflow; 'provider' and the removed 'mode' key are rejected.",
1006
1019
  properties: {
1007
- mode: { type: "string", enum: ["off", "shadow"] },
1008
- provider: {
1009
- type: "object",
1010
- required: ["base_url", "model"],
1011
- additionalProperties: false,
1012
- properties: {
1013
- base_url: {
1014
- type: "string",
1015
- format: "uri",
1016
- description: "Server root serving POST /v1/systemone (Ollama or TypeSafe Jev).",
1017
- },
1018
- model: { type: "string", minLength: 1 },
1019
- api_key_env: { type: "string", minLength: 1, description: "Env var holding the bearer key." },
1020
- },
1020
+ min_confidence: {
1021
+ type: "number",
1022
+ minimum: DECIDER_CONFIDENCE_FLOOR,
1023
+ maximum: 1,
1024
+ default: DECIDER_MIN_CONFIDENCE,
1025
+ description: "Accept the decider's answer only at or above this confidence.",
1026
+ },
1027
+ min_margin: {
1028
+ type: "number",
1029
+ minimum: DECIDER_MARGIN_FLOOR,
1030
+ maximum: 1,
1031
+ default: DECIDER_MIN_MARGIN,
1032
+ description: "Accept it only when the top label leads the runner-up by at least this much.",
1021
1033
  },
1022
1034
  },
1023
- if: { properties: { mode: { const: "shadow" } } },
1024
- then: { required: ["provider"] },
1025
1035
  },
1026
1036
  SafeOutput: {
1027
1037
  type: "object",
@@ -1174,7 +1184,7 @@ export const workflowJsonSchema = {
1174
1184
  },
1175
1185
  decider: {
1176
1186
  $ref: "#/$defs/Decider",
1177
- description: "Decision model for route choices (shadow mode only). Default: off.",
1187
+ description: "Thresholds for the decision model that decides the routes of route_by: decider nodes when confident. Default: 0.85 confidence, 0.2 margin.",
1178
1188
  },
1179
1189
  context_mode: {
1180
1190
  type: "string",
@@ -1335,6 +1345,11 @@ export const workflowJsonSchema = {
1335
1345
  enum: [...NODE_ON_FAIL],
1336
1346
  description: "What to do when this node finishes 'failed' (agent-level failure, or an eval failure that exhausted retries and was not softened by fail_soft). 'halt' (default) stops the workflow with the failure surfaced so a broken node never advances down a conditional edge; 'continue' preserves the legacy fall-through where routing proceeds from the failed node. Distinct from requires.on_fail (the pre-condition gate).",
1337
1347
  },
1348
+ route_by: {
1349
+ type: "string",
1350
+ enum: [...ROUTE_BY],
1351
+ description: "Who picks this node's natural-language route. 'agent' (default): the agent's route evaluation. 'decider': the operator's decision model first, the agent when it is not confident. Expressions always win over both; put safety conditions in expressions.",
1352
+ },
1338
1353
  permissions: {
1339
1354
  $ref: "#/$defs/Permissions",
1340
1355
  description: "What this node's agent may do. Absent: read when the node declares outputs, else the workflow's permissions, else write.",
@@ -1456,6 +1471,11 @@ export const workflowJsonSchema = {
1456
1471
  },
1457
1472
  ],
1458
1473
  },
1474
+ description: {
1475
+ type: "string",
1476
+ minLength: 1,
1477
+ description: "The condition in natural language. For an { expr } edge, what the next rung reads when the expression cannot be evaluated because a field it needs is missing or invalid.",
1478
+ },
1459
1479
  max_iterations: {
1460
1480
  type: "integer",
1461
1481
  minimum: 1,
@@ -7,14 +7,51 @@
7
7
  * the sweny process, with the github skill's token (`GITHUB_TOKEN`), right
8
8
  * before it opens the PR. The agent never holds the token.
9
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).
10
+ * The checkout is agent-written, so nothing in it may run, or steer the push,
11
+ * while the token is in play. The push therefore never runs in the checkout:
12
+ *
13
+ * 1. Read-only git in the checkout, with no token in its env, resolves the
14
+ * head commit, checks that every fetch and push URL of `origin` is the PR's
15
+ * repo on the GitHub server, and finds the object store.
16
+ * 2. A private temporary bare repo borrows those objects (`alternates`, data
17
+ * only) and gets one ref, the head commit.
18
+ * 3. git pushes from that repo, with no system or global config, an empty
19
+ * hooks dir, `--no-verify` and no credential helper, to an explicit URL
20
+ * built from the PR's repo (`<server>/<owner>/<repo>.git`) with an explicit
21
+ * refspec. No agent-written config, hook, push URL or `insteadOf` is read.
22
+ * The token goes to git as command-scope config in the child env, never
23
+ * argv, and only on this one invocation.
24
+ *
25
+ * The git binary, its PATH, the proxy and CA settings and the server URL come
26
+ * from the environment captured at process startup (startup-env.ts), never
27
+ * from a value the workspace `.env` introduced, and git is run by an absolute
28
+ * path that lies outside the workspace, the temp dir and any directory this
29
+ * user can write (see {@link resolveTrustedGit}).
30
+ *
31
+ * The private repo lives in the temp dir, which this user (and so the agent)
32
+ * can write. A process the agent left running could edit its config between
33
+ * `init` and `push` (an `insteadOf`, a proxy, `sslVerify=false`). Two layers:
34
+ * the push carries command-scope config for its exact URL (proxy from the
35
+ * trusted env or none, `sslVerify=true`, CA only from the trusted env), which
36
+ * beats repo-local config of the same URL; and the repo's config, alternates,
37
+ * top-level entries and hooks dir are fingerprinted after setup and compared
38
+ * right before the push, which is refused on any change. git has no switch to
39
+ * skip `$GIT_DIR/config`, so a byte check is the idiomatic guard. Residual
40
+ * risk: a change landing in the milliseconds between that check and git's own
41
+ * config read. The harnesses stop the agent's process (Codex its whole process
42
+ * group) when a node ends, but a deliberately detached process (`setsid`)
43
+ * escapes any group kill; only the sandbox's own process boundary ends it.
44
+ *
45
+ * Guard rails: only a local branch that exists, only when origin is the PR's
46
+ * own repo, never the base branch or the repo's default branch (from the
47
+ * GitHub API, not the checkout's `origin/HEAD`). The head must be new on the
48
+ * remote, or an unprotected branch with an open PR into the base (checked by
49
+ * the caller through the API); the push leases on the sha that check saw and
50
+ * only fast-forwards.
15
51
  * A push that fails is logged and the PR is still requested: the agent may
16
52
  * already have pushed (persisted credentials, an `agent_env` grant).
17
53
  */
54
+ import type { BranchPusher } from "../types.js";
18
55
  export interface GitRunResult {
19
56
  code: number;
20
57
  stdout: string;
@@ -24,6 +61,16 @@ export type GitRunner = (args: string[], opts: {
24
61
  cwd: string;
25
62
  env: NodeJS.ProcessEnv;
26
63
  }) => Promise<GitRunResult>;
64
+ /** The trusted entries of a PATH value, in order (see {@link trustedDir}). */
65
+ export declare function trustedPathDirs(pathValue: string | undefined, workspace: string): string[];
66
+ /**
67
+ * The absolute path of the first `git` on `pathValue` (the startup PATH) whose
68
+ * directory, and whose resolved file and its directory, pass
69
+ * {@link trustedDir}. Undefined when there is none: nothing is pushed.
70
+ */
71
+ export declare function resolveTrustedGit(pathValue: string | undefined, workspace: string): string | undefined;
72
+ /** A runner that executes the git at `gitPath` (absolute), never a PATH lookup. */
73
+ export declare function gitAt(gitPath: string): GitRunner;
27
74
  export interface PushHeadOptions {
28
75
  /** `owner/repo` the PR is for. */
29
76
  repo: string;
@@ -31,12 +78,30 @@ export interface PushHeadOptions {
31
78
  head: string;
32
79
  /** The PR's base branch. */
33
80
  base: string;
34
- /** The github skill's token. Without it, git uses whatever credential the checkout has. */
81
+ /**
82
+ * The repo's default branch, from the GitHub API. Without it nothing is
83
+ * pushed: the checkout's `origin/HEAD` is agent-written and not consulted.
84
+ */
85
+ defaultBranch?: string;
86
+ /**
87
+ * The head branch's sha on the remote, as the API reported it, or "" when it
88
+ * did not exist (`headPushLease` in github.ts). The push leases on it, so a
89
+ * branch created or moved since the check is not overwritten. Without it
90
+ * nothing is pushed.
91
+ */
92
+ remoteSha?: string;
93
+ /** The github skill's token. Without it nothing is pushed. */
35
94
  token?: string;
36
- /** The checkout. Default: `process.cwd()`. */
37
- cwd?: string;
95
+ /** The checkout (the run's `cwd`). */
96
+ cwd: string;
97
+ /**
98
+ * The operator's env: `GITHUB_SERVER_URL`, proxy and CA settings. Default:
99
+ * `process.env`, read through the startup snapshot (startup-env.ts), so a
100
+ * value the workspace `.env` introduced is never used. PATH always comes from
101
+ * the startup snapshot.
102
+ */
38
103
  env?: NodeJS.ProcessEnv;
39
- /** Test seam. */
104
+ /** Test seam. Default: the git {@link resolveTrustedGit} finds on the startup PATH. */
40
105
  git?: GitRunner;
41
106
  }
42
107
  export interface PushHeadResult {
@@ -54,9 +119,33 @@ export declare function repoFromRemote(url: string): {
54
119
  } | undefined;
55
120
  /** A branch name sweny will pass to git: plain ref characters, no option or traversal forms. */
56
121
  export declare function isSafeBranch(name: string): boolean;
122
+ /** `owner/repo` with plain name characters only. */
123
+ export declare function isSafeRepo(repo: string): boolean;
57
124
  /** The command-scope git config that authenticates one push to `server` with `token`. */
58
125
  export declare function pushAuthConfig(server: string, token: string, env: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
59
126
  /**
60
- * Push the PR's head branch to `origin` from the sweny process. Never throws.
127
+ * Command-scope transport config for the push to `destination`, keyed to that
128
+ * exact URL so it beats repo-local `http.*` and `http.<url>.*` entries: the
129
+ * proxy from the trusted env or none (an empty `http.proxy` disables proxying,
130
+ * env included), TLS verification on, and a CA bundle only from the trusted env.
131
+ */
132
+ export declare function pushTransportConfig(destination: string, trusted: NodeJS.ProcessEnv): Array<[string, string]>;
133
+ /**
134
+ * The env every sweny-side git call runs with: the trusted PATH, no system or
135
+ * global config, no prompts, no inherited git settings, and no kept value the
136
+ * workspace `.env` introduced.
137
+ */
138
+ export declare function hardenedGitEnv(env: NodeJS.ProcessEnv, trustedPath?: string): NodeJS.ProcessEnv;
139
+ /**
140
+ * True when a fresh `git init --bare` config holds only repo-format keys
141
+ * (`[core]` format/fs flags, `[extensions]` object format and ref storage).
142
+ */
143
+ export declare function isPlainRepoConfig(text: string): boolean;
144
+ /** A branch name without a leading `refs/heads/`, so `refs/heads/develop` and `develop` compare equal. */
145
+ export declare function branchName(ref: string): string;
146
+ /**
147
+ * Push the PR's head branch from the sweny process. Never throws.
61
148
  */
62
149
  export declare function pushHeadBranch(opts: PushHeadOptions): Promise<PushHeadResult>;
150
+ /** A pusher bound to one run's checkout and env (handed to tools as `ToolContext.pushBranch`). */
151
+ export declare function bindBranchPusher(cwd: string, env?: NodeJS.ProcessEnv): BranchPusher;