@sweny-ai/core 0.1.193 → 0.1.195
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/README.md +57 -0
- package/dist/browser.d.ts +1 -1
- package/dist/cli/comment-output.d.ts +13 -9
- package/dist/cli/comment-output.js +15 -14
- package/dist/cli/first-success.d.ts +65 -0
- package/dist/cli/first-success.js +81 -0
- package/dist/cli/main.js +21 -16
- package/dist/cli/new.d.ts +9 -0
- package/dist/cli/new.js +13 -1
- package/dist/cli/packs.js +2 -3
- package/dist/cli/record.d.ts +1 -1
- package/dist/cli/record.js +3 -3
- package/dist/cli/run-output.js +1 -1
- package/dist/cli/try.d.ts +2 -0
- package/dist/cli/try.js +4 -2
- package/dist/executor.js +5 -0
- package/dist/index.d.ts +1 -1
- package/dist/journal.js +2 -2
- package/dist/pr-comment.d.ts +93 -0
- package/dist/pr-comment.js +182 -0
- package/dist/run-context.d.ts +16 -0
- package/dist/run-context.js +73 -0
- package/dist/safe-outputs.d.ts +5 -1
- package/dist/safe-outputs.js +25 -4
- package/dist/skills/github.d.ts +14 -0
- package/dist/skills/github.js +82 -4
- package/dist/types.d.ts +28 -0
- package/dist/types.js +8 -0
- package/package.json +11 -7
package/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# @sweny-ai/core
|
|
2
|
+
|
|
3
|
+
**Workflows for coding agents. One set of rules, a receipt for every run.**
|
|
4
|
+
|
|
5
|
+
SWEny runs a DAG of coding-agent steps from a YAML file, on your laptop or in CI. The agent does the work. SWEny decides what it can touch, checks what it hands back, and records what it did.
|
|
6
|
+
|
|
7
|
+
## Start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx @sweny-ai/core new # pick a named workflow, e.g. explain-repo (no skill credentials)
|
|
11
|
+
npx @sweny-ai/core check # preflight: is your agent logged in?
|
|
12
|
+
npx @sweny-ai/core workflow run .sweny/workflows/explain-repo.yml
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Install once with `npm install -g @sweny-ai/core` and the binary is `sweny`. Want to watch it first? `sweny try` replays a recorded demo with no login and no network (sample data, not a real run).
|
|
16
|
+
|
|
17
|
+
## Credentials
|
|
18
|
+
|
|
19
|
+
Requires Node 20+ and a login or API key for the agent that runs your nodes. Claude Code is the default; pick another with `--agent`.
|
|
20
|
+
|
|
21
|
+
| Agent | Credentials it needs |
|
|
22
|
+
|-------|----------------------|
|
|
23
|
+
| Claude Code (default) | `CLAUDE_CODE_OAUTH_TOKEN` (from `claude setup-token`), or `ANTHROPIC_API_KEY` / `ANTHROPIC_AUTH_TOKEN`, or a Claude Code login (`claude`, then `/login`), or Bedrock / Vertex routing. |
|
|
24
|
+
| Codex (`--agent codex`) | `CODEX_API_KEY`, `OPENAI_API_KEY` or `CODEX_ACCESS_TOKEN`, or a `codex login`. |
|
|
25
|
+
| pi (`--agent pi`) | The API key of the model provider pi calls (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, ...). |
|
|
26
|
+
| ACP agent (`--agent "acp:<command>"`) | Whatever that agent needs; pass its key var through `SWENY_ENV_PASSTHROUGH`. |
|
|
27
|
+
|
|
28
|
+
For Claude Code and Codex, `sweny check` and `sweny workflow run` stop with the fix when no credential or login is found, before any node runs. Skill credentials (`GITHUB_TOKEN` and so on) are separate and only needed by workflows that use those skills.
|
|
29
|
+
|
|
30
|
+
## What you get
|
|
31
|
+
|
|
32
|
+
Every run ends with a receipt: nodes, tool calls, duration, tokens, cost, and the policy the agent ran under. An example:
|
|
33
|
+
|
|
34
|
+
```text
|
|
35
|
+
╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮
|
|
36
|
+
┆ ✓ passed run 3f9a2c ┆
|
|
37
|
+
┆ explain-repo ┆
|
|
38
|
+
┆ ┆
|
|
39
|
+
┆ nodes 2/2 ┆
|
|
40
|
+
┆ tool calls 12 ┆
|
|
41
|
+
┆ duration 28s ┆
|
|
42
|
+
┆ tokens 30k ┆
|
|
43
|
+
┆ cost $0.13 ┆
|
|
44
|
+
┆ policy env scoped, sandbox on ┆
|
|
45
|
+
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
|
|
46
|
+
┆ [ ENV SCOPED · SANDBOXED ] ┆
|
|
47
|
+
╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╯
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Then `sweny runs` lists your history, and `sweny runs diff` shows what changed in the answers between two runs. The [GitHub Action](https://docs.sweny.ai/action/) posts the same receipt on a PR, and recurring [packs](https://docs.sweny.ai/workflows/packs/) (weekly digest, dependency drift, PR risk review) run it on a schedule.
|
|
51
|
+
|
|
52
|
+
## Links
|
|
53
|
+
|
|
54
|
+
- [Quick start](https://docs.sweny.ai/getting-started/quick-start/)
|
|
55
|
+
- [CLI reference](https://docs.sweny.ai/cli/commands/)
|
|
56
|
+
- [Agents and policies](https://docs.sweny.ai/getting-started/agents-and-policies/)
|
|
57
|
+
- [Source and issues](https://github.com/swenyai/sweny)
|
package/dist/browser.d.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* - `buildWorkflow` / `refineWorkflow` — depend on the Claude interface.
|
|
14
14
|
* - Testing helpers — `MockClaude`, `createFileSkill` use `node:fs`.
|
|
15
15
|
*/
|
|
16
|
-
export type { Skill, SkillCategory, Tool, ToolContext, ConfigField, JSONSchema, Workflow, Node, Edge, EdgeWhen, WhenExpression, NodeResult, ToolCall, ExecutionEvent, Observer, Claude, Logger, Evaluator, EvaluatorRule, EvalResult, EvaluatorKind, NodeRequires, NodeRetry, NodeToolFilter, OutputMatch, NodeSources, EvalPolicy, RequiresOnFail, NodeOnFail, McpTransport, WorkflowType, SkillHarnessKey, } from "./types.js";
|
|
16
|
+
export type { Skill, SkillCategory, Tool, ToolContext, ToolRunContext, ConfigField, JSONSchema, Workflow, Node, Edge, EdgeWhen, WhenExpression, NodeResult, ToolCall, ExecutionEvent, Observer, Claude, Logger, Evaluator, EvaluatorRule, EvalResult, EvaluatorKind, NodeRequires, NodeRetry, NodeToolFilter, OutputMatch, NodeSources, EvalPolicy, RequiresOnFail, NodeOnFail, McpTransport, WorkflowType, SkillHarnessKey, } from "./types.js";
|
|
17
17
|
export { consoleLogger } from "./types.js";
|
|
18
18
|
export { EVALUATOR_KINDS, EVAL_POLICIES, REQUIRES_ON_FAIL, NODE_ON_FAIL, MCP_TRANSPORTS, SKILL_CATEGORIES, SKILL_HARNESSES, SKILL_ID_PATTERN, SKILL_ID_MAX_LENGTH, isValidSkillId, } from "./types.js";
|
|
19
19
|
export { github, linear, slack, sentry, datadog, betterstack, notification, supabase, builtinSkills, createSkillMap, allSkills, isSkillConfigured, configuredBuiltinSkills, validateWorkflowSkills, } from "./skills/index.js";
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* PR billboard
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* PR billboard. Quiet by default (#536): `--comment-file` now writes an empty
|
|
3
|
+
* file, so the Action's poster (scripts/post-run-comment.sh skips an empty
|
|
4
|
+
* file) adds nothing to the PR. The receipt and DAG go to the job summary, and
|
|
5
|
+
* findings go in the workflow's one updating comment (pr-comment.ts). The
|
|
6
|
+
* formatters below remain for the offline demo (`sweny try`).
|
|
5
7
|
*
|
|
6
8
|
* METADATA ONLY. Node names, statuses, counts, and durations. Never prompts,
|
|
7
9
|
* tool inputs, node outputs, errors, or model prose. The optional "since last
|
|
@@ -9,6 +11,7 @@
|
|
|
9
11
|
*/
|
|
10
12
|
import type { ExecutionTrace, NodeResult, Workflow } from "../types.js";
|
|
11
13
|
import { type RunSummary } from "./run-output.js";
|
|
14
|
+
import { prCommentMarker } from "../pr-comment.js";
|
|
12
15
|
export declare const RUN_COMMENT_FOOTER = "Run with [SWEny](https://github.com/swenyai/sweny): `npx @sweny-ai/core new`";
|
|
13
16
|
export interface RunCommentOptions {
|
|
14
17
|
trace?: ExecutionTrace;
|
|
@@ -19,8 +22,8 @@ export interface RunCommentOptions {
|
|
|
19
22
|
/** Pre-rendered "since last run" markdown line (run-answers.ts formatSinceLastMarkdown): counts only. */
|
|
20
23
|
sinceLast?: string | null;
|
|
21
24
|
}
|
|
22
|
-
/** Hidden marker the poster searches for to update the comment in place. */
|
|
23
|
-
export declare
|
|
25
|
+
/** Hidden marker the poster searches for to update the comment in place (the findings comment's marker too). */
|
|
26
|
+
export declare const runCommentMarker: typeof prCommentMarker;
|
|
24
27
|
export declare function formatRunComment(workflow: Workflow, results: Map<string, NodeResult>, summary: RunSummary, opts?: RunCommentOptions): string;
|
|
25
28
|
/**
|
|
26
29
|
* Minimal comment for a run that crashed before producing node results: no DAG
|
|
@@ -28,7 +31,8 @@ export declare function formatRunComment(workflow: Workflow, results: Map<string
|
|
|
28
31
|
* success comment for the same workflow.
|
|
29
32
|
*/
|
|
30
33
|
export declare function formatCrashComment(workflow: Workflow, summary: RunSummary, runId?: string): string;
|
|
31
|
-
/**
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Write an empty comment file: no metadata-only comment on the PR. Truncates a
|
|
36
|
+
* file an earlier step left there. Never throws; returns true only when written.
|
|
37
|
+
*/
|
|
38
|
+
export declare function writeRunComment(file: string): boolean;
|
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* PR billboard
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* PR billboard. Quiet by default (#536): `--comment-file` now writes an empty
|
|
3
|
+
* file, so the Action's poster (scripts/post-run-comment.sh skips an empty
|
|
4
|
+
* file) adds nothing to the PR. The receipt and DAG go to the job summary, and
|
|
5
|
+
* findings go in the workflow's one updating comment (pr-comment.ts). The
|
|
6
|
+
* formatters below remain for the offline demo (`sweny try`).
|
|
5
7
|
*
|
|
6
8
|
* METADATA ONLY. Node names, statuses, counts, and durations. Never prompts,
|
|
7
9
|
* tool inputs, node outputs, errors, or model prose. The optional "since last
|
|
8
10
|
* run" line carries counts of added and resolved answers, never their text.
|
|
9
11
|
*/
|
|
10
|
-
import { workspaceRoot, writeFileNoFollow } from "../safe-file.js";
|
|
11
12
|
import { toMermaidBlock } from "../mermaid.js";
|
|
12
13
|
import { formatReceiptDuration, nodeStates } from "./run-output.js";
|
|
13
14
|
import { formatTicketText } from "./ticket.js";
|
|
14
15
|
import { glyphs } from "./terminal.js";
|
|
16
|
+
import { workspaceRoot, writeFileNoFollow } from "../safe-file.js";
|
|
17
|
+
import { prCommentMarker } from "../pr-comment.js";
|
|
15
18
|
export const RUN_COMMENT_FOOTER = "Run with [SWEny](https://github.com/swenyai/sweny): `npx @sweny-ai/core new`";
|
|
16
19
|
/** The receipt ticket as a fenced block, the same card a TTY run ends on. */
|
|
17
20
|
function ticketBlock(workflow, summary, runId, crashed) {
|
|
18
21
|
return ["```text", formatTicketText({ summary, workflow: workflow.id, runId, crashed }), "```"].join("\n");
|
|
19
22
|
}
|
|
20
|
-
/** Hidden marker the poster searches for to update the comment in place. */
|
|
21
|
-
export
|
|
22
|
-
const safe = workflowId.replace(/[^A-Za-z0-9._-]+/g, "_").replace(/-{2,}/g, "-") || "workflow";
|
|
23
|
-
return `<!-- sweny-run-comment:${safe} -->`;
|
|
24
|
-
}
|
|
23
|
+
/** Hidden marker the poster searches for to update the comment in place (the findings comment's marker too). */
|
|
24
|
+
export const runCommentMarker = prCommentMarker;
|
|
25
25
|
/** Table-cell-safe text: no pipes, newlines, or backticks. */
|
|
26
26
|
function cell(text) {
|
|
27
27
|
return text.replace(/\s+/g, " ").replace(/\|/g, "\\|").replace(/`/g, "'").trim();
|
|
@@ -75,13 +75,14 @@ export function formatCrashComment(workflow, summary, runId) {
|
|
|
75
75
|
"",
|
|
76
76
|
].join("\n");
|
|
77
77
|
}
|
|
78
|
-
/**
|
|
79
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Write an empty comment file: no metadata-only comment on the PR. Truncates a
|
|
80
|
+
* file an earlier step left there. Never throws; returns true only when written.
|
|
81
|
+
*/
|
|
82
|
+
export function writeRunComment(file) {
|
|
80
83
|
try {
|
|
81
84
|
// The file may sit in the agent-writable workspace: no link on the path is followed.
|
|
82
|
-
writeFileNoFollow(file,
|
|
83
|
-
? formatCrashComment(workflow, summary, opts.runId)
|
|
84
|
-
: formatRunComment(workflow, results, summary, opts), { ...workspaceRoot(file), mode: 0o644, dirMode: 0o755 });
|
|
85
|
+
writeFileNoFollow(file, "", { ...workspaceRoot(file), mode: 0o644, dirMode: 0o755 });
|
|
85
86
|
return true;
|
|
86
87
|
}
|
|
87
88
|
catch (err) {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The "what next" note after a workspace's first successful real run (#534).
|
|
3
|
+
*
|
|
4
|
+
* `sweny try` ends with one command to run next; a real run used to end in
|
|
5
|
+
* silence, at the exact moment a user has just seen value. This prints one
|
|
6
|
+
* short block (history, compare, schedule) once per workspace.
|
|
7
|
+
*
|
|
8
|
+
* Once means once per workspace for this user: the marker lives in sweny's
|
|
9
|
+
* per-user state dir (`<state>/first-success/<workspace hash>`), never in the
|
|
10
|
+
* repo, so it is not committed and an agent in the workspace cannot reset it.
|
|
11
|
+
* The marker is claimed atomically (`wx`), so two runs finishing together
|
|
12
|
+
* print the note once.
|
|
13
|
+
*
|
|
14
|
+
* Silent when: CI, a failed run, `--json`, `--stream`, `--dry-run`, or the
|
|
15
|
+
* marker already exists. A marker that cannot be written means no note: a
|
|
16
|
+
* note that might repeat is worse than none. Every other output (a TTY, a
|
|
17
|
+
* pipe, NO_COLOR) gets the same words; only the paint and glyphs change, and
|
|
18
|
+
* the last line is always plain text.
|
|
19
|
+
*/
|
|
20
|
+
import { type Paint } from "./style.js";
|
|
21
|
+
type Env = Record<string, string | undefined>;
|
|
22
|
+
/** How to schedule a workflow you wrote: the Action's cron and dispatch triggers. */
|
|
23
|
+
export declare const SCHEDULING_DOCS_URL = "https://docs.sweny.ai/action/scheduling/";
|
|
24
|
+
/** The two command steps, in order. Commands are real subcommands (a spec asserts it). */
|
|
25
|
+
export declare const WHAT_NEXT_STEPS: ReadonlyArray<{
|
|
26
|
+
command: string;
|
|
27
|
+
what: string;
|
|
28
|
+
}>;
|
|
29
|
+
/** The third step, and the plain closing line. */
|
|
30
|
+
export declare const WHAT_NEXT_SCHEDULE = "Run it on a schedule: https://docs.sweny.ai/action/scheduling/";
|
|
31
|
+
/** CI: the `CI` convention, or GitHub Actions itself. */
|
|
32
|
+
export declare function inCi(env?: Env): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* The note: a heading, one aligned row per step, then a plain closing line
|
|
35
|
+
* that says how to schedule it. Same words with or without color or Unicode.
|
|
36
|
+
*/
|
|
37
|
+
export declare function formatWhatNext(paint?: Paint, unicode?: boolean): string;
|
|
38
|
+
/** `<state>/first-success/<workspace hash>`: one empty file per workspace. */
|
|
39
|
+
export declare function firstSuccessMarker(cwd: string, env?: Env): string;
|
|
40
|
+
/**
|
|
41
|
+
* Claim the workspace's marker. True only for the caller that created it.
|
|
42
|
+
* False when it already exists or cannot be written (read-only home, no
|
|
43
|
+
* state dir): the note then stays silent rather than risk repeating.
|
|
44
|
+
*/
|
|
45
|
+
export declare function claimFirstSuccess(cwd: string, env?: Env): boolean;
|
|
46
|
+
export interface WhatNextOptions {
|
|
47
|
+
/** Where the note goes: the run's stdout. */
|
|
48
|
+
stream: {
|
|
49
|
+
write(s: string): unknown;
|
|
50
|
+
isTTY?: boolean;
|
|
51
|
+
};
|
|
52
|
+
/** The run succeeded (no failed node, not crashed). Anything else prints nothing. */
|
|
53
|
+
ok: boolean;
|
|
54
|
+
/** `--json`, `--stream` or `--dry-run`: stdout is data, or nothing real ran. */
|
|
55
|
+
quiet?: boolean;
|
|
56
|
+
env?: Env;
|
|
57
|
+
cwd?: string;
|
|
58
|
+
/** Override color. Default: `colorEnabled(stream, env)`. */
|
|
59
|
+
color?: boolean;
|
|
60
|
+
/** Override Unicode. Default: `supportsUnicode(env)`. */
|
|
61
|
+
unicode?: boolean;
|
|
62
|
+
}
|
|
63
|
+
/** Print the note if this is the workspace's first successful real run. Returns whether it printed. */
|
|
64
|
+
export declare function writeWhatNext(opts: WhatNextOptions): boolean;
|
|
65
|
+
export {};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The "what next" note after a workspace's first successful real run (#534).
|
|
3
|
+
*
|
|
4
|
+
* `sweny try` ends with one command to run next; a real run used to end in
|
|
5
|
+
* silence, at the exact moment a user has just seen value. This prints one
|
|
6
|
+
* short block (history, compare, schedule) once per workspace.
|
|
7
|
+
*
|
|
8
|
+
* Once means once per workspace for this user: the marker lives in sweny's
|
|
9
|
+
* per-user state dir (`<state>/first-success/<workspace hash>`), never in the
|
|
10
|
+
* repo, so it is not committed and an agent in the workspace cannot reset it.
|
|
11
|
+
* The marker is claimed atomically (`wx`), so two runs finishing together
|
|
12
|
+
* print the note once.
|
|
13
|
+
*
|
|
14
|
+
* Silent when: CI, a failed run, `--json`, `--stream`, `--dry-run`, or the
|
|
15
|
+
* marker already exists. A marker that cannot be written means no note: a
|
|
16
|
+
* note that might repeat is worse than none. Every other output (a TTY, a
|
|
17
|
+
* pipe, NO_COLOR) gets the same words; only the paint and glyphs change, and
|
|
18
|
+
* the last line is always plain text.
|
|
19
|
+
*/
|
|
20
|
+
import fs from "node:fs";
|
|
21
|
+
import path from "node:path";
|
|
22
|
+
import { swenyStateDir, workspaceScope } from "../journal.js";
|
|
23
|
+
import { createPaint } from "./style.js";
|
|
24
|
+
import { colorEnabled, glyphsFor, supportsUnicode } from "./terminal.js";
|
|
25
|
+
/** How to schedule a workflow you wrote: the Action's cron and dispatch triggers. */
|
|
26
|
+
export const SCHEDULING_DOCS_URL = "https://docs.sweny.ai/action/scheduling/";
|
|
27
|
+
/** The two command steps, in order. Commands are real subcommands (a spec asserts it). */
|
|
28
|
+
export const WHAT_NEXT_STEPS = [
|
|
29
|
+
{ command: "sweny runs", what: "see this run and the ones before it" },
|
|
30
|
+
{ command: "sweny runs diff", what: "run it again, then see what changed in the answers" },
|
|
31
|
+
];
|
|
32
|
+
/** The third step, and the plain closing line. */
|
|
33
|
+
export const WHAT_NEXT_SCHEDULE = `Run it on a schedule: ${SCHEDULING_DOCS_URL}`;
|
|
34
|
+
function truthy(v) {
|
|
35
|
+
return v !== undefined && v !== "" && v !== "0" && v.toLowerCase() !== "false";
|
|
36
|
+
}
|
|
37
|
+
/** CI: the `CI` convention, or GitHub Actions itself. */
|
|
38
|
+
export function inCi(env = process.env) {
|
|
39
|
+
return truthy(env.CI) || env.GITHUB_ACTIONS === "true";
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The note: a heading, one aligned row per step, then a plain closing line
|
|
43
|
+
* that says how to schedule it. Same words with or without color or Unicode.
|
|
44
|
+
*/
|
|
45
|
+
export function formatWhatNext(paint = createPaint(false), unicode = true) {
|
|
46
|
+
const g = glyphsFor(unicode);
|
|
47
|
+
const width = Math.max(...WHAT_NEXT_STEPS.map((s) => s.command.length));
|
|
48
|
+
const rows = WHAT_NEXT_STEPS.map((s) => ` ${paint.strong(s.command.padEnd(width))} ${paint.muted(s.what)}`);
|
|
49
|
+
return [` ${paint.brand(g.brand)} ${paint.strong("Next")}`, ...rows, ` ${WHAT_NEXT_SCHEDULE}`].join("\n");
|
|
50
|
+
}
|
|
51
|
+
/** `<state>/first-success/<workspace hash>`: one empty file per workspace. */
|
|
52
|
+
export function firstSuccessMarker(cwd, env = process.env) {
|
|
53
|
+
return path.join(swenyStateDir(env), "first-success", workspaceScope(cwd));
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Claim the workspace's marker. True only for the caller that created it.
|
|
57
|
+
* False when it already exists or cannot be written (read-only home, no
|
|
58
|
+
* state dir): the note then stays silent rather than risk repeating.
|
|
59
|
+
*/
|
|
60
|
+
export function claimFirstSuccess(cwd, env = process.env) {
|
|
61
|
+
const file = firstSuccessMarker(cwd, env);
|
|
62
|
+
try {
|
|
63
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
64
|
+
fs.closeSync(fs.openSync(file, "wx", 0o600));
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** Print the note if this is the workspace's first successful real run. Returns whether it printed. */
|
|
72
|
+
export function writeWhatNext(opts) {
|
|
73
|
+
const env = opts.env ?? process.env;
|
|
74
|
+
if (!opts.ok || opts.quiet || inCi(env))
|
|
75
|
+
return false;
|
|
76
|
+
if (!claimFirstSuccess(opts.cwd ?? process.cwd(), env))
|
|
77
|
+
return false;
|
|
78
|
+
const paint = createPaint(opts.color ?? colorEnabled(opts.stream, env));
|
|
79
|
+
opts.stream.write(`${formatWhatNext(paint, opts.unicode ?? supportsUnicode(env))}\n\n`);
|
|
80
|
+
return true;
|
|
81
|
+
}
|
package/dist/cli/main.js
CHANGED
|
@@ -45,6 +45,7 @@ import { createStreamObserver } from "./stream-observer.js";
|
|
|
45
45
|
import { createRunLogger, summarizeRun, writeStepSummary, WORKFLOW_RUN_DESCRIPTION, WORKFLOW_RUN_OPTIONS, } from "./run-output.js";
|
|
46
46
|
import { writeRunComment } from "./comment-output.js";
|
|
47
47
|
import { writeReceipt } from "./ticket.js";
|
|
48
|
+
import { writeWhatNext } from "./first-success.js";
|
|
48
49
|
import { createNodeProgress, toolCallsText } from "./progress.js";
|
|
49
50
|
import { applyNoColor, createPaint } from "./style.js";
|
|
50
51
|
import { colorEnabled, glyphsFor, richOutput, spinnerFramesFor, supportsUnicode } from "./terminal.js";
|
|
@@ -1029,6 +1030,8 @@ resume) {
|
|
|
1029
1030
|
// --max-steps overrides the executor's own DEFAULT_MAX_STEPS when set.
|
|
1030
1031
|
const wfTimeoutMs = budget.timeoutMs;
|
|
1031
1032
|
const wfMaxSteps = budget.maxSteps;
|
|
1033
|
+
// The job summary is written once: by the finished run, else by the crash path.
|
|
1034
|
+
let stepSummaryWritten = false;
|
|
1032
1035
|
try {
|
|
1033
1036
|
const { results, trace } = await runWithWallClockBudget((signal) => execute(workflow, workflowInput, {
|
|
1034
1037
|
skills,
|
|
@@ -1073,16 +1076,12 @@ resume) {
|
|
|
1073
1076
|
catch {
|
|
1074
1077
|
// silent
|
|
1075
1078
|
}
|
|
1076
|
-
//
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
runId,
|
|
1083
|
-
sinceLast: sinceLast ? formatSinceLastMarkdown(sinceLast) : null,
|
|
1084
|
-
});
|
|
1085
|
-
}
|
|
1079
|
+
// Receipt metadata belongs in the job summary, including --json and failed runs.
|
|
1080
|
+
stepSummaryWritten = true;
|
|
1081
|
+
writeStepSummary(workflow, results, summarizeRun(results, wfDurationMs, false, trace), trace, process.env, sinceLast ? formatSinceLastMarkdown(sinceLast) : null);
|
|
1082
|
+
// Quiet by default (#536): an empty comment file, so no metadata-only PR comment.
|
|
1083
|
+
if (options.commentFile)
|
|
1084
|
+
writeRunComment(options.commentFile);
|
|
1086
1085
|
if (isJson) {
|
|
1087
1086
|
await writeResultJson(results, outputSecrets(skills));
|
|
1088
1087
|
process.exit(wfHasFailed ? 1 : 0);
|
|
@@ -1103,10 +1102,9 @@ resume) {
|
|
|
1103
1102
|
catch {
|
|
1104
1103
|
// silent
|
|
1105
1104
|
}
|
|
1106
|
-
// Run receipt (metadata only)
|
|
1105
|
+
// Run receipt (metadata only). The job summary was written before early exits.
|
|
1107
1106
|
runLogger.flush();
|
|
1108
1107
|
const receipt = summarizeRun(results, wfDurationMs, false, trace);
|
|
1109
|
-
writeStepSummary(workflow, results, receipt, trace, process.env, sinceLast ? formatSinceLastMarkdown(sinceLast) : null);
|
|
1110
1108
|
// The answer, above the receipt: the terminal node's result, or the failed node's error.
|
|
1111
1109
|
// Also saved to .sweny/runs/<id>/output.md (local only; --no-history skips the file).
|
|
1112
1110
|
// Redacted with the journal's redactor; in CI printed only with --show-output.
|
|
@@ -1139,6 +1137,12 @@ resume) {
|
|
|
1139
1137
|
await writeReceipt(receipt, { workflow: workflow.id, runId, stream: process.stdout });
|
|
1140
1138
|
if (sinceLast)
|
|
1141
1139
|
console.log(createPaint(colorEnabled(process.stdout)).muted(` ${formatSinceLast(sinceLast)}`));
|
|
1140
|
+
// First successful real run in this workspace: one "what next" note (#534).
|
|
1141
|
+
writeWhatNext({
|
|
1142
|
+
stream: process.stdout,
|
|
1143
|
+
ok: true,
|
|
1144
|
+
quiet: Boolean(options.stream) || config.dryRun,
|
|
1145
|
+
});
|
|
1142
1146
|
process.exit(0);
|
|
1143
1147
|
}
|
|
1144
1148
|
catch (err) {
|
|
@@ -1158,10 +1162,11 @@ resume) {
|
|
|
1158
1162
|
recordHistory(nodeTimer.lastResults, undefined, true);
|
|
1159
1163
|
const crashSummary = summarizeRun(new Map(), Date.now() - runStart, true);
|
|
1160
1164
|
await writeReceipt(crashSummary, { workflow: workflow.id, runId, crashed: true, stream: process.stderr });
|
|
1161
|
-
// A crash
|
|
1162
|
-
if (
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
+
// A crash records its receipt in the job summary; the PR gets no status comment.
|
|
1166
|
+
if (!stepSummaryWritten)
|
|
1167
|
+
writeStepSummary(workflow, new Map(), crashSummary);
|
|
1168
|
+
if (options.commentFile)
|
|
1169
|
+
writeRunComment(options.commentFile);
|
|
1165
1170
|
// Finalize the cloud run as failed (covers thrown errors, incl.
|
|
1166
1171
|
// RouteEvaluationError). Without this a crashed workflow run stays
|
|
1167
1172
|
// "running" in cloud forever.
|
package/dist/cli/new.d.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { type WorkflowTemplate } from "./templates.js";
|
|
11
11
|
import { type Skill } from "../types.js";
|
|
12
|
+
import { SCHEDULING_DOCS_URL } from "./first-success.js";
|
|
12
13
|
export interface Credential {
|
|
13
14
|
key: string;
|
|
14
15
|
hint?: string;
|
|
@@ -171,3 +172,11 @@ export declare function runNew(options?: {
|
|
|
171
172
|
/** Internal: the id came from the marketplace picker, skip the built-in shortcut. */
|
|
172
173
|
forceMarketplace?: boolean;
|
|
173
174
|
}): Promise<void>;
|
|
175
|
+
export { SCHEDULING_DOCS_URL };
|
|
176
|
+
/**
|
|
177
|
+
* The closing line (#534). A pack already shows its own trigger step, so it
|
|
178
|
+
* closes as before. Any other workflow that was written (a starter or a
|
|
179
|
+
* described one) says how to schedule THIS workflow; it never names another
|
|
180
|
+
* pack, because no template metadata says which pack would suit it.
|
|
181
|
+
*/
|
|
182
|
+
export declare function outroMessage(hasExistingConfig: boolean, offerSchedule: boolean): string;
|
package/dist/cli/new.js
CHANGED
|
@@ -20,6 +20,7 @@ import { configuredSkills } from "../skills/custom-loader.js";
|
|
|
20
20
|
import { builtinSkills } from "../skills/index.js";
|
|
21
21
|
import { DagRenderer } from "./renderer.js";
|
|
22
22
|
import { runE2eInit } from "./e2e.js";
|
|
23
|
+
import { SCHEDULING_DOCS_URL } from "./first-success.js";
|
|
23
24
|
// ── Skill → Credential extras ─────────────────────────────────────────
|
|
24
25
|
/**
|
|
25
26
|
* Supplementary metadata (URLs, hints, defaults) for env vars.
|
|
@@ -853,7 +854,18 @@ export async function runNew(options) {
|
|
|
853
854
|
steps.push(`${stepNum++}. Create your first workflow:`, ` ${cli} new`);
|
|
854
855
|
}
|
|
855
856
|
p.note(steps.join("\n"), "Next steps");
|
|
856
|
-
p.outro(hasExistingConfig
|
|
857
|
+
p.outro(outroMessage(hasExistingConfig, Boolean(template) && !template?.pack));
|
|
858
|
+
}
|
|
859
|
+
export { SCHEDULING_DOCS_URL };
|
|
860
|
+
/**
|
|
861
|
+
* The closing line (#534). A pack already shows its own trigger step, so it
|
|
862
|
+
* closes as before. Any other workflow that was written (a starter or a
|
|
863
|
+
* described one) says how to schedule THIS workflow; it never names another
|
|
864
|
+
* pack, because no template metadata says which pack would suit it.
|
|
865
|
+
*/
|
|
866
|
+
export function outroMessage(hasExistingConfig, offerSchedule) {
|
|
867
|
+
const done = hasExistingConfig ? "Workflow added!" : "You're all set!";
|
|
868
|
+
return offerSchedule ? `${done} Run it on a schedule: ${SCHEDULING_DOCS_URL}` : done;
|
|
857
869
|
}
|
|
858
870
|
// ── Custom workflow branch ────────────────────────────────────────────
|
|
859
871
|
/**
|
package/dist/cli/packs.js
CHANGED
|
@@ -1000,8 +1000,7 @@ nodes:
|
|
|
1000
1000
|
(type comment). It can only land on context.input.pr_number; sweny
|
|
1001
1001
|
posts it after this step.
|
|
1002
1002
|
|
|
1003
|
-
Start the body with the
|
|
1004
|
-
Then: "Risk: LEVEL" with the size, then the reasons as short bullets
|
|
1003
|
+
Start the body with "Risk: LEVEL" and the size, then the reasons as short bullets
|
|
1005
1004
|
(each naming its evidence), then the focus pointers, then a last line:
|
|
1006
1005
|
"Read-only scope review. No code was changed."
|
|
1007
1006
|
Keep it under 15 lines. No emoji walls, no praise.
|
|
@@ -1053,7 +1052,7 @@ on:
|
|
|
1053
1052
|
|
|
1054
1053
|
permissions:
|
|
1055
1054
|
contents: read
|
|
1056
|
-
pull-requests: write # the review comment
|
|
1055
|
+
pull-requests: write # the one review comment, updated in place
|
|
1057
1056
|
|
|
1058
1057
|
concurrency:
|
|
1059
1058
|
group: sweny-pr-risk-\${{ github.event.pull_request.number }}
|
package/dist/cli/record.d.ts
CHANGED
|
@@ -55,7 +55,7 @@ export declare function buildTimeline(lines: string[], fixture: TryFixture): {
|
|
|
55
55
|
/** Render the timeline as a self-contained animated SVG. Pure and deterministic. */
|
|
56
56
|
export declare function renderRecordingSvg(lines: string[], fixture: TryFixture): string;
|
|
57
57
|
/**
|
|
58
|
-
* The
|
|
58
|
+
* The job summary preview (heading plus DAG) sits between the answer and the
|
|
59
59
|
* ticket. The recording drops it to stay a README-sized hero: tagline, nodes,
|
|
60
60
|
* answer, ticket.
|
|
61
61
|
*/
|
package/dist/cli/record.js
CHANGED
|
@@ -17,7 +17,7 @@ import fs from "node:fs";
|
|
|
17
17
|
import path from "node:path";
|
|
18
18
|
import chalk from "chalk";
|
|
19
19
|
import { GLYPHS, PALETTE, ROLE_COLORS, SPINNER_FRAMES, SURFACE_COLORS } from "../theme.js";
|
|
20
|
-
import { loadTryFixture, runTry } from "./try.js";
|
|
20
|
+
import { loadTryFixture, runTry, SUMMARY_PREVIEW_LEAD } from "./try.js";
|
|
21
21
|
/** Terminal width the demo is rendered at (the ticket and DAG are designed for 80). */
|
|
22
22
|
export const RECORD_COLUMNS = 80;
|
|
23
23
|
/** Lines longer than this wrap in the window, like a terminal would. */
|
|
@@ -292,12 +292,12 @@ export function renderRecordingSvg(lines, fixture) {
|
|
|
292
292
|
// ── Entry points ────────────────────────────────────────────────
|
|
293
293
|
const stripAnsi = (s) => s.replace(/\x1B\[[0-9;]*[A-Za-z]/g, "");
|
|
294
294
|
/**
|
|
295
|
-
* The
|
|
295
|
+
* The job summary preview (heading plus DAG) sits between the answer and the
|
|
296
296
|
* ticket. The recording drops it to stay a README-sized hero: tagline, nodes,
|
|
297
297
|
* answer, ticket.
|
|
298
298
|
*/
|
|
299
299
|
export function dropCommentPreview(lines) {
|
|
300
|
-
const from = lines.findIndex((l) => stripAnsi(l).includes(
|
|
300
|
+
const from = lines.findIndex((l) => stripAnsi(l).includes(SUMMARY_PREVIEW_LEAD));
|
|
301
301
|
const to = lines.findIndex((l, i) => i > from && stripAnsi(l).startsWith(" \u256D"));
|
|
302
302
|
if (from < 0 || to < 0)
|
|
303
303
|
return lines;
|
package/dist/cli/run-output.js
CHANGED
|
@@ -320,7 +320,7 @@ export const WORKFLOW_RUN_OPTIONS = [
|
|
|
320
320
|
["--mermaid", "Output a Mermaid diagram with execution state after run"],
|
|
321
321
|
[
|
|
322
322
|
"--comment-file <path>",
|
|
323
|
-
"Write
|
|
323
|
+
"Write an empty PR-comment file at <path> (kept for the Action; the receipt and DAG go to the job summary)",
|
|
324
324
|
],
|
|
325
325
|
["--input <json>", "JSON string of input data to pass to the workflow"],
|
|
326
326
|
["--agent <id>", "Coding agent that runs the nodes: claude (default), codex, or pi (experimental)"],
|
package/dist/cli/try.d.ts
CHANGED
|
@@ -95,6 +95,8 @@ export interface TryOptions {
|
|
|
95
95
|
fixture?: TryFixture;
|
|
96
96
|
}
|
|
97
97
|
export declare const DEFAULT_TRY_PACE_MS = 1200;
|
|
98
|
+
/** The line that opens the job summary preview (record.ts cuts the preview from here). */
|
|
99
|
+
export declare const SUMMARY_PREVIEW_LEAD = "In CI, the job summary gets this heading, this DAG and the receipt below:";
|
|
98
100
|
/** Replay the recording. Returns the process exit code. */
|
|
99
101
|
export declare function runTry(opts?: TryOptions): Promise<number>;
|
|
100
102
|
export declare function registerTryCommand(program: Command): Command;
|
package/dist/cli/try.js
CHANGED
|
@@ -176,6 +176,8 @@ export function formatTryNext(paint = createPaint(false)) {
|
|
|
176
176
|
}
|
|
177
177
|
export const DEFAULT_TRY_PACE_MS = 1200;
|
|
178
178
|
const silentLogger = { info() { }, warn() { }, error() { }, debug() { } };
|
|
179
|
+
/** The line that opens the job summary preview (record.ts cuts the preview from here). */
|
|
180
|
+
export const SUMMARY_PREVIEW_LEAD = "In CI, the job summary gets this heading, this DAG and the receipt below:";
|
|
179
181
|
/** Replay the recording. Returns the process exit code. */
|
|
180
182
|
export async function runTry(opts = {}) {
|
|
181
183
|
const write = opts.write ?? ((s) => void process.stdout.write(s));
|
|
@@ -247,9 +249,9 @@ export async function runTry(opts = {}) {
|
|
|
247
249
|
const answer = resolveFinalOutput(workflow, results);
|
|
248
250
|
if (answer)
|
|
249
251
|
write(`${formatFinalOutput(answer)}\n\n`);
|
|
250
|
-
// Compact preview of
|
|
252
|
+
// Compact preview of CI's job summary: heading and DAG here, receipt below.
|
|
251
253
|
const comment = formatRunComment(workflow, results, receipt, { trace, durationsMs });
|
|
252
|
-
write(` ${paint.muted(
|
|
254
|
+
write(` ${paint.muted(SUMMARY_PREVIEW_LEAD)}\n\n`);
|
|
253
255
|
const tone = receipt.ok ? paint.success : paint.error;
|
|
254
256
|
write(` ${tone(receipt.ok ? g.success : g.failure)} ${paint.strong(workflow.name)}\n\n`);
|
|
255
257
|
write(`${dag
|
package/dist/executor.js
CHANGED
|
@@ -26,6 +26,7 @@ import { gitCredentialWarning, scanGitCredentials } from "./git-credentials.js";
|
|
|
26
26
|
// #473: the sweny-side branch push in `github_create_pr` (Node only; skills/ stays browser-safe).
|
|
27
27
|
import { bindBranchPusher } from "./skills/git-push.js";
|
|
28
28
|
import { trustedEnvValue } from "./startup-env.js";
|
|
29
|
+
import { toolRunContext } from "./run-context.js";
|
|
29
30
|
import { fenceUntrusted, fenceUntrustedJson } from "./untrusted.js";
|
|
30
31
|
import { asClaude } from "./harness/compat.js";
|
|
31
32
|
import { budgetGate, isToolClass, policyGate, resolveHarnessPolicy } from "./harness/policy.js";
|
|
@@ -213,6 +214,8 @@ async function executeRun(workflow, input, options, results, trace, at) {
|
|
|
213
214
|
const pushBranch = bindBranchPusher(resolveCwd, runEnv);
|
|
214
215
|
// GHES: the REST base for the github skill, never a value the workspace .env introduced.
|
|
215
216
|
const githubApiUrl = trustedEnvValue(runEnv, "GITHUB_API_URL") || undefined;
|
|
217
|
+
// The run's identity and job-summary writer, the same for every tool, from the operator's env.
|
|
218
|
+
const runToolContext = toolRunContext(workflow.id, runEnv);
|
|
216
219
|
const resolvedSources = await resolveSources(sourceMap, {
|
|
217
220
|
cwd: resolveCwd,
|
|
218
221
|
env: options.env ?? process.env,
|
|
@@ -421,6 +424,7 @@ async function executeRun(workflow, input, options, results, trace, at) {
|
|
|
421
424
|
staged: stageOutputs,
|
|
422
425
|
pushBranch,
|
|
423
426
|
...(githubApiUrl ? { githubApiUrl } : {}),
|
|
427
|
+
...runToolContext,
|
|
424
428
|
};
|
|
425
429
|
const output = await t.handler(toolInput, toolCtx);
|
|
426
430
|
safeObserve(observer, { type: "tool:result", node: currentId, tool: t.name, output }, logger);
|
|
@@ -832,6 +836,7 @@ async function executeRun(workflow, input, options, results, trace, at) {
|
|
|
832
836
|
cwd: resolveCwd,
|
|
833
837
|
pushBranch,
|
|
834
838
|
...(githubApiUrl ? { githubApiUrl } : {}),
|
|
839
|
+
...runToolContext,
|
|
835
840
|
state: writeState,
|
|
836
841
|
logger,
|
|
837
842
|
input: runInput,
|
package/dist/index.d.ts
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* })
|
|
22
22
|
* ```
|
|
23
23
|
*/
|
|
24
|
-
export type { Skill, SkillCategory, SkillDefinition, Tool, ToolContext, ConfigField, JSONSchema, Workflow, Node, Edge, EdgeWhen, WhenExpression, NodeResult, ToolCall, ExecutionEvent, Observer, Claude, Logger, TraceStep, TraceEdge, ExecutionTrace, ExecutionResult, Source, ResolvedSource, SourceKind, SourceResolutionMap, WorkflowInputs, WorkflowInputField, WorkflowInputType, InputValidationError, InputValidationResult, Evaluator, EvaluatorRule, EvalResult, EvaluatorKind, NodeRequires, NodeRetry, NodeToolFilter, OutputMatch, NodeSources, EvalPolicy, RequiresOnFail, NodeOnFail, McpTransport, WorkflowType, SkillHarnessKey, NodeAccess, NodePermissions, NodePermissionsSpec, SafeOutputType, SafeOutputDeclaration, SafeOutputsPolicy, SafeOutputReceipt, SafeOutputPin, AuthorAssociation, } from "./types.js";
|
|
24
|
+
export type { Skill, SkillCategory, SkillDefinition, Tool, ToolContext, ToolRunContext, ConfigField, JSONSchema, Workflow, Node, Edge, EdgeWhen, WhenExpression, NodeResult, ToolCall, ExecutionEvent, Observer, Claude, Logger, TraceStep, TraceEdge, ExecutionTrace, ExecutionResult, Source, ResolvedSource, SourceKind, SourceResolutionMap, WorkflowInputs, WorkflowInputField, WorkflowInputType, InputValidationError, InputValidationResult, Evaluator, EvaluatorRule, EvalResult, EvaluatorKind, NodeRequires, NodeRetry, NodeToolFilter, OutputMatch, NodeSources, EvalPolicy, RequiresOnFail, NodeOnFail, McpTransport, WorkflowType, SkillHarnessKey, NodeAccess, NodePermissions, NodePermissionsSpec, SafeOutputType, SafeOutputDeclaration, SafeOutputsPolicy, SafeOutputReceipt, SafeOutputPin, AuthorAssociation, } from "./types.js";
|
|
25
25
|
export { WORKFLOW_INPUT_TYPES } from "./types.js";
|
|
26
26
|
export { EVALUATOR_KINDS, EVAL_POLICIES, REQUIRES_ON_FAIL, NODE_ON_FAIL, MCP_TRANSPORTS, SKILL_CATEGORIES, SKILL_HARNESSES, SKILL_ID_PATTERN, SKILL_ID_MAX_LENGTH, isValidSkillId, NODE_ACCESS, TOOL_CLASSES, SAFE_OUTPUT_TYPES, SAFE_OUTPUT_APPLIERS, AUTHOR_ASSOCIATIONS, } from "./types.js";
|
|
27
27
|
export { consoleLogger } from "./types.js";
|
package/dist/journal.js
CHANGED
|
@@ -1411,7 +1411,7 @@ async function probeProvider(toolName, args, key, skill, ctx, since) {
|
|
|
1411
1411
|
return { state: "unknown", reason: err instanceof Error ? err.message : String(err) };
|
|
1412
1412
|
}
|
|
1413
1413
|
}
|
|
1414
|
-
/** The ids a write produced, nothing else (refs and
|
|
1414
|
+
/** The ids a write produced and a skip reason, nothing else (refs, URLs and skips for receipts). */
|
|
1415
1415
|
function slimOutput(o, depth = 0) {
|
|
1416
1416
|
if (!o || typeof o !== "object" || Array.isArray(o) || depth > 3)
|
|
1417
1417
|
return null;
|
|
@@ -1425,7 +1425,7 @@ function slimOutput(o, depth = 0) {
|
|
|
1425
1425
|
out.transient = true;
|
|
1426
1426
|
}
|
|
1427
1427
|
for (const [k, v] of Object.entries(o)) {
|
|
1428
|
-
if (["number", "identifier", "id", "html_url", "url", "success", "reused"].includes(k)) {
|
|
1428
|
+
if (["number", "identifier", "id", "html_url", "url", "success", "reused", "skipped"].includes(k)) {
|
|
1429
1429
|
if (typeof v === "string" || typeof v === "number" || typeof v === "boolean")
|
|
1430
1430
|
out[k] = v;
|
|
1431
1431
|
}
|