@turri-dev/cli 0.1.8-next.e553476 → 0.1.8
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 +1 -3
- package/dist/cli.js +6 -2
- package/dist/config-edit.d.ts +1 -1
- package/dist/config-edit.js +4 -0
- package/dist/config.d.ts +27 -0
- package/dist/config.js +28 -1
- package/dist/extension.js +75 -9
- package/dist/genome.js +2 -0
- package/dist/host.d.ts +3 -1
- package/dist/host.js +4 -1
- package/dist/init-config.d.ts +3 -0
- package/dist/init-config.js +15 -0
- package/dist/ledger-cli.d.ts +1 -0
- package/dist/ledger-cli.js +50 -0
- package/dist/ledger-tools.d.ts +10 -0
- package/dist/ledger-tools.js +64 -0
- package/dist/loop-cli.js +4 -0
- package/dist/pi.d.ts +2 -0
- package/dist/pi.js +24 -4
- package/dist/regenerate/activity.d.ts +9 -0
- package/dist/regenerate/activity.js +11 -0
- package/dist/regenerate/agent-env.d.ts +7 -0
- package/dist/regenerate/agent-env.js +126 -5
- package/dist/regenerate/agent-shell.js +5 -0
- package/dist/regenerate/chunk-plan.d.ts +5 -6
- package/dist/regenerate/chunk-plan.js +43 -10
- package/dist/regenerate/conformance-agents.js +2 -2
- package/dist/regenerate/conformance.js +6 -3
- package/dist/regenerate/debug-log.d.ts +3 -1
- package/dist/regenerate/debug-log.js +10 -2
- package/dist/regenerate/diagnose.js +12 -2
- package/dist/regenerate/environment-error.d.ts +6 -0
- package/dist/regenerate/environment-error.js +19 -1
- package/dist/regenerate/job-log.d.ts +3 -1
- package/dist/regenerate/job-log.js +8 -5
- package/dist/regenerate/jobs-tree.d.ts +7 -3
- package/dist/regenerate/jobs-tree.js +21 -6
- package/dist/regenerate/jobs.d.ts +9 -0
- package/dist/regenerate/jobs.js +51 -5
- package/dist/regenerate/ledger-view.d.ts +92 -0
- package/dist/regenerate/ledger-view.js +345 -0
- package/dist/regenerate/ledger.d.ts +275 -0
- package/dist/regenerate/ledger.js +349 -0
- package/dist/regenerate/lineage.d.ts +9 -1
- package/dist/regenerate/lineage.js +64 -12
- package/dist/regenerate/loop.js +12 -3
- package/dist/regenerate/pulse-files.d.ts +14 -0
- package/dist/regenerate/pulse-files.js +63 -0
- package/dist/regenerate/regenerate.js +7 -5
- package/dist/regenerate/regression.js +4 -2
- package/dist/regenerate/services.js +3 -0
- package/dist/regenerate/side-panel.d.ts +2 -0
- package/dist/regenerate/side-panel.js +4 -3
- package/dist/regenerate/test-scripts.js +18 -5
- package/dist/regenerate/testing.js +3 -0
- package/dist/regenerate/unit-review.js +3 -0
- package/dist/run-report.js +9 -5
- package/docs/config.md +23 -2
- package/docs/development.md +3 -1
- package/docs/headless.md +5 -4
- package/docs/hosting.md +29 -0
- package/docs/installation.md +1 -0
- package/docs/regenerating.md +7 -7
- package/docs/sandbox.md +5 -5
- package/docs/using.md +36 -5
- package/package.json +1 -1
package/dist/pi.js
CHANGED
|
@@ -2,13 +2,14 @@ import { basename, dirname, join, resolve, sep } from "node:path";
|
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
3
|
import { createAgentSession, DefaultResourceLoader, getAgentDir, ModelRegistry, ModelRuntime, SessionManager, } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { loadRules, renderRulesBlock } from "./rules.js";
|
|
5
|
-
import { inheritModel, stackOf, readProjectConfig, ROLE_THINKING, roleModel, roleTurnLimitMs, splitModelSpec } from "./config.js";
|
|
5
|
+
import { debugMode, inheritModel, stackOf, readProjectConfig, ROLE_THINKING, roleModel, roleTurnLimitMs, splitModelSpec } from "./config.js";
|
|
6
6
|
import { maskToolResults } from "./regenerate/masking.js";
|
|
7
7
|
import { withListArguments } from "./regenerate/list-arguments.js";
|
|
8
8
|
import { checkAgainstScript, ENVIRONMENT_ERROR_TOOL, EnvironmentError, environmentErrorTool, sandboxGapOf } from "./regenerate/environment-error.js";
|
|
9
9
|
import { MODULES_DIR, projectRootOf, sessionsDir } from "./regenerate/paths.js";
|
|
10
10
|
import { currentStopSignal, describeToolCall, describeToolResult, JobStoppedError, shownName, reportActivity, reportNow, reportAgent, reportTranscript, reportModelEvent, reportUsage, } from "./regenerate/activity.js";
|
|
11
11
|
import { debugLogOf, startDebugLog } from "./regenerate/debug-log.js";
|
|
12
|
+
import { firstLine, record } from "./regenerate/ledger.js";
|
|
12
13
|
let thinkingSource;
|
|
13
14
|
/**
|
|
14
15
|
* Where sub-agents take their thinking level from: the conversation that started them.
|
|
@@ -280,6 +281,8 @@ async function askOnce(session, text, onDelta) {
|
|
|
280
281
|
// Calls to one tool failing one after another - arguments it refuses, or errors - with nothing going through.
|
|
281
282
|
let failing = 0;
|
|
282
283
|
let failingTool;
|
|
284
|
+
/** When each tool call under way started, by its id: how long it took is recorded as it ends. */
|
|
285
|
+
const toolStarted = new Map();
|
|
283
286
|
harnessStops.delete(session);
|
|
284
287
|
reportNow("waiting for the model");
|
|
285
288
|
// Every entry says which agent it is from, and what it runs on - for reading one agent at a time.
|
|
@@ -348,6 +351,7 @@ async function askOnce(session, text, onDelta) {
|
|
|
348
351
|
}
|
|
349
352
|
}
|
|
350
353
|
if (event.type === "tool_execution_start") {
|
|
354
|
+
toolStarted.set(event.toolCallId, Date.now());
|
|
351
355
|
const args = JSON.stringify(event.args ?? {});
|
|
352
356
|
transcript({ kind: "tool", text: `${shownName(event.toolName)} ${clipText(args, MAX_TOOL_ARGS)}`, full: `${shownName(event.toolName)} ${args}` });
|
|
353
357
|
const took = Date.now() - stepStarted;
|
|
@@ -369,6 +373,9 @@ async function askOnce(session, text, onDelta) {
|
|
|
369
373
|
stopTurn(session, `${failing} ${shownName(event.toolName)} calls failed in a row`);
|
|
370
374
|
}
|
|
371
375
|
const output = resultText(event.result);
|
|
376
|
+
const started = toolStarted.get(event.toolCallId);
|
|
377
|
+
toolStarted.delete(event.toolCallId);
|
|
378
|
+
record({ kind: "tool.call", tool: event.toolName, ms: started !== undefined ? Date.now() - started : 0, ok: !event.isError, resultChars: output.length }, { agent: modelEventOf(session) });
|
|
372
379
|
transcript({ kind: "result", text: clipText(output, MAX_TOOL_RESULT), error: event.isError, full: output });
|
|
373
380
|
const line = describeToolResult(event.toolName, event.result, event.isError);
|
|
374
381
|
if (line !== undefined)
|
|
@@ -483,6 +490,7 @@ async function askOnce(session, text, onDelta) {
|
|
|
483
490
|
// A turn turri ended is ended, whatever the provider was doing then: what comes back of the abort is not the provider failing.
|
|
484
491
|
if (providerError !== undefined && stoppedFor === undefined) {
|
|
485
492
|
debug?.note(`turn ended with a provider error after ${seconds(Date.now() - turnStarted)}: ${providerError}`);
|
|
493
|
+
record({ kind: "provider.error", error: firstLine(providerError, 300), transient: TRANSIENT_PROVIDER_ERROR.test(providerError) }, { agent: modelEventOf(session) });
|
|
486
494
|
throw new ProviderError(`${session.model?.id ?? "model"}: ${providerError}`);
|
|
487
495
|
}
|
|
488
496
|
debug?.note(`turn finished after ${seconds(Date.now() - turnStarted)}`);
|
|
@@ -659,6 +667,11 @@ export function guardRepeats(tools, session, budget) {
|
|
|
659
667
|
// Calls refused in the restricted turn under way, and which turn's set that was.
|
|
660
668
|
let refused = 0;
|
|
661
669
|
let refusedIn;
|
|
670
|
+
/** The agent a refusal is of, for the ledger. */
|
|
671
|
+
const guardPlace = () => {
|
|
672
|
+
const current = session();
|
|
673
|
+
return current !== undefined ? { agent: modelEventOf(current) } : {};
|
|
674
|
+
};
|
|
662
675
|
return tools.map((tool) => ({
|
|
663
676
|
...tool,
|
|
664
677
|
execute: async (...args) => {
|
|
@@ -669,6 +682,7 @@ export function guardRepeats(tools, session, budget) {
|
|
|
669
682
|
if (refusedIn !== only)
|
|
670
683
|
(refused = 0), (refusedIn = only);
|
|
671
684
|
refused += 1;
|
|
685
|
+
record({ kind: "guard.refused", rule: "restricted", tool: tool.name, times: refused }, guardPlace());
|
|
672
686
|
if (refused > REFUSED_BEFORE_STOP) {
|
|
673
687
|
reportActivity(` stopped the agent's turn: ${refused} calls to tools this turn does not have`);
|
|
674
688
|
stopTurn(current, `${refused} calls to tools this turn does not have`);
|
|
@@ -679,6 +693,7 @@ export function guardRepeats(tools, session, budget) {
|
|
|
679
693
|
}
|
|
680
694
|
calls += 1;
|
|
681
695
|
if (budget !== undefined && !budget.finish.includes(tool.name) && calls > budget.calls) {
|
|
696
|
+
record({ kind: "guard.refused", rule: "budget", tool: tool.name, times: calls - budget.calls }, guardPlace());
|
|
682
697
|
if (calls > budget.calls * 2) {
|
|
683
698
|
reportActivity(` stopped the agent's turn: ${calls} tool calls, past its budget of ${budget.calls}`);
|
|
684
699
|
stopTurn(session(), `${calls} tool calls, past its budget of ${budget.calls}`);
|
|
@@ -707,6 +722,8 @@ export function guardRepeats(tools, session, budget) {
|
|
|
707
722
|
// Anything else may have changed what a look would find.
|
|
708
723
|
looks.clear();
|
|
709
724
|
}
|
|
725
|
+
if (repeats > REPEATS_ALLOWED)
|
|
726
|
+
record({ kind: "guard.refused", rule: "repeat", tool: tool.name, times: repeats }, guardPlace());
|
|
710
727
|
if (repeats > REPEATS_BEFORE_STOP) {
|
|
711
728
|
reportActivity(` stopped the agent's turn: it made the same ${tool.name} call ${repeats} times with nothing changed`);
|
|
712
729
|
stopTurn(session(), `the same ${tool.name} call ${repeats} times with nothing changed`);
|
|
@@ -736,11 +753,14 @@ export function guardRepeats(tools, session, budget) {
|
|
|
736
753
|
* start that has forgotten what it just wrote.
|
|
737
754
|
*/
|
|
738
755
|
export async function createExecutor(opts) {
|
|
739
|
-
// Saved to disk, in the project
|
|
740
|
-
//
|
|
756
|
+
// Saved to disk, in the project, for an agent picked back up after the process that ran it is gone - a skill's run -
|
|
757
|
+
// and in debug mode, as the record of what each did. Nothing else reads one back: the rest are kept in memory.
|
|
758
|
+
const project = projectRootOf(opts.cwd);
|
|
741
759
|
const sessionManager = opts.resumeFrom !== undefined
|
|
742
760
|
? SessionManager.open(opts.resumeFrom)
|
|
743
|
-
:
|
|
761
|
+
: opts.saveSession === true || debugMode(project)
|
|
762
|
+
? SessionManager.create(opts.cwd, sessionsDir(project))
|
|
763
|
+
: SessionManager.inMemory(opts.cwd);
|
|
744
764
|
// An agent that writes and runs code - given the harness's run its reports are checked against - can say the machine
|
|
745
765
|
// lacks what the work needs: its turn ends, and the pulse stops with it. Observers, explorers and judges cannot.
|
|
746
766
|
const hasShell = opts.customTools.some((tool) => tool.name === "bash") && opts.checkEnvironment !== undefined;
|
|
@@ -36,6 +36,9 @@ export interface ActivitySink {
|
|
|
36
36
|
kind: string;
|
|
37
37
|
summary: string;
|
|
38
38
|
};
|
|
39
|
+
/** The project the job works in, and its module: where what it does is recorded (ledger.ts). */
|
|
40
|
+
project?: string;
|
|
41
|
+
module?: string;
|
|
39
42
|
/** The spec was found to contradict itself - for the person to decide. */
|
|
40
43
|
specConflict?(conflict: SpecConflict): void;
|
|
41
44
|
/** The functional spec being built now, or nothing when none is. */
|
|
@@ -89,6 +92,12 @@ export declare function currentJob(): {
|
|
|
89
92
|
kind: string;
|
|
90
93
|
summary: string;
|
|
91
94
|
} | undefined;
|
|
95
|
+
/** Where this code runs: the job it runs for, its project and its module - each when it runs for one. */
|
|
96
|
+
export declare function currentPlace(): {
|
|
97
|
+
job?: string;
|
|
98
|
+
project?: string;
|
|
99
|
+
module?: string;
|
|
100
|
+
};
|
|
92
101
|
/**
|
|
93
102
|
* The conversation an agent is having, as it happens: what it was asked, what it wrote and
|
|
94
103
|
* thought, what it called and what came back. Text and thinking arrive as deltas and are
|
|
@@ -22,6 +22,17 @@ export function currentStopSignal() {
|
|
|
22
22
|
export function currentJob() {
|
|
23
23
|
return store.getStore()?.job;
|
|
24
24
|
}
|
|
25
|
+
/** Where this code runs: the job it runs for, its project and its module - each when it runs for one. */
|
|
26
|
+
export function currentPlace() {
|
|
27
|
+
const sink = store.getStore();
|
|
28
|
+
if (sink === undefined)
|
|
29
|
+
return {};
|
|
30
|
+
return {
|
|
31
|
+
...(sink.job !== undefined ? { job: sink.job.id } : {}),
|
|
32
|
+
...(sink.project !== undefined ? { project: sink.project } : {}),
|
|
33
|
+
...(sink.module !== undefined ? { module: sink.module } : {}),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
25
36
|
export function reportTranscript(entry) {
|
|
26
37
|
store.getStore()?.transcript(entry);
|
|
27
38
|
}
|
|
@@ -28,6 +28,13 @@ export declare function agentEnvironment(moduleDir: string, options?: {
|
|
|
28
28
|
hostEnv?: NodeJS.ProcessEnv;
|
|
29
29
|
home?: string;
|
|
30
30
|
}): AgentEnvironment;
|
|
31
|
+
/**
|
|
32
|
+
* Hold the package caches of a scratch area to `keepMb` megabytes: past it, the caches least recently written go whole -
|
|
33
|
+
* a package manager's folder in `cache`, or a tool's in an agent's home - until it is under. A cache is as recent as the
|
|
34
|
+
* newest thing written anywhere in it: a package manager writes deep inside its folder, never beside it. What goes is
|
|
35
|
+
* downloaded again when it is wanted. Never a reason for the work to fail.
|
|
36
|
+
*/
|
|
37
|
+
export declare function tidyCaches(scratch: string, keepMb: number): void;
|
|
31
38
|
/**
|
|
32
39
|
* On macOS, what a program asks the system for as the per-user temporary folder - not TMPDIR - is that folder with this
|
|
33
40
|
* suffix added: a folder of the module's own inside it. The JVM keeps its attach sockets there (`.java_pid<pid>`), which
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { chmodSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { basename, join, resolve } from "node:path";
|
|
5
|
-
import { readProjectConfig, stackOf } from "../config.js";
|
|
5
|
+
import { DEFAULT_CACHE_KEEP_MB, readProjectConfig, stackOf } from "../config.js";
|
|
6
6
|
import { registerSecrets } from "./masking.js";
|
|
7
7
|
import { projectRootOf, modulesScratchDir } from "./paths.js";
|
|
8
8
|
/**
|
|
@@ -12,9 +12,9 @@ import { projectRootOf, modulesScratchDir } from "./paths.js";
|
|
|
12
12
|
* agent, and an agent's command - or the code it runs - could read any of them. So only an allow-list passes: the path,
|
|
13
13
|
* the locale, the terminal, the user, and the variables that locate the host's toolchains. Then turri's own: a HOME of
|
|
14
14
|
* the module's own, kept between pulses, so a tool's settings and an agent's dotfiles never land in the person's; a
|
|
15
|
-
* TMPDIR; the package caches in the project, so a dependency downloads once
|
|
16
|
-
* project that is itself a repository is not found from the code. Last, what the project passes on
|
|
17
|
-
* `agent-env:` in its config - which never replaces turri's own.
|
|
15
|
+
* TMPDIR; the package caches in the project, so a dependency downloads once - held to `cache-keep:`; and git stopped
|
|
16
|
+
* at the module, so a project that is itself a repository is not found from the code. Last, what the project passes on
|
|
17
|
+
* purpose - `agent-env:` in its config - which never replaces turri's own.
|
|
18
18
|
*/
|
|
19
19
|
/** Passed from the host as they are. */
|
|
20
20
|
const ALLOWED = ["PATH", "LANG", "TERM", "TZ", "USER", "LOGNAME", "COLORTERM"];
|
|
@@ -62,11 +62,18 @@ export function cacheEnv(cache) {
|
|
|
62
62
|
GRADLE_USER_HOME: join(cache, "gradle"),
|
|
63
63
|
CARGO_HOME: join(cache, "cargo"),
|
|
64
64
|
npm_config_cache: join(cache, "npm"),
|
|
65
|
+
// node-gyp's headers, which it keeps in the home folder - a copy each module's - once for the project.
|
|
66
|
+
npm_config_devdir: join(cache, "node-gyp"),
|
|
65
67
|
YARN_CACHE_FOLDER: join(cache, "yarn"),
|
|
66
68
|
PIP_CACHE_DIR: join(cache, "pip"),
|
|
67
69
|
UV_CACHE_DIR: join(cache, "uv"),
|
|
68
70
|
GOCACHE: join(cache, "go-build"),
|
|
69
71
|
GOMODCACHE: join(cache, "go-mod"),
|
|
72
|
+
// The browsers the end-to-end test tools download - hundreds of megabytes each, kept in the home folder: once for
|
|
73
|
+
// the project, not once a module.
|
|
74
|
+
PLAYWRIGHT_BROWSERS_PATH: join(cache, "ms-playwright"),
|
|
75
|
+
PUPPETEER_CACHE_DIR: join(cache, "puppeteer"),
|
|
76
|
+
CYPRESS_CACHE_FOLDER: join(cache, "Cypress"),
|
|
70
77
|
};
|
|
71
78
|
}
|
|
72
79
|
/** The git identity an agent's commits in a scratch repository get - never the person's. */
|
|
@@ -101,9 +108,123 @@ export function agentEnvironment(moduleDir, options = {}) {
|
|
|
101
108
|
Object.assign(env, turrisOwn(module, name));
|
|
102
109
|
return { env, passed: Object.keys(values).sort(), values, missing };
|
|
103
110
|
}
|
|
111
|
+
/** The scratch areas whose caches this process has held to `cache-keep:`: once a run, before its first command. */
|
|
112
|
+
const tidied = new Set();
|
|
113
|
+
/** Where in an agent's home folder its tools keep caches of their own. */
|
|
114
|
+
const HOME_CACHES = [join("Library", "Caches"), ".cache"];
|
|
115
|
+
/**
|
|
116
|
+
* Hold the package caches of a scratch area to `keepMb` megabytes: past it, the caches least recently written go whole -
|
|
117
|
+
* a package manager's folder in `cache`, or a tool's in an agent's home - until it is under. A cache is as recent as the
|
|
118
|
+
* newest thing written anywhere in it: a package manager writes deep inside its folder, never beside it. What goes is
|
|
119
|
+
* downloaded again when it is wanted. Never a reason for the work to fail.
|
|
120
|
+
*/
|
|
121
|
+
export function tidyCaches(scratch, keepMb) {
|
|
122
|
+
const caches = [];
|
|
123
|
+
const folders = [join(scratch, "cache")];
|
|
124
|
+
for (const home of entries(join(scratch, "home")))
|
|
125
|
+
for (const inHome of HOME_CACHES)
|
|
126
|
+
folders.push(join(scratch, "home", home, inHome));
|
|
127
|
+
for (const folder of folders) {
|
|
128
|
+
for (const name of entries(folder)) {
|
|
129
|
+
const path = join(folder, name);
|
|
130
|
+
try {
|
|
131
|
+
caches.push({ path, ...measure(path) });
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
/* gone meanwhile */
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
let total = caches.reduce((sum, cache) => sum + cache.bytes, 0);
|
|
139
|
+
for (const cache of caches.sort((a, b) => a.written - b.written)) {
|
|
140
|
+
if (total <= keepMb * 1024 * 1024)
|
|
141
|
+
break;
|
|
142
|
+
total -= cache.bytes;
|
|
143
|
+
// What could not be removed still counts: a cache half gone is not the room it took.
|
|
144
|
+
if (existsSync(cache.path))
|
|
145
|
+
total += remove(cache.path);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function entries(dir) {
|
|
149
|
+
try {
|
|
150
|
+
return readdirSync(dir);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return [];
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/** A file's or folder's bytes and when anything in it was last written, its links not followed. */
|
|
157
|
+
function measure(path) {
|
|
158
|
+
const stat = lstatSync(path);
|
|
159
|
+
if (!stat.isDirectory())
|
|
160
|
+
return { bytes: stat.size, written: stat.mtimeMs };
|
|
161
|
+
let bytes = 0;
|
|
162
|
+
let written = stat.mtimeMs;
|
|
163
|
+
for (const name of entries(path)) {
|
|
164
|
+
try {
|
|
165
|
+
const inside = measure(join(path, name));
|
|
166
|
+
bytes += inside.bytes;
|
|
167
|
+
written = Math.max(written, inside.written);
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
/* gone meanwhile */
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return { bytes, written };
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Remove a cache whole, and say how many of its bytes are left. Some tools make what they cache read-only - Go's module
|
|
177
|
+
* cache, so it is never edited in place - and a folder that cannot be written cannot be emptied: each is made writable
|
|
178
|
+
* first, as `go clean -modcache` does.
|
|
179
|
+
*/
|
|
180
|
+
function remove(path) {
|
|
181
|
+
try {
|
|
182
|
+
rmSync(path, { recursive: true, force: true });
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
writable(path);
|
|
186
|
+
try {
|
|
187
|
+
rmSync(path, { recursive: true, force: true });
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
/* what cannot be removed stays */
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
return existsSync(path) ? measure(path).bytes : 0;
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return 0;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/** Every folder under `path` made writable by its owner, links not followed. */
|
|
201
|
+
function writable(path) {
|
|
202
|
+
try {
|
|
203
|
+
const stat = lstatSync(path);
|
|
204
|
+
if (!stat.isDirectory())
|
|
205
|
+
return;
|
|
206
|
+
chmodSync(path, stat.mode | 0o700);
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
for (const name of entries(path))
|
|
212
|
+
writable(join(path, name));
|
|
213
|
+
}
|
|
104
214
|
/** HOME, TMPDIR, the caches and git's ceiling: what no setting of the project's replaces. */
|
|
105
215
|
function turrisOwn(module, name) {
|
|
106
216
|
const scratch = modulesScratchDir(module);
|
|
217
|
+
if (!tidied.has(scratch)) {
|
|
218
|
+
tidied.add(scratch);
|
|
219
|
+
let keep = DEFAULT_CACHE_KEEP_MB;
|
|
220
|
+
try {
|
|
221
|
+
keep = readProjectConfig(projectRootOf(module)).cacheKeep ?? DEFAULT_CACHE_KEEP_MB;
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
/* a config that does not parse is said elsewhere: the default holds */
|
|
225
|
+
}
|
|
226
|
+
tidyCaches(scratch, keep);
|
|
227
|
+
}
|
|
107
228
|
const fakeHome = join(scratch, "home", name);
|
|
108
229
|
mkdirSync(fakeHome, { recursive: true });
|
|
109
230
|
const gitconfig = join(fakeHome, ".gitconfig");
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { dirname, join, resolve, sep } from "node:path";
|
|
2
2
|
import { readProjectConfig, stackOf } from "../config.js";
|
|
3
3
|
import { reportNote } from "./activity.js";
|
|
4
|
+
import { record } from "./ledger.js";
|
|
4
5
|
import { agentEnvironment, DARWIN_TEMP_SUFFIX, packageCacheDir, TOOLCHAIN_VARIABLES } from "./agent-env.js";
|
|
5
6
|
import { afterCommand, auditMessage, beforeCommand } from "./audit.js";
|
|
6
7
|
import { checkCommand, machineReadable } from "./command-check.js";
|
|
@@ -67,6 +68,9 @@ export function agentShell(role, options = {}) {
|
|
|
67
68
|
throw error;
|
|
68
69
|
}
|
|
69
70
|
const result = afterCommand(before);
|
|
71
|
+
if (result.putBack.length + result.left.length + result.lost.length + result.conversation.length > 0) {
|
|
72
|
+
record({ kind: "audit.putBack", putBack: result.putBack.length, left: result.left.length, lost: result.lost.length, conversation: result.conversation.length });
|
|
73
|
+
}
|
|
70
74
|
if (result.putBack.length > 0)
|
|
71
75
|
reportNote(` an agent's command changed ${result.putBack.join(", ")} - put back`);
|
|
72
76
|
if (result.left.length > 0)
|
|
@@ -90,6 +94,7 @@ export function agentShell(role, options = {}) {
|
|
|
90
94
|
async run(command, runOptions = {}) {
|
|
91
95
|
const checked = checkCommand(command, commandFence);
|
|
92
96
|
if (!checked.ok) {
|
|
97
|
+
record({ kind: "command.refused", rule: checked.rule });
|
|
93
98
|
reportNote(` refused an agent's command (${checked.rule}): ${command.length > 120 ? `${command.slice(0, 120)}...` : command}`);
|
|
94
99
|
const output = `Not run: ${checked.reason}`;
|
|
95
100
|
runOptions.onData?.(Buffer.from(output));
|
|
@@ -79,6 +79,11 @@ export declare function stepsOf(input: PlanInput, draft: DraftJob[]): Array<Draf
|
|
|
79
79
|
step: SpecStep;
|
|
80
80
|
}>;
|
|
81
81
|
export declare const MAX_SPECS_PER_JOB = 3;
|
|
82
|
+
/** The most a pulse's title is - in words, and in characters - however it was made: a host shows it as its heading. */
|
|
83
|
+
export declare const TITLE_WORDS = 10;
|
|
84
|
+
export declare const TITLE_CHARS = 70;
|
|
85
|
+
/** A title held to `TITLE_WORDS` and `TITLE_CHARS` - or less, for room beside it - cut at a word, with an ellipsis. */
|
|
86
|
+
export declare function shortTitle(text: string, chars?: number, words?: number): string;
|
|
82
87
|
export declare const MAX_LINES_PER_JOB = 120;
|
|
83
88
|
/**
|
|
84
89
|
* A functional spec to build, for the grouping: the number the agent answers with, its text, and whether it is built
|
|
@@ -147,12 +152,6 @@ export interface ChunkAgents {
|
|
|
147
152
|
export declare function groupProblems(specs: NewSpec[], answer: GroupAnswer, maxSpecs?: number): string[];
|
|
148
153
|
/** What is wrong with a placement: every new req once, each where there is such a job. */
|
|
149
154
|
export declare function placeProblems(input: PlaceInput, answer: PlaceAnswer): string[];
|
|
150
|
-
/**
|
|
151
|
-
* Cut the queue's work - from `input.base` to `input.current` - into jobs, as the three steps say. Returns the draft
|
|
152
|
-
* the work loop runs: checked (checkDraft), or, when nothing holds, the whole work as one job. `notes` is given what
|
|
153
|
-
* the plan did not do as asked - the person's wishes an agent did not follow and why, an answer that did not hold, a
|
|
154
|
-
* group split for its size - for the conversation to say, rather than guess.
|
|
155
|
-
*/
|
|
156
155
|
export declare function planChunks(input: PlanInput, agents?: ChunkAgents, notes?: string[]): Promise<DraftJob[]>;
|
|
157
156
|
export declare function groupPrompt(specs: NewSpec[], steering?: Steering[], maxSpecs?: number): string;
|
|
158
157
|
export declare function placePrompt(input: PlaceInput): string;
|
|
@@ -4,6 +4,7 @@ import { DEFAULT_CONCEPTS, extractConceptsFromDefinition, extractConceptsFromSpe
|
|
|
4
4
|
import { ACCEPTANCE_TESTS, DEFINITIONS, FUNCTIONAL_REQUIREMENTS, NON_FUNCTIONAL_REQUIREMENTS, REQUIRED_FUNCTIONAL_SPECS, TEST_REQUIREMENTS } from "../plain/spec.js";
|
|
5
5
|
import { ask, BEGIN, createExecutor } from "../pi.js";
|
|
6
6
|
import { reportActivity, reportStage } from "./activity.js";
|
|
7
|
+
import { firstLine, record } from "./ledger.js";
|
|
7
8
|
import { steeringBlock } from "./steer.js";
|
|
8
9
|
import { marksRegenerateAgain } from "./records.js";
|
|
9
10
|
import { validateConcepts } from "../plain/concepts.js";
|
|
@@ -70,6 +71,19 @@ export function stepsOf(input, draft) {
|
|
|
70
71
|
});
|
|
71
72
|
}
|
|
72
73
|
export const MAX_SPECS_PER_JOB = 3;
|
|
74
|
+
/** The most a pulse's title is - in words, and in characters - however it was made: a host shows it as its heading. */
|
|
75
|
+
export const TITLE_WORDS = 10;
|
|
76
|
+
export const TITLE_CHARS = 70;
|
|
77
|
+
/** A title held to `TITLE_WORDS` and `TITLE_CHARS` - or less, for room beside it - cut at a word, with an ellipsis. */
|
|
78
|
+
export function shortTitle(text, chars = TITLE_CHARS, words = TITLE_WORDS) {
|
|
79
|
+
const all = text.trim().split(/\s+/).filter((word) => word !== "");
|
|
80
|
+
let cut = all.slice(0, words).join(" ");
|
|
81
|
+
if (all.length <= words && cut.length <= chars)
|
|
82
|
+
return cut;
|
|
83
|
+
if (cut.length > chars - 1)
|
|
84
|
+
cut = cut.slice(0, chars - 1).replace(/\s+\S*$/, "") || cut.slice(0, chars - 1);
|
|
85
|
+
return `${cut.replace(/[\s,.;:-]+$/, "")}…`;
|
|
86
|
+
}
|
|
73
87
|
export const MAX_LINES_PER_JOB = 120;
|
|
74
88
|
/** How often an answer that does not hold goes back to its agent before the plan falls back. */
|
|
75
89
|
const RETRIES = 2;
|
|
@@ -123,6 +137,9 @@ export function groupProblems(specs, answer, maxSpecs = MAX_SPECS_PER_JOB) {
|
|
|
123
137
|
}
|
|
124
138
|
if (group.title.trim() === "")
|
|
125
139
|
problems.push("Every group needs a title.");
|
|
140
|
+
else if (group.title.trim().split(/\s+/).length > TITLE_WORDS || group.title.trim().length > TITLE_CHARS) {
|
|
141
|
+
problems.push(`"${group.title}" is too long for a title: at most ${TITLE_WORDS} words and ${TITLE_CHARS} characters.`);
|
|
142
|
+
}
|
|
126
143
|
}
|
|
127
144
|
return problems;
|
|
128
145
|
}
|
|
@@ -226,7 +243,8 @@ function draftOf(chunks, net) {
|
|
|
226
243
|
})
|
|
227
244
|
.filter((entry) => entry.changes.length > 0)
|
|
228
245
|
.map((entry) => ({
|
|
229
|
-
title
|
|
246
|
+
// However the title was made - an agent's, a spec's first line, several joined - it is held to the limit.
|
|
247
|
+
title: shortTitle(entry.chunk.title),
|
|
230
248
|
reason: entry.chunk.built !== undefined
|
|
231
249
|
? "what is built already, changed"
|
|
232
250
|
: entry.chunk.specs.length > 0 && entry.chunk.specs.every(regeneratesAgain)
|
|
@@ -237,7 +255,7 @@ function draftOf(chunks, net) {
|
|
|
237
255
|
}
|
|
238
256
|
/** A spec's first line, as a group's title when no agent gave one. */
|
|
239
257
|
function titleOf(change) {
|
|
240
|
-
return textOf(change).split("\n")[0].replace(/^-\s*/, "")
|
|
258
|
+
return shortTitle(textOf(change).split("\n")[0].replace(/^-\s*/, ""));
|
|
241
259
|
}
|
|
242
260
|
/**
|
|
243
261
|
* Cut the queue's work - from `input.base` to `input.current` - into jobs, as the three steps say. Returns the draft
|
|
@@ -245,6 +263,11 @@ function titleOf(change) {
|
|
|
245
263
|
* the plan did not do as asked - the person's wishes an agent did not follow and why, an answer that did not hold, a
|
|
246
264
|
* group split for its size - for the conversation to say, rather than guess.
|
|
247
265
|
*/
|
|
266
|
+
/** A planner agent that failed: the plan falls back, and the ledger says why. */
|
|
267
|
+
function plannerFailed(step, error) {
|
|
268
|
+
record({ kind: "planner.failed", step, error: firstLine(error instanceof Error ? error.message : String(error), 300) });
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
248
271
|
export async function planChunks(input, agents, notes) {
|
|
249
272
|
if (input.track !== "conformance")
|
|
250
273
|
return cut(input, agents, notes);
|
|
@@ -370,7 +393,7 @@ async function cut(input, agents, notes) {
|
|
|
370
393
|
...list.map((group) => ({ title: group.title, specs: group.specs.map((number) => bySpec.get(number)), definitions: [], reqs: [] })),
|
|
371
394
|
];
|
|
372
395
|
const conformance = input.track === "conformance";
|
|
373
|
-
const grouped = conformance ? undefined : ((await agents?.group(numbered, (answer) => groupProblems(numbered, answer, maxSpecs), input.steering, maxSpecs).catch(() =>
|
|
396
|
+
const grouped = conformance ? undefined : ((await agents?.group(numbered, (answer) => groupProblems(numbered, answer, maxSpecs), input.steering, maxSpecs).catch((error) => plannerFailed("group", error))) ?? undefined);
|
|
374
397
|
const groups = conformance
|
|
375
398
|
? conformanceGroups(numbered, input.separately === true, maxLines, (list) => {
|
|
376
399
|
const trial = chunksFor(list);
|
|
@@ -386,10 +409,14 @@ async function cut(input, agents, notes) {
|
|
|
386
409
|
if (grouped?.notes !== undefined && grouped.notes.trim() !== "")
|
|
387
410
|
say(`the grouping: ${grouped.notes.trim()}`);
|
|
388
411
|
if (!conformance && agents !== undefined) {
|
|
389
|
-
if (grouped === undefined)
|
|
412
|
+
if (grouped === undefined) {
|
|
413
|
+
record({ kind: "planner.fellBack", step: "group", why: "no grouping was given" });
|
|
390
414
|
say("the grouping was not given - a functional spec a pulse");
|
|
391
|
-
|
|
415
|
+
}
|
|
416
|
+
else if (groupProblems(numbered, grouped, maxSpecs).length > 0) {
|
|
417
|
+
record({ kind: "planner.fellBack", step: "group", why: firstLine(groupProblems(numbered, grouped, maxSpecs).join(" "), 300) });
|
|
392
418
|
say(`the grouping did not hold (${groupProblems(numbered, grouped, maxSpecs).join(" ")}) - a functional spec a pulse`);
|
|
419
|
+
}
|
|
393
420
|
}
|
|
394
421
|
let chunks = chunksFor(groups);
|
|
395
422
|
// 2. The definitions each group uses.
|
|
@@ -407,10 +434,12 @@ async function cut(input, agents, notes) {
|
|
|
407
434
|
implementationReqs: implementationReqs.map((change, index) => ({ number: index + 1, text: textOf(change) })),
|
|
408
435
|
testReqs: testReqs.map((change, index) => ({ number: index + 1, text: textOf(change) })),
|
|
409
436
|
};
|
|
410
|
-
const answer = (await agents?.place(placeInput, (given) => placeProblems(placeInput, given)).catch(() =>
|
|
437
|
+
const answer = (await agents?.place(placeInput, (given) => placeProblems(placeInput, given)).catch((error) => plannerFailed("place", error))) ?? undefined;
|
|
411
438
|
if (answer?.notes !== undefined && answer.notes.trim() !== "")
|
|
412
439
|
say(`the placement: ${answer.notes.trim()}`);
|
|
413
440
|
placement = answer !== undefined && placeProblems(placeInput, answer).length === 0 ? answer : undefined;
|
|
441
|
+
if (placement === undefined && agents !== undefined)
|
|
442
|
+
record({ kind: "planner.fellBack", step: "place", why: answer === undefined ? "no placement was given" : firstLine(placeProblems(placeInput, answer).join(" "), 300) });
|
|
414
443
|
if (placement === undefined && agents !== undefined)
|
|
415
444
|
say("the placement did not hold - every implementation req with the first pulse, every test req with the first conformance pulse");
|
|
416
445
|
}
|
|
@@ -458,7 +487,10 @@ async function cut(input, agents, notes) {
|
|
|
458
487
|
}
|
|
459
488
|
runs.push(run);
|
|
460
489
|
say(`"${group.title}" is past ${maxLines} lines with its definitions and reqs - split into ${runs.length} pulses`);
|
|
461
|
-
runs.forEach((specs, index) => split.push({
|
|
490
|
+
runs.forEach((specs, index) => split.push({
|
|
491
|
+
specs,
|
|
492
|
+
title: specs.length === 1 ? titleOf(bySpec.get(specs[0])) : `${shortTitle(group.title, TITLE_CHARS - 9, TITLE_WORDS - 3)} (${index + 1} of ${runs.length})`,
|
|
493
|
+
}));
|
|
462
494
|
});
|
|
463
495
|
const before = placement;
|
|
464
496
|
// A req placed with a group that split goes with the first functional spec of it.
|
|
@@ -508,7 +540,8 @@ export function groupPrompt(specs, steering, maxSpecs = MAX_SPECS_PER_JOB) {
|
|
|
508
540
|
"Never group specs only because they are of the same kind.",
|
|
509
541
|
"",
|
|
510
542
|
"Only functional specs next to each other in the same module go together, and they stay in the order given: the",
|
|
511
|
-
|
|
543
|
+
`order is what each is built on. Give each group a title that says what it builds - at most ${TITLE_WORDS} words and ${TITLE_CHARS}`,
|
|
544
|
+
"characters: it heads the group's pulse where the person watches the work, and a longer one is cut short there.",
|
|
512
545
|
"",
|
|
513
546
|
"The specification is split into modules - a ***plain matter, not how the code is laid out. Each numbers its",
|
|
514
547
|
"functional specs from 1: each below is given with its module and its id there, and a number of its own to answer with.",
|
|
@@ -588,9 +621,9 @@ export function llmChunkAgents(options) {
|
|
|
588
621
|
tool: (take) => defineTool({
|
|
589
622
|
name: "submit_groups",
|
|
590
623
|
label: "Submit the groups",
|
|
591
|
-
description:
|
|
624
|
+
description: `The new functional specs in groups, in order: each group's spec numbers and a title of at most ${TITLE_WORDS} words and ${TITLE_CHARS} characters.`,
|
|
592
625
|
parameters: Type.Object({
|
|
593
|
-
groups: Type.Array(Type.Object({ specs: Type.Array(Type.Number()), title: Type.String() })),
|
|
626
|
+
groups: Type.Array(Type.Object({ specs: Type.Array(Type.Number()), title: Type.String({ description: `What the group builds: at most ${TITLE_WORDS} words and ${TITLE_CHARS} characters.` }) })),
|
|
594
627
|
notes: Type.Optional(Type.String({ description: NOTES })),
|
|
595
628
|
}),
|
|
596
629
|
async execute(_id, params) {
|
|
@@ -316,9 +316,9 @@ export function createAskExplorerTool(options) {
|
|
|
316
316
|
return defineTool({
|
|
317
317
|
name: "ask_explorer",
|
|
318
318
|
label: "Ask the explorer",
|
|
319
|
-
description: "Ask the explorer - who read
|
|
319
|
+
description: "Ask the explorer - who read the code, and wrote the account of it you were given - something the account " +
|
|
320
320
|
"does not say: how a functionality is reached, what a field is called, what the software needs around it. It reads " +
|
|
321
|
-
`
|
|
321
|
+
`the code to answer - it runs nothing, so what only running the software shows is yours to try. ${options.questions ?? EXPLORER_QUESTIONS} questions a run: ask what reading the code yourself would take long to find.`,
|
|
322
322
|
promptSnippet: "ask_explorer: a question about how the software is reached, answered by the explorer from the code",
|
|
323
323
|
parameters: Type.Object({ question: Type.String({ description: "What you need to know, exactly" }) }),
|
|
324
324
|
async execute(_id, params) {
|
|
@@ -5,7 +5,8 @@ import { PLAIN_SOURCE_FILE_EXTENSION, plainFileParser } from "../plain/parser.js
|
|
|
5
5
|
import { ACCEPTANCE_TESTS, DEFINITIONS, FUNCTIONAL_REQUIREMENTS, NON_FUNCTIONAL_REQUIREMENTS, getFrids, getInheritedFunctionalSpecs, getSpecificationsForFrid, TEST_REQUIREMENTS, } from "../plain/spec.js";
|
|
6
6
|
import { reportActivity, reportLogNote, reportSpec, reportStage, unitOf } from "./activity.js";
|
|
7
7
|
import { confinedTools } from "./confine.js";
|
|
8
|
-
import { abandonWork, diffSinceSnapshot,
|
|
8
|
+
import { abandonWork, diffSinceSnapshot, landWork, moduleDirOf, startWork, workDiff, workSnapshot } from "./lineage.js";
|
|
9
|
+
import { firstLine, record } from "./ledger.js";
|
|
9
10
|
import { createWorkLedger, describeAccounts, SUBMIT_WORK_NOTE, unanswered, unansweredPrompt } from "./submit-work.js";
|
|
10
11
|
import { CONFORMANCE_COMPACTION } from "./compaction.js";
|
|
11
12
|
import { NOTES_TURN, notesForSession, passedOutcome, readNotes, reflectOnReview, withNotes } from "./notes.js";
|
|
@@ -417,6 +418,7 @@ export async function renderConformanceTests(options) {
|
|
|
417
418
|
// every functionality's tests - and the code's unit testing script.
|
|
418
419
|
stage(`running the conformance testing script on the whole suite (attempt ${rounds + 1} of ${maxRounds})`);
|
|
419
420
|
rounds += 1;
|
|
421
|
+
record({ kind: "gate.round", gate: "conformance", round: rounds, of: maxRounds });
|
|
420
422
|
let recheck = await runConformanceScript(buildDir, [conformanceDir], scripts);
|
|
421
423
|
runs.push({ command: recheck.command, exitCode: recheck.exitCode });
|
|
422
424
|
options.onRun?.(recheck.command, recheck.exitCode);
|
|
@@ -537,6 +539,7 @@ export async function renderConformanceTests(options) {
|
|
|
537
539
|
onDelta: options.onDelta,
|
|
538
540
|
});
|
|
539
541
|
const reason = verdict.reason.split("\n")[0].slice(0, 160);
|
|
542
|
+
record({ kind: "judge.verdict", culprit: verdict.culprit, round: rounds, reason: firstLine(verdict.reason, 300) });
|
|
540
543
|
// Stopped by the time limit while it was still getting on: no agent can give it more time. The pulse stops now -
|
|
541
544
|
// rolled back, no retry - and the person is told how to raise the limit.
|
|
542
545
|
if (verdict.culprit === "environment" && recheck.timedOut === true) {
|
|
@@ -797,9 +800,9 @@ export async function renderConformanceTests(options) {
|
|
|
797
800
|
// The tests, and the code when it changed: one commit if they share a module folder.
|
|
798
801
|
const subject = `conformance ${moduleName} ${options.diff !== undefined ? "for a spec change" : `fs ${frid}`}${changed ? " (code changed)" : ""}`;
|
|
799
802
|
stage(`committing the conformance tests${changed ? " and the code changes" : ""}`);
|
|
800
|
-
|
|
803
|
+
landWork(conformanceDir, subject);
|
|
801
804
|
if (moduleDirOf(buildDir) !== moduleDirOf(conformanceDir))
|
|
802
|
-
|
|
805
|
+
landWork(buildDir, subject);
|
|
803
806
|
}
|
|
804
807
|
else if (start !== undefined) {
|
|
805
808
|
const why = notLandedWhy(options.diff !== undefined ? "for the spec change" : `of functionality ${frid}`, unlabelled);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { AgentSessionEvent } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
/**
|
|
3
|
-
* Every render agent's whole conversation, streamed to a file of its own as it happens
|
|
3
|
+
* Every render agent's whole conversation, streamed to a file of its own as it happens: an agent of a pulse's for as
|
|
4
|
+
* long as the pulse runs - what the diagnosis of a failure reads - and kept after it in debug mode alone (`debug:`,
|
|
5
|
+
* pulse-files.ts); an agent outside a pulse only in debug mode.
|
|
4
6
|
*
|
|
5
7
|
* A job's transcript keeps a tool call's arguments to 600 characters and its result to 2000, and only while pi is
|
|
6
8
|
* running. When an agent goes wrong, what it was actually
|
|
@@ -2,9 +2,13 @@ import { appendFileSync, existsSync } from "node:fs";
|
|
|
2
2
|
import { maskSecrets } from "./masking.js";
|
|
3
3
|
import { dirname, join, resolve, sep } from "node:path";
|
|
4
4
|
import { currentJob, reportActivity, reportTranscript } from "./activity.js";
|
|
5
|
+
import { debugMode } from "../config.js";
|
|
6
|
+
import { pulseWrote } from "./pulse-files.js";
|
|
5
7
|
import { ensureInScratch, MODULES_DIR, projectRootOf, scratchDir } from "./paths.js";
|
|
6
8
|
/**
|
|
7
|
-
* Every render agent's whole conversation, streamed to a file of its own as it happens
|
|
9
|
+
* Every render agent's whole conversation, streamed to a file of its own as it happens: an agent of a pulse's for as
|
|
10
|
+
* long as the pulse runs - what the diagnosis of a failure reads - and kept after it in debug mode alone (`debug:`,
|
|
11
|
+
* pulse-files.ts); an agent outside a pulse only in debug mode.
|
|
8
12
|
*
|
|
9
13
|
* A job's transcript keeps a tool call's arguments to 600 characters and its result to 2000, and only while pi is
|
|
10
14
|
* running. When an agent goes wrong, what it was actually
|
|
@@ -47,8 +51,11 @@ export function debugLogOf(session) {
|
|
|
47
51
|
*/
|
|
48
52
|
export function startDebugLog(session, options) {
|
|
49
53
|
let file;
|
|
54
|
+
const dir = debugDirFor(options.cwd);
|
|
55
|
+
// Outside a pulse nothing reads it - no diagnosis, no person but one debugging turri.
|
|
56
|
+
if (currentJob() === undefined && !debugMode(dirname(dirname(dir))))
|
|
57
|
+
return undefined;
|
|
50
58
|
try {
|
|
51
|
-
const dir = debugDirFor(options.cwd);
|
|
52
59
|
ensureInScratch(dirname(dir), dir);
|
|
53
60
|
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
54
61
|
const name = options.label.replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "agent";
|
|
@@ -56,6 +63,7 @@ export function startDebugLog(session, options) {
|
|
|
56
63
|
for (let n = 2; existsSync(file); n += 1)
|
|
57
64
|
file = join(dir, `${stamp}-${name}-${n}.log`);
|
|
58
65
|
appendFileSync(file, maskSecrets(header(session, options)), { flag: "wx" });
|
|
66
|
+
pulseWrote(file);
|
|
59
67
|
}
|
|
60
68
|
catch (error) {
|
|
61
69
|
reportActivity(`debug log not started: ${error instanceof Error ? error.message : String(error)}`);
|