@sweny-ai/core 0.1.159 → 0.1.160
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/cli/final-output.d.ts +41 -0
- package/dist/cli/final-output.js +155 -0
- package/dist/cli/main.js +15 -1
- package/dist/cli/run-history.js +4 -1
- package/dist/cli/run-output.d.ts +25 -0
- package/dist/cli/run-output.js +47 -0
- package/dist/cli/templates.js +27 -1
- package/dist/harness/claude-code.d.ts +2 -0
- package/dist/harness/claude-code.js +13 -1
- package/dist/types.d.ts +14 -0
- package/package.json +1 -1
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The answer at the end of `sweny workflow run`: the terminal node's result
|
|
3
|
+
* (or the failed node's error), rendered for a human.
|
|
4
|
+
*
|
|
5
|
+
* Unlike run-history and the receipt, this DOES read node output. It is
|
|
6
|
+
* local-only: printed to the terminal and saved to
|
|
7
|
+
* `.sweny/runs/<run-id>/output.md`. Nothing here is sent to the cloud.
|
|
8
|
+
*/
|
|
9
|
+
import type { JSONSchema, NodeResult, Workflow } from "../types.js";
|
|
10
|
+
/** Lines of the answer printed to the terminal before the rest is left in output.md. */
|
|
11
|
+
export declare const FINAL_OUTPUT_MAX_LINES = 60;
|
|
12
|
+
export interface FinalOutput {
|
|
13
|
+
kind: "result" | "error";
|
|
14
|
+
/** Node id the text came from. */
|
|
15
|
+
node: string;
|
|
16
|
+
/** Rendered lines, uncapped. */
|
|
17
|
+
lines: string[];
|
|
18
|
+
}
|
|
19
|
+
/** Render a node's declared output fields as a compact `key: value` block. Empty when nothing declared is present. */
|
|
20
|
+
export declare function renderSchemaOutput(data: Record<string, unknown>, schema: JSONSchema): string[];
|
|
21
|
+
/**
|
|
22
|
+
* Pick what the user should see: the last-run terminal node's result on
|
|
23
|
+
* success, the first failed node's error on failure. Null when there is
|
|
24
|
+
* nothing to show (no schema fields and no summary text).
|
|
25
|
+
*/
|
|
26
|
+
export declare function resolveFinalOutput(workflow: Workflow, results: Map<string, NodeResult>): FinalOutput | null;
|
|
27
|
+
/**
|
|
28
|
+
* Terminal text: the first `maxLines` lines, then
|
|
29
|
+
* `... (N more lines, see .sweny/runs/<id>/output.md)`. When no file was
|
|
30
|
+
* saved the pointer is dropped, never invented.
|
|
31
|
+
*/
|
|
32
|
+
export declare function formatFinalOutput(out: FinalOutput, opts?: {
|
|
33
|
+
maxLines?: number;
|
|
34
|
+
outputPath?: string | null;
|
|
35
|
+
}): string;
|
|
36
|
+
/** Markdown saved to output.md: the full, uncapped answer. */
|
|
37
|
+
export declare function formatFinalMarkdown(workflow: Workflow, out: FinalOutput): string;
|
|
38
|
+
/** Relative path shown to the user: `.sweny/runs/<id>/output.md`. */
|
|
39
|
+
export declare function outputRelPath(runId: string): string;
|
|
40
|
+
/** Write output.md. Local only. Returns the relative path, or null on failure. Never throws. */
|
|
41
|
+
export declare function writeFinalOutput(runId: string, markdown: string, cwd?: string): string | null;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The answer at the end of `sweny workflow run`: the terminal node's result
|
|
3
|
+
* (or the failed node's error), rendered for a human.
|
|
4
|
+
*
|
|
5
|
+
* Unlike run-history and the receipt, this DOES read node output. It is
|
|
6
|
+
* local-only: printed to the terminal and saved to
|
|
7
|
+
* `.sweny/runs/<run-id>/output.md`. Nothing here is sent to the cloud.
|
|
8
|
+
*/
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { RUN_HISTORY_DIR } from "./run-history.js";
|
|
12
|
+
/** Lines of the answer printed to the terminal before the rest is left in output.md. */
|
|
13
|
+
export const FINAL_OUTPUT_MAX_LINES = 60;
|
|
14
|
+
const isObj = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
15
|
+
function isEmpty(v) {
|
|
16
|
+
return (v === undefined ||
|
|
17
|
+
v === null ||
|
|
18
|
+
(typeof v === "string" && v.trim() === "") ||
|
|
19
|
+
(Array.isArray(v) && v.length === 0) ||
|
|
20
|
+
(isObj(v) && Object.keys(v).length === 0));
|
|
21
|
+
}
|
|
22
|
+
function scalar(v) {
|
|
23
|
+
return typeof v === "string" ? v : JSON.stringify(v);
|
|
24
|
+
}
|
|
25
|
+
function indentLines(lines, pad) {
|
|
26
|
+
return lines.map((l) => (l === "" ? l : pad + l));
|
|
27
|
+
}
|
|
28
|
+
/** One value as lines under a label: `key: value`, or `key:` plus an indented block. */
|
|
29
|
+
function renderField(label, value, schema) {
|
|
30
|
+
if (Array.isArray(value)) {
|
|
31
|
+
const itemSchema = isObj(schema?.items) ? schema.items : undefined;
|
|
32
|
+
const out = [`${label}:`];
|
|
33
|
+
for (const item of value)
|
|
34
|
+
out.push(...renderListItem(item, itemSchema));
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
if (isObj(value)) {
|
|
38
|
+
return [`${label}:`, ...indentLines(renderObject(value, schema), " ")];
|
|
39
|
+
}
|
|
40
|
+
const text = scalar(value);
|
|
41
|
+
if (!text.includes("\n"))
|
|
42
|
+
return [`${label}: ${text}`];
|
|
43
|
+
return [`${label}:`, ...indentLines(text.split("\n"), " ")];
|
|
44
|
+
}
|
|
45
|
+
function renderListItem(item, schema) {
|
|
46
|
+
if (isObj(item)) {
|
|
47
|
+
const inner = renderObject(item, schema);
|
|
48
|
+
if (inner.length === 0)
|
|
49
|
+
return [];
|
|
50
|
+
return [`- ${inner[0]}`, ...indentLines(inner.slice(1), " ")];
|
|
51
|
+
}
|
|
52
|
+
if (Array.isArray(item)) {
|
|
53
|
+
return [`- ${item.map(scalar).join(", ")}`];
|
|
54
|
+
}
|
|
55
|
+
const text = scalar(item);
|
|
56
|
+
const [first, ...rest] = text.split("\n");
|
|
57
|
+
return [`- ${first}`, ...indentLines(rest, " ")];
|
|
58
|
+
}
|
|
59
|
+
/** Declared fields first (schema order), then none of the extras: the schema is the contract. */
|
|
60
|
+
function renderObject(data, schema) {
|
|
61
|
+
const props = isObj(schema?.properties) ? schema.properties : undefined;
|
|
62
|
+
const keys = props ? Object.keys(props) : Object.keys(data);
|
|
63
|
+
const out = [];
|
|
64
|
+
for (const key of keys) {
|
|
65
|
+
const value = data[key];
|
|
66
|
+
if (isEmpty(value))
|
|
67
|
+
continue;
|
|
68
|
+
const propSchema = props && isObj(props[key]) ? props[key] : undefined;
|
|
69
|
+
const title = typeof propSchema?.title === "string" && propSchema.title.trim() ? propSchema.title.trim() : key;
|
|
70
|
+
out.push(...renderField(title, value, propSchema));
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
/** Render a node's declared output fields as a compact `key: value` block. Empty when nothing declared is present. */
|
|
75
|
+
export function renderSchemaOutput(data, schema) {
|
|
76
|
+
const props = schema.properties;
|
|
77
|
+
if (!isObj(props) || Object.keys(props).length === 0)
|
|
78
|
+
return [];
|
|
79
|
+
return renderObject(data, schema);
|
|
80
|
+
}
|
|
81
|
+
function trimBlock(text) {
|
|
82
|
+
const lines = text.replace(/\r\n/g, "\n").trim().split("\n");
|
|
83
|
+
return lines.map((l) => l.replace(/\s+$/, ""));
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Pick what the user should see: the last-run terminal node's result on
|
|
87
|
+
* success, the first failed node's error on failure. Null when there is
|
|
88
|
+
* nothing to show (no schema fields and no summary text).
|
|
89
|
+
*/
|
|
90
|
+
export function resolveFinalOutput(workflow, results) {
|
|
91
|
+
for (const [id, r] of results) {
|
|
92
|
+
if (r.status !== "failed")
|
|
93
|
+
continue;
|
|
94
|
+
const err = r.data?.error;
|
|
95
|
+
const text = typeof err === "string" && err.trim() !== "" ? err : "node failed";
|
|
96
|
+
return { kind: "error", node: id, lines: trimBlock(text) };
|
|
97
|
+
}
|
|
98
|
+
const hasOutgoing = new Set(workflow.edges.map((e) => e.from));
|
|
99
|
+
const succeeded = [...results].filter(([, r]) => r.status === "success");
|
|
100
|
+
const terminal = succeeded.filter(([id]) => !hasOutgoing.has(id));
|
|
101
|
+
// Walk back from the end: the last node that actually yields something wins.
|
|
102
|
+
const candidates = [...(terminal.length > 0 ? terminal : succeeded)].reverse();
|
|
103
|
+
for (const [id, r] of candidates) {
|
|
104
|
+
const schema = workflow.nodes[id]?.output;
|
|
105
|
+
let lines = [];
|
|
106
|
+
if (schema)
|
|
107
|
+
lines = renderSchemaOutput(r.data ?? {}, schema);
|
|
108
|
+
if (lines.length === 0) {
|
|
109
|
+
const summary = r.data?.summary;
|
|
110
|
+
if (typeof summary === "string" && summary.trim() !== "")
|
|
111
|
+
lines = trimBlock(summary);
|
|
112
|
+
}
|
|
113
|
+
if (lines.length > 0)
|
|
114
|
+
return { kind: "result", node: id, lines };
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Terminal text: the first `maxLines` lines, then
|
|
120
|
+
* `... (N more lines, see .sweny/runs/<id>/output.md)`. When no file was
|
|
121
|
+
* saved the pointer is dropped, never invented.
|
|
122
|
+
*/
|
|
123
|
+
export function formatFinalOutput(out, opts = {}) {
|
|
124
|
+
const max = opts.maxLines ?? FINAL_OUTPUT_MAX_LINES;
|
|
125
|
+
const shown = out.lines.slice(0, max);
|
|
126
|
+
const hidden = out.lines.length - shown.length;
|
|
127
|
+
if (hidden > 0) {
|
|
128
|
+
const noun = hidden === 1 ? "line" : "lines";
|
|
129
|
+
shown.push(opts.outputPath ? `... (${hidden} more ${noun}, see ${opts.outputPath})` : `... (${hidden} more ${noun})`);
|
|
130
|
+
}
|
|
131
|
+
return shown.map((l) => (l === "" ? "" : ` ${l}`)).join("\n");
|
|
132
|
+
}
|
|
133
|
+
/** Markdown saved to output.md: the full, uncapped answer. */
|
|
134
|
+
export function formatFinalMarkdown(workflow, out) {
|
|
135
|
+
const nodeName = workflow.nodes[out.node]?.name ?? out.node;
|
|
136
|
+
const head = out.kind === "error" ? `# ${workflow.name}: ${nodeName} failed` : `# ${workflow.name}`;
|
|
137
|
+
return `${head}\n\n${out.lines.join("\n")}\n`;
|
|
138
|
+
}
|
|
139
|
+
/** Relative path shown to the user: `.sweny/runs/<id>/output.md`. */
|
|
140
|
+
export function outputRelPath(runId) {
|
|
141
|
+
return path.posix.join(...RUN_HISTORY_DIR.split(path.sep), runId, "output.md");
|
|
142
|
+
}
|
|
143
|
+
/** Write output.md. Local only. Returns the relative path, or null on failure. Never throws. */
|
|
144
|
+
export function writeFinalOutput(runId, markdown, cwd = process.cwd()) {
|
|
145
|
+
try {
|
|
146
|
+
const rel = outputRelPath(runId);
|
|
147
|
+
const file = path.join(cwd, rel);
|
|
148
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
149
|
+
fs.writeFileSync(file, markdown);
|
|
150
|
+
return rel;
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
}
|
package/dist/cli/main.js
CHANGED
|
@@ -30,6 +30,7 @@ import * as readline from "node:readline";
|
|
|
30
30
|
import { loadDotenv, loadConfigFile, applyAgentFileConfig } from "./config-file.js";
|
|
31
31
|
import { buildCredentialMap } from "./credentials.js";
|
|
32
32
|
import { nonInteractiveUsage, runNew } from "./new.js";
|
|
33
|
+
import { formatFinalMarkdown, formatFinalOutput, resolveFinalOutput, writeFinalOutput } from "./final-output.js";
|
|
33
34
|
import { buildRunRecord, createNodeTimer, historyDisabled, newRunId, recordRun } from "./run-history.js";
|
|
34
35
|
import { registerRunsCommand } from "./runs.js";
|
|
35
36
|
import { runE2eRun, runWithWallClockBudget, DEFAULT_WORKFLOW_TIMEOUT_MS } from "./e2e.js";
|
|
@@ -1049,6 +1050,19 @@ export async function workflowRunAction(file, options) {
|
|
|
1049
1050
|
runLogger.flush();
|
|
1050
1051
|
const receipt = summarizeRun(results, wfDurationMs);
|
|
1051
1052
|
writeStepSummary(workflow, results, receipt, trace);
|
|
1053
|
+
// The answer, above the receipt: the terminal node's result, or the failed node's error.
|
|
1054
|
+
// Also saved to .sweny/runs/<id>/output.md (local only; --no-history skips the file).
|
|
1055
|
+
const finalOutput = resolveFinalOutput(workflow, results);
|
|
1056
|
+
if (finalOutput) {
|
|
1057
|
+
const savedTo = historyDisabled(options.history, fileConfig["history"])
|
|
1058
|
+
? null
|
|
1059
|
+
: writeFinalOutput(runId, formatFinalMarkdown(workflow, finalOutput));
|
|
1060
|
+
const block = formatFinalOutput(finalOutput, { outputPath: savedTo });
|
|
1061
|
+
if (wfHasFailed)
|
|
1062
|
+
console.error(`${block}\n`);
|
|
1063
|
+
else
|
|
1064
|
+
console.log(`${block}\n`);
|
|
1065
|
+
}
|
|
1052
1066
|
if (wfHasFailed) {
|
|
1053
1067
|
console.error(` ${renderReceiptLine(receipt, isTTY)}\n`);
|
|
1054
1068
|
process.exit(1);
|
|
@@ -1142,7 +1156,7 @@ const workflowRunCmd = workflowCmd
|
|
|
1142
1156
|
for (const [flags, description] of WORKFLOW_RUN_OPTIONS) {
|
|
1143
1157
|
workflowRunCmd.option(flags, description);
|
|
1144
1158
|
}
|
|
1145
|
-
workflowRunCmd.option("--no-history", "Do not record this run in .sweny/runs/ (or set `history: off` in .sweny.yml)");
|
|
1159
|
+
workflowRunCmd.option("--no-history", "Do not record this run or save its output.md in .sweny/runs/ (or set `history: off` in .sweny.yml)");
|
|
1146
1160
|
registerRunsCommand(program);
|
|
1147
1161
|
workflowCmd
|
|
1148
1162
|
.command("diagram <file>")
|
package/dist/cli/run-history.js
CHANGED
|
@@ -153,8 +153,11 @@ export function pruneRuns(cwd = process.cwd(), keep = RUN_HISTORY_KEEP) {
|
|
|
153
153
|
.filter((f) => f.endsWith(".json") && RUN_ID_RE.test(f.slice(0, -5)))
|
|
154
154
|
.sort();
|
|
155
155
|
const extra = files.slice(0, Math.max(0, files.length - keep));
|
|
156
|
-
for (const f of extra)
|
|
156
|
+
for (const f of extra) {
|
|
157
157
|
fs.rmSync(path.join(dir, f), { force: true });
|
|
158
|
+
// output.md lives beside the record in <run-id>/ (final-output.ts)
|
|
159
|
+
fs.rmSync(path.join(dir, f.slice(0, -5)), { recursive: true, force: true });
|
|
160
|
+
}
|
|
158
161
|
return extra.length;
|
|
159
162
|
}
|
|
160
163
|
catch {
|
package/dist/cli/run-output.d.ts
CHANGED
|
@@ -25,6 +25,12 @@ export interface RunSummary {
|
|
|
25
25
|
* (`max_turns`, `egress allowlist`, `deny [write]`). Absent when none.
|
|
26
26
|
*/
|
|
27
27
|
degraded?: string[];
|
|
28
|
+
/**
|
|
29
|
+
* What the run's opinions did, from facts the nodes reported: env scope and
|
|
30
|
+
* sandbox (Claude Code only) and safe-output counts. Absent when the run
|
|
31
|
+
* knows none of it.
|
|
32
|
+
*/
|
|
33
|
+
policy?: RunPolicy;
|
|
28
34
|
/** The spend budget a node crossed (#449). Absent when none was. */
|
|
29
35
|
budget?: {
|
|
30
36
|
node: string;
|
|
@@ -34,10 +40,29 @@ export interface RunSummary {
|
|
|
34
40
|
spent: number;
|
|
35
41
|
};
|
|
36
42
|
}
|
|
43
|
+
export interface RunPolicy {
|
|
44
|
+
/** Agent env narrowed to the allowlist. Absent when no node reported it. */
|
|
45
|
+
envScope?: boolean;
|
|
46
|
+
/** Sandbox facts. `started` is false when the mode asked for one but the host could not. */
|
|
47
|
+
sandbox?: {
|
|
48
|
+
mode: "off" | "auto" | "strict";
|
|
49
|
+
started: boolean;
|
|
50
|
+
};
|
|
51
|
+
/** Safe outputs staged (previewed) and applied (written), summed over nodes. */
|
|
52
|
+
outputs?: {
|
|
53
|
+
staged: number;
|
|
54
|
+
applied: number;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
37
57
|
/** The short key of a `degraded` entry: the text before its first colon. */
|
|
38
58
|
export declare function degradedKey(entry: string): string;
|
|
39
59
|
/** Sum the per-node results into one run summary. */
|
|
40
60
|
export declare function summarizeRun(results: Map<string, NodeResult>, durationMs: number, crashed?: boolean): RunSummary;
|
|
61
|
+
/**
|
|
62
|
+
* `policy: env scoped, sandbox on, 1 output staged`. Only what the run knew:
|
|
63
|
+
* no segment for a fact no node reported. Undefined when there is nothing to say.
|
|
64
|
+
*/
|
|
65
|
+
export declare function formatPolicySegment(p: RunPolicy | undefined): string | undefined;
|
|
41
66
|
export declare function formatReceiptDuration(ms: number): string;
|
|
42
67
|
export declare function formatTokenCount(n: number): string;
|
|
43
68
|
export declare function formatCost(usd: number): string;
|
package/dist/cli/run-output.js
CHANGED
|
@@ -25,6 +25,9 @@ export function summarizeRun(results, durationMs, crashed = false) {
|
|
|
25
25
|
let harness;
|
|
26
26
|
const degraded = new Set();
|
|
27
27
|
let budget;
|
|
28
|
+
const policy = {};
|
|
29
|
+
let staged = 0;
|
|
30
|
+
let applied = 0;
|
|
28
31
|
const isNum = (v) => typeof v === "number" && Number.isFinite(v);
|
|
29
32
|
for (const [nodeId, r] of results) {
|
|
30
33
|
if (r.budget && !budget)
|
|
@@ -32,6 +35,20 @@ export function summarizeRun(results, durationMs, crashed = false) {
|
|
|
32
35
|
harness ??= r.harness?.id;
|
|
33
36
|
for (const d of r.degraded ?? [])
|
|
34
37
|
degraded.add(degradedKey(d));
|
|
38
|
+
if (r.policy) {
|
|
39
|
+
// Any node that ran unscoped or unsandboxed counts: the receipt must not overstate.
|
|
40
|
+
policy.envScope = policy.envScope === undefined ? r.policy.envScope : policy.envScope && r.policy.envScope;
|
|
41
|
+
const sb = { mode: r.policy.sandbox, started: r.policy.sandboxStarted };
|
|
42
|
+
// Keep the weakest node: the first one that did not run sandboxed.
|
|
43
|
+
if (!policy.sandbox || (policy.sandbox.started && !sb.started))
|
|
44
|
+
policy.sandbox = sb;
|
|
45
|
+
}
|
|
46
|
+
for (const o of r.outputs ?? []) {
|
|
47
|
+
if (o.status === "staged")
|
|
48
|
+
staged++;
|
|
49
|
+
else if (o.status === "applied")
|
|
50
|
+
applied++;
|
|
51
|
+
}
|
|
35
52
|
if (r.status === "success")
|
|
36
53
|
nodesOk++;
|
|
37
54
|
else if (r.status === "skipped")
|
|
@@ -49,6 +66,8 @@ export function summarizeRun(results, durationMs, crashed = false) {
|
|
|
49
66
|
costUsd = (costUsd ?? 0) + u.costUsd;
|
|
50
67
|
}
|
|
51
68
|
}
|
|
69
|
+
if (staged > 0 || applied > 0)
|
|
70
|
+
policy.outputs = { staged, applied };
|
|
52
71
|
return {
|
|
53
72
|
ok: !failed,
|
|
54
73
|
nodesOk,
|
|
@@ -60,9 +79,36 @@ export function summarizeRun(results, durationMs, crashed = false) {
|
|
|
60
79
|
...(costUsd !== undefined ? { costUsd } : {}),
|
|
61
80
|
...(harness !== undefined ? { harness } : {}),
|
|
62
81
|
...(degraded.size > 0 ? { degraded: [...degraded] } : {}),
|
|
82
|
+
...(Object.keys(policy).length > 0 ? { policy } : {}),
|
|
63
83
|
...(budget ? { budget } : {}),
|
|
64
84
|
};
|
|
65
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* `policy: env scoped, sandbox on, 1 output staged`. Only what the run knew:
|
|
88
|
+
* no segment for a fact no node reported. Undefined when there is nothing to say.
|
|
89
|
+
*/
|
|
90
|
+
export function formatPolicySegment(p) {
|
|
91
|
+
if (!p)
|
|
92
|
+
return undefined;
|
|
93
|
+
const parts = [];
|
|
94
|
+
if (p.envScope !== undefined)
|
|
95
|
+
parts.push(p.envScope ? "env scoped" : "env unscoped");
|
|
96
|
+
if (p.sandbox) {
|
|
97
|
+
parts.push(p.sandbox.mode === "off"
|
|
98
|
+
? "sandbox off"
|
|
99
|
+
: p.sandbox.started
|
|
100
|
+
? "sandbox on"
|
|
101
|
+
: "sandbox unavailable (ran unsandboxed)");
|
|
102
|
+
}
|
|
103
|
+
if (p.outputs) {
|
|
104
|
+
const n = (count) => `${count} ${count === 1 ? "output" : "outputs"}`;
|
|
105
|
+
if (p.outputs.staged > 0)
|
|
106
|
+
parts.push(`${n(p.outputs.staged)} staged`);
|
|
107
|
+
if (p.outputs.applied > 0)
|
|
108
|
+
parts.push(`${n(p.outputs.applied)} applied`);
|
|
109
|
+
}
|
|
110
|
+
return parts.length > 0 ? `policy: ${parts.join(", ")}` : undefined;
|
|
111
|
+
}
|
|
66
112
|
export function formatReceiptDuration(ms) {
|
|
67
113
|
if (ms < 1000)
|
|
68
114
|
return `${Math.max(0, Math.round(ms))}ms`;
|
|
@@ -108,6 +154,7 @@ export function formatReceipt(s) {
|
|
|
108
154
|
...(s.costUsd !== undefined ? [formatCost(s.costUsd)] : []),
|
|
109
155
|
...(s.harness !== undefined && s.harness !== "claude-code" ? [s.harness] : []),
|
|
110
156
|
...(s.degraded && s.degraded.length > 0 ? [`degraded: ${s.degraded.join(", ")}`] : []),
|
|
157
|
+
...(formatPolicySegment(s.policy) ? [formatPolicySegment(s.policy)] : []),
|
|
111
158
|
...(s.budget ? [formatBudgetOverrun(s.budget)] : []),
|
|
112
159
|
];
|
|
113
160
|
return parts.join(" · ");
|
package/dist/cli/templates.js
CHANGED
|
@@ -31,7 +31,33 @@ nodes:
|
|
|
31
31
|
From the survey, write a short plain-English explanation:
|
|
32
32
|
what this project does, how it is organized, how to build and
|
|
33
33
|
run it, and where a new contributor should start.
|
|
34
|
-
Keep
|
|
34
|
+
Keep every field short and concrete: a sentence or a few
|
|
35
|
+
bullets, in plain English, naming real files and commands.
|
|
36
|
+
output:
|
|
37
|
+
type: object
|
|
38
|
+
properties:
|
|
39
|
+
purpose:
|
|
40
|
+
type: string
|
|
41
|
+
title: What it is
|
|
42
|
+
description: One or two sentences on what this project does and who it is for.
|
|
43
|
+
how_to_run:
|
|
44
|
+
type: array
|
|
45
|
+
title: How to run it
|
|
46
|
+
description: The commands, in order, to install, build, test, and start it.
|
|
47
|
+
items:
|
|
48
|
+
type: string
|
|
49
|
+
key_files:
|
|
50
|
+
type: array
|
|
51
|
+
title: Key files
|
|
52
|
+
description: The files and directories a new contributor should open first, each with a few words on why.
|
|
53
|
+
items:
|
|
54
|
+
type: string
|
|
55
|
+
risks:
|
|
56
|
+
type: array
|
|
57
|
+
title: Watch out for
|
|
58
|
+
description: Sharp edges, missing docs, or things that look fragile. Empty if none.
|
|
59
|
+
items:
|
|
60
|
+
type: string
|
|
35
61
|
|
|
36
62
|
edges:
|
|
37
63
|
- from: survey
|
|
@@ -150,6 +150,8 @@ export declare class ClaudeCodeHarness implements Claude, AgentHarness {
|
|
|
150
150
|
private sandboxProbe;
|
|
151
151
|
private sandboxWarned;
|
|
152
152
|
private envScope;
|
|
153
|
+
/** Floor facts from the latest runQuery, read by run() for the receipt. */
|
|
154
|
+
private lastPolicyFacts;
|
|
153
155
|
private toolBridge;
|
|
154
156
|
private toolBridgeShim;
|
|
155
157
|
private authProbe;
|
|
@@ -217,6 +217,8 @@ export class ClaudeCodeHarness {
|
|
|
217
217
|
sandboxProbe;
|
|
218
218
|
sandboxWarned = false;
|
|
219
219
|
envScope;
|
|
220
|
+
/** Floor facts from the latest runQuery, read by run() for the receipt. */
|
|
221
|
+
lastPolicyFacts;
|
|
220
222
|
toolBridge;
|
|
221
223
|
toolBridgeShim;
|
|
222
224
|
authProbe;
|
|
@@ -312,7 +314,12 @@ export class ClaudeCodeHarness {
|
|
|
312
314
|
sandboxMode: policy.sandbox,
|
|
313
315
|
agentAccess: { envVars: req.agentAccess?.envVars ?? [], domains: policy.egress, noPush: req.agentAccess?.noPush },
|
|
314
316
|
});
|
|
315
|
-
return {
|
|
317
|
+
return {
|
|
318
|
+
...result,
|
|
319
|
+
harness: this.info(),
|
|
320
|
+
degraded: gate.degraded,
|
|
321
|
+
...(this.lastPolicyFacts ? { policy: this.lastPolicyFacts } : {}),
|
|
322
|
+
};
|
|
316
323
|
}
|
|
317
324
|
async runQuery(opts) {
|
|
318
325
|
const { instruction, context, tools, outputSchema, onProgress, maxTurns, model, timeoutMs, signal, readOnly, agentAccess, onUsage, } = opts;
|
|
@@ -334,6 +341,11 @@ export class ClaudeCodeHarness {
|
|
|
334
341
|
probe: this.sandboxProbe,
|
|
335
342
|
logger: this.logger,
|
|
336
343
|
});
|
|
344
|
+
this.lastPolicyFacts = {
|
|
345
|
+
envScope: resolveEnvScope(process.env, this.envScope),
|
|
346
|
+
sandbox: sandbox.mode,
|
|
347
|
+
sandboxStarted: sandbox.settings !== undefined,
|
|
348
|
+
};
|
|
337
349
|
if (sandbox.error) {
|
|
338
350
|
this.logger.error(sandbox.error);
|
|
339
351
|
return { status: "failed", data: { error: sandbox.error, refused: true }, toolCalls: [] };
|
package/dist/types.d.ts
CHANGED
|
@@ -622,6 +622,11 @@ export interface NodeResult {
|
|
|
622
622
|
};
|
|
623
623
|
/** Opinions this run could not honor natively. Always empty for Claude Code. */
|
|
624
624
|
degraded?: string[];
|
|
625
|
+
/**
|
|
626
|
+
* Agent floor this node actually ran under, for the run receipt. Set by the
|
|
627
|
+
* Claude Code harness only; absent for harnesses that do not report it.
|
|
628
|
+
*/
|
|
629
|
+
policy?: NodePolicyFacts;
|
|
625
630
|
/** Safe outputs (#365): what the write stage did with each intent. Absent when the node declares none. */
|
|
626
631
|
outputs?: SafeOutputReceipt[];
|
|
627
632
|
/**
|
|
@@ -631,6 +636,15 @@ export interface NodeResult {
|
|
|
631
636
|
*/
|
|
632
637
|
budget?: BudgetOverrun;
|
|
633
638
|
}
|
|
639
|
+
/** Facts about the agent floor (scoped env, process sandbox) a node ran under. Booleans and a mode only. */
|
|
640
|
+
export interface NodePolicyFacts {
|
|
641
|
+
/** The agent subprocess env was narrowed to the allowlist. */
|
|
642
|
+
envScope: boolean;
|
|
643
|
+
/** Requested sandbox mode. */
|
|
644
|
+
sandbox: "off" | "auto" | "strict";
|
|
645
|
+
/** The sandbox was enabled for this node (false under `off`, or `auto` on a host that cannot sandbox). */
|
|
646
|
+
sandboxStarted: boolean;
|
|
647
|
+
}
|
|
634
648
|
export interface ToolCall {
|
|
635
649
|
tool: string;
|
|
636
650
|
input: unknown;
|