@turri-dev/cli 0.1.16 → 0.1.17-next.63f454c
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/ask.d.ts +2 -0
- package/dist/ask.js +2 -0
- package/dist/chat-commands.d.ts +25 -0
- package/dist/chat-commands.js +162 -0
- package/dist/chat-records.d.ts +44 -0
- package/dist/chat-records.js +102 -0
- package/dist/chat-scope.d.ts +14 -0
- package/dist/chat-scope.js +15 -0
- package/dist/chat-terminal.d.ts +23 -0
- package/dist/chat-terminal.js +163 -0
- package/dist/chat-ui.d.ts +33 -0
- package/dist/chat-ui.js +192 -0
- package/dist/chats.d.ts +192 -0
- package/dist/chats.js +378 -0
- package/dist/cli.js +5 -7
- package/dist/config.d.ts +8 -2
- package/dist/config.js +18 -4
- package/dist/extension.d.ts +20 -33
- package/dist/extension.js +427 -272
- package/dist/genome.d.ts +13 -7
- package/dist/genome.js +13 -3
- package/dist/host-session.d.ts +58 -0
- package/dist/host-session.js +72 -0
- package/dist/host.d.ts +5 -4
- package/dist/host.js +6 -5
- package/dist/identity.js +5 -0
- package/dist/pi-launch.js +23 -1
- package/dist/question-dialog.d.ts +4 -0
- package/dist/question-dialog.js +13 -2
- package/dist/regenerate/activity.d.ts +8 -1
- package/dist/regenerate/activity.js +23 -2
- package/dist/regenerate/jobs.d.ts +2 -0
- package/dist/regenerate/jobs.js +3 -0
- package/dist/regenerate/ledger.d.ts +7 -0
- package/dist/regenerate/ledger.js +8 -1
- package/dist/regenerate/test-scripts.d.ts +1 -1
- package/dist/stacks.d.ts +2 -1
- package/dist/stacks.js +4 -3
- package/docs/README.md +4 -2
- package/docs/chats.md +75 -0
- package/docs/config.md +3 -3
- package/docs/development.md +4 -1
- package/docs/hosting.md +37 -28
- package/docs/skills-and-gates.md +2 -2
- package/docs/using.md +2 -2
- package/package.json +1 -1
package/dist/genome.d.ts
CHANGED
|
@@ -17,12 +17,7 @@ export interface RunSkillResult {
|
|
|
17
17
|
/** The last step's artifact - what the caller usually wants. */
|
|
18
18
|
output: string;
|
|
19
19
|
}
|
|
20
|
-
|
|
21
|
-
* Run one skill to completion. The only entry point: the CLI and the Pi extension
|
|
22
|
-
* both go through here, so a skill behaves identically whether a human launched it
|
|
23
|
-
* or a conversational agent decided to.
|
|
24
|
-
*/
|
|
25
|
-
export declare function runSkill(opts: {
|
|
20
|
+
export interface RunSkillOptions {
|
|
26
21
|
skillDir: string;
|
|
27
22
|
cwd: string;
|
|
28
23
|
runDir: string;
|
|
@@ -51,7 +46,18 @@ export declare function runSkill(opts: {
|
|
|
51
46
|
loopTools?: ToolDefinition[];
|
|
52
47
|
/** The jobs the render queue runs, for a skill that waits for its builds and reads what they recorded. */
|
|
53
48
|
jobs?: JobRegistry;
|
|
54
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Stops the run: its agents end their turns and it throws JobStoppedError, its manifest left as it was - an
|
|
51
|
+
* interrupted run, for `resume` to pick up. What it started on the work loop goes on.
|
|
52
|
+
*/
|
|
53
|
+
signal?: AbortSignal;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Run one skill to completion. The only entry point: the CLI and the Pi extension
|
|
57
|
+
* both go through here, so a skill behaves identically whether a human launched it
|
|
58
|
+
* or a conversational agent decided to.
|
|
59
|
+
*/
|
|
60
|
+
export declare function runSkill(opts: RunSkillOptions): Promise<RunSkillResult>;
|
|
55
61
|
/** Whether there is a run of this skill here that was interrupted, and could be resumed. */
|
|
56
62
|
export declare function interruptedRun(runDir: string, skillName: string): {
|
|
57
63
|
step?: string;
|
package/dist/genome.js
CHANGED
|
@@ -4,11 +4,11 @@ import { Command, MemorySaver } from "@langchain/langgraph";
|
|
|
4
4
|
import { compileSkill } from "./graph.js";
|
|
5
5
|
import { createExecutor } from "./pi.js";
|
|
6
6
|
import { toolchainsBlock } from "./stack.js";
|
|
7
|
-
import { describeToolCall, describeToolResult } from "./regenerate/activity.js";
|
|
7
|
+
import { clearOfStop, describeToolCall, describeToolResult, withStopSignal } from "./regenerate/activity.js";
|
|
8
8
|
import { createLiveReviewer } from "./review/live.js";
|
|
9
9
|
import { loadSkill, readRubric } from "./skill.js";
|
|
10
10
|
import { createAskUserTool, createEndQuestionsTool, createReviewTools, createWriteArtifactTool, withReviewNote } from "./tools.js";
|
|
11
|
-
import { DISMISSED } from "./ask.js";
|
|
11
|
+
import { DECLINED, DISMISSED } from "./ask.js";
|
|
12
12
|
import { createQuestioning, levelOf, questioningQuestion } from "./questioning.js";
|
|
13
13
|
import { createSpecDiffTools } from "./spec-tools.js";
|
|
14
14
|
import { createMatureTools, MATURE_TOOLS } from "./mature-tools.js";
|
|
@@ -23,6 +23,16 @@ const SAID_MAX = 6000;
|
|
|
23
23
|
* or a conversational agent decided to.
|
|
24
24
|
*/
|
|
25
25
|
export async function runSkill(opts) {
|
|
26
|
+
if (opts.signal === undefined)
|
|
27
|
+
return runStoppable(opts);
|
|
28
|
+
return withStopSignal(opts.signal, () => runStoppable({
|
|
29
|
+
...opts,
|
|
30
|
+
...(opts.renderQueue !== undefined ? { renderQueue: clearOfStop(opts.renderQueue) } : {}),
|
|
31
|
+
...(opts.jobs !== undefined ? { jobs: clearOfStop(opts.jobs) } : {}),
|
|
32
|
+
...(opts.loopTools !== undefined ? { loopTools: opts.loopTools.map(clearOfStop) } : {}),
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
async function runStoppable(opts) {
|
|
26
36
|
const skill = loadSkill(opts.skillDir);
|
|
27
37
|
const runDir = resolve(opts.runDir);
|
|
28
38
|
// A resumed run keeps its folder - the artifacts are in it - and starts where it stopped.
|
|
@@ -102,7 +112,7 @@ export async function runSkill(opts) {
|
|
|
102
112
|
customTools.push(createAskUserTool(opts.askUser, (answer, confirmsPhase) => {
|
|
103
113
|
said = "";
|
|
104
114
|
// A phase's confirmation, or a question dismissed, owes the spec nothing.
|
|
105
|
-
diffs?.pace.answered(!confirmsPhase && answer !== DISMISSED);
|
|
115
|
+
diffs?.pace.answered(!confirmsPhase && answer !== DISMISSED && answer !== DECLINED);
|
|
106
116
|
}, () => said, holdsQuestions ? questions : undefined));
|
|
107
117
|
if (capsQuestions)
|
|
108
118
|
customTools.push(createEndQuestionsTool(questions));
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { type AgentSession, type ExtensionFactory, type ExtensionUIContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { AskUserIO } from "./ask.ts";
|
|
3
|
+
import { type ChatInfo, type TurriHost } from "./chats.ts";
|
|
4
|
+
import type { JobRegistry } from "./regenerate/jobs.ts";
|
|
5
|
+
import type { WorkLoop } from "./regenerate/loop.ts";
|
|
6
|
+
/**
|
|
7
|
+
* turri's session for a host that runs its chats itself - a desktop app, an editor: the chats, each a pi session with
|
|
8
|
+
* turri in it, on one job registry and work loop. The host shows each chat in its own way - subscribes to its session,
|
|
9
|
+
* prompts it - and tells turri which one its person looks at (`focus`): a pulse a closed chat asked for is reported
|
|
10
|
+
* there.
|
|
11
|
+
*/
|
|
12
|
+
export interface TurriOptions extends Omit<TurriHost, "dialogs"> {
|
|
13
|
+
/** pi's folder - its settings, models and credentials. Default: pi's own (`~/.pi/agent`). */
|
|
14
|
+
agentDir?: string;
|
|
15
|
+
/** Where the chats' conversations are saved. Default: pi's own folder for this project. */
|
|
16
|
+
sessionDir?: string;
|
|
17
|
+
/** The host's own extensions, built into every chat after turri's. */
|
|
18
|
+
extensions?: ExtensionFactory[];
|
|
19
|
+
/** The dialogs a chat's questions are put in - ask_user's, a skill's interview, a review gate's - by the chat's id. */
|
|
20
|
+
dialogs?: (chat: string) => AskUserIO | undefined;
|
|
21
|
+
/** What a chat's other UI calls - its notices, its working line - come to, by the chat's id. */
|
|
22
|
+
uiContext?: (chat: string) => ExtensionUIContext | undefined;
|
|
23
|
+
/** Reopen the chats of the last session here that has ended, in their order - as `turri --continue` does. */
|
|
24
|
+
restore?: boolean;
|
|
25
|
+
}
|
|
26
|
+
export interface Turri {
|
|
27
|
+
/** The session's job registry: every pulse any chat starts. */
|
|
28
|
+
readonly jobs: JobRegistry;
|
|
29
|
+
/** The session's work loop - once a chat is open: what each codebase has planned, and why. */
|
|
30
|
+
readonly loop: WorkLoop;
|
|
31
|
+
/** The chats, in their order. */
|
|
32
|
+
chats(): ChatInfo[];
|
|
33
|
+
/** A chat's pi session: to subscribe to, prompt and abort. */
|
|
34
|
+
session(id: string): AgentSession | undefined;
|
|
35
|
+
/** A fresh chat - or the saved one `sessionFile` is. At most nine are open. */
|
|
36
|
+
openChat(options?: {
|
|
37
|
+
sessionFile?: string;
|
|
38
|
+
}): Promise<ChatInfo & {
|
|
39
|
+
session: AgentSession;
|
|
40
|
+
}>;
|
|
41
|
+
/** A chat closed: its turn stopped, its conversation deleted - its pulses run on. */
|
|
42
|
+
closeChat(id: string): Promise<void>;
|
|
43
|
+
/** The chat the person looks at now. */
|
|
44
|
+
focus(id: string): void;
|
|
45
|
+
/**
|
|
46
|
+
* ctrl+c in a chat, as the terminal has it: what the chat is doing stopped - its turn, a skill or a tool in it, a `!`
|
|
47
|
+
* command, a compaction - and nothing in the others (`stopped`). In a chat doing nothing the session ends - every
|
|
48
|
+
* chat let go and what activate started stopped, as `dispose` does - for the host to close what showed it (`ended`).
|
|
49
|
+
* A second interrupt straight after one that stopped something does nothing (`ignored`); `missing` for a chat not open.
|
|
50
|
+
*/
|
|
51
|
+
interrupt(id: string): Promise<"stopped" | "ignored" | "ended" | "missing">;
|
|
52
|
+
/** Told when a chat is opened, closed, renamed, or starts or stops working. */
|
|
53
|
+
onChats(listener: (chats: ChatInfo[]) => void): () => void;
|
|
54
|
+
/** Every chat let go, and what activate started stopped. */
|
|
55
|
+
dispose(): Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
/** turri's session in a host, in the process's working folder - the project's. */
|
|
58
|
+
export declare function createTurri(options?: TurriOptions): Promise<Turri>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { createAgentSession, DefaultResourceLoader, getAgentDir, SessionManager } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { lastRun } from "./chat-records.js";
|
|
3
|
+
import { interruptChat } from "./chats.js";
|
|
4
|
+
import { createTurriCore, sessionLoop } from "./extension.js";
|
|
5
|
+
import { stopDeployed } from "./regenerate/release.js";
|
|
6
|
+
/** turri's session in a host, in the process's working folder - the project's. */
|
|
7
|
+
export async function createTurri(options = {}) {
|
|
8
|
+
const { dialogs, uiContext, extensions = [], agentDir = getAgentDir(), sessionDir, restore, ...host } = options;
|
|
9
|
+
const core = createTurriCore(host, { persist: true, ...(dialogs !== undefined ? { dialogs } : {}) });
|
|
10
|
+
const cwd = core.cwd;
|
|
11
|
+
core.setDriver({
|
|
12
|
+
async create(chat, sessionFile) {
|
|
13
|
+
const resourceLoader = new DefaultResourceLoader({ cwd, agentDir, extensionFactories: [core.extension, ...extensions] });
|
|
14
|
+
await resourceLoader.reload();
|
|
15
|
+
const sessionManager = sessionFile !== undefined ? SessionManager.open(sessionFile, sessionDir) : SessionManager.create(cwd, sessionDir);
|
|
16
|
+
const { session } = await createAgentSession({ cwd, agentDir, resourceLoader, sessionManager });
|
|
17
|
+
chat.session = session;
|
|
18
|
+
await session.bindExtensions({ uiContext: uiContext?.(chat.id), mode: "tui" });
|
|
19
|
+
},
|
|
20
|
+
async dispose(chat) {
|
|
21
|
+
const session = chat.session;
|
|
22
|
+
if (session === undefined)
|
|
23
|
+
return;
|
|
24
|
+
try {
|
|
25
|
+
await session.abort();
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
session.dispose();
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
if (restore === true) {
|
|
33
|
+
const saved = lastRun(cwd);
|
|
34
|
+
if (saved !== undefined)
|
|
35
|
+
await core.restore(saved);
|
|
36
|
+
const first = core.chats()[0];
|
|
37
|
+
if (first !== undefined)
|
|
38
|
+
await core.show(first.id);
|
|
39
|
+
}
|
|
40
|
+
let lastStop = 0;
|
|
41
|
+
const dispose = async () => {
|
|
42
|
+
await core.dispose();
|
|
43
|
+
await stopDeployed();
|
|
44
|
+
};
|
|
45
|
+
return {
|
|
46
|
+
jobs: core.jobs,
|
|
47
|
+
get loop() {
|
|
48
|
+
return sessionLoop(core);
|
|
49
|
+
},
|
|
50
|
+
chats: () => core.chats(),
|
|
51
|
+
session: (id) => core.session(id),
|
|
52
|
+
async openChat(asked = {}) {
|
|
53
|
+
const chat = await core.open(asked);
|
|
54
|
+
return { ...chat, session: core.session(chat.id) };
|
|
55
|
+
},
|
|
56
|
+
closeChat: (id) => core.close(id),
|
|
57
|
+
focus: (id) => void core.show(id),
|
|
58
|
+
async interrupt(id) {
|
|
59
|
+
const session = core.session(id);
|
|
60
|
+
if (session === undefined)
|
|
61
|
+
return "missing";
|
|
62
|
+
const did = interruptChat(session, { lastStop, idle: () => "ended" });
|
|
63
|
+
if (did === "stopped")
|
|
64
|
+
lastStop = Date.now();
|
|
65
|
+
if (did === "ended")
|
|
66
|
+
await dispose();
|
|
67
|
+
return did;
|
|
68
|
+
},
|
|
69
|
+
onChats: (listener) => core.onChats(() => listener(core.chats())),
|
|
70
|
+
dispose,
|
|
71
|
+
};
|
|
72
|
+
}
|
package/dist/host.d.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* turri for a host that runs it itself - a desktop app, an editor - imported as `@turri-dev/cli/host`: the
|
|
3
|
-
*
|
|
2
|
+
* turri for a host that runs it itself - a desktop app, an editor - imported as `@turri-dev/cli/host`: the session and
|
|
3
|
+
* its chats, with the host told of its jobs and asking its questions (createTurri), and
|
|
4
4
|
* what a host shows beside it - a spec parsed, checked and coloured, its concepts and links, the standard library, how
|
|
5
5
|
* far each functional spec is built, the work a spec change would plan. It is turri's own code, as the terminal runs it; a 0.x release may change any of it.
|
|
6
6
|
*/
|
|
7
|
-
export {
|
|
7
|
+
export { createTurri, type Turri, type TurriOptions } from "./host-session.ts";
|
|
8
|
+
export { MAX_CHATS, type ChatInfo, type ChatStatus, type TurriHost } from "./chats.ts";
|
|
8
9
|
export type { AskUserIO } from "./ask.ts";
|
|
9
|
-
/** pi itself, as turri
|
|
10
|
+
/** pi itself, as turri is built against it: a host's own extensions and UI are made with this one, not another copy. */
|
|
10
11
|
export * as pi from "@earendil-works/pi-coding-agent";
|
|
11
12
|
export { configForSpec, debugMode, hasProjectConfig, modelSpec, projectModels, readProjectConfig, stacksOf, type ProjectConfig } from "./config.ts";
|
|
12
13
|
export { MODULES_DIR, projectDirOf, resolveTemplateDirs } from "./regenerate/paths.ts";
|
package/dist/host.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* turri for a host that runs it itself - a desktop app, an editor - imported as `@turri-dev/cli/host`: the
|
|
3
|
-
*
|
|
2
|
+
* turri for a host that runs it itself - a desktop app, an editor - imported as `@turri-dev/cli/host`: the session and
|
|
3
|
+
* its chats, with the host told of its jobs and asking its questions (createTurri), and
|
|
4
4
|
* what a host shows beside it - a spec parsed, checked and coloured, its concepts and links, the standard library, how
|
|
5
5
|
* far each functional spec is built, the work a spec change would plan. It is turri's own code, as the terminal runs it; a 0.x release may change any of it.
|
|
6
6
|
*/
|
|
7
|
-
// The
|
|
8
|
-
export {
|
|
9
|
-
|
|
7
|
+
// The session and its chats.
|
|
8
|
+
export { createTurri } from "./host-session.js";
|
|
9
|
+
export { MAX_CHATS } from "./chats.js";
|
|
10
|
+
/** pi itself, as turri is built against it: a host's own extensions and UI are made with this one, not another copy. */
|
|
10
11
|
export * as pi from "@earendil-works/pi-coding-agent";
|
|
11
12
|
// The project and its settings.
|
|
12
13
|
export { configForSpec, debugMode, hasProjectConfig, modelSpec, projectModels, readProjectConfig, stacksOf } from "./config.js";
|
package/dist/identity.js
CHANGED
|
@@ -48,6 +48,11 @@ const IDENTITY_BEFORE = [
|
|
|
48
48
|
"turri in the third person, as if it were someone else, and never tell the person to tell or ask turri something:",
|
|
49
49
|
"they are talking to turri. What they should ask for, they ask you for, here.",
|
|
50
50
|
"",
|
|
51
|
+
"The person may have several chats with you open on this project at once, this one among them. What regenerates is",
|
|
52
|
+
"the same in every chat: the pulses and their plan, steering, the engine's coding model and what activate started -",
|
|
53
|
+
"pulses lists every pulse, whichever chat asked for it, and you are told when one asked for here ends. What was said",
|
|
54
|
+
"in another chat is not in this one: when the person speaks of it, ask.",
|
|
55
|
+
"",
|
|
51
56
|
"Your words, and the older ones for the same things - a project's AGENTS.md, a skill, notes or the person may use",
|
|
52
57
|
"either, and they mean the same:",
|
|
53
58
|
"- A pulse is one unit of work regenerating the code - a spec diff regenerated in a codebase, or its conformance",
|
package/dist/pi-launch.js
CHANGED
|
@@ -12,6 +12,9 @@ import { main, SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
|
12
12
|
import { ProcessTerminal } from "@earendil-works/pi-tui";
|
|
13
13
|
import "./terminal-images.js";
|
|
14
14
|
import { resumeLine } from "./resume-line.js";
|
|
15
|
+
import { lastRun } from "./chat-records.js";
|
|
16
|
+
import { driveTerminal } from "./chat-terminal.js";
|
|
17
|
+
import turri, { createTurriCore, processSession } from "./extension.js";
|
|
15
18
|
/**
|
|
16
19
|
* pi, started the way its own CLI starts it, for `turri`.
|
|
17
20
|
*
|
|
@@ -21,6 +24,8 @@ import { resumeLine } from "./resume-line.js";
|
|
|
21
24
|
* quiet every `pi` on the machine, so it is answered for this run instead. `--verbose` still shows it.
|
|
22
25
|
*/
|
|
23
26
|
const args = process.argv.slice(2);
|
|
27
|
+
/** Set by cli.ts when pi loads turri's extension itself - an older turri installed it into every pi. */
|
|
28
|
+
const GLOBAL_EXTENSION = "TURRI_EXTENSION_IN_PI";
|
|
24
29
|
if (!args.includes("--verbose")) {
|
|
25
30
|
SettingsManager.prototype.getQuietStartup = () => true;
|
|
26
31
|
}
|
|
@@ -42,4 +47,21 @@ process.stdout.write = ((chunk, ...rest) => write(typeof chunk === "string" ? re
|
|
|
42
47
|
const setup = pathToFileURL(join(dirname(fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent"))), "cli", "setup.js")).href;
|
|
43
48
|
const { setupCli } = (await import(__rewriteRelativeImportExtension(setup)));
|
|
44
49
|
setupCli();
|
|
45
|
-
|
|
50
|
+
// turri's session, with its chats: built into pi here, in this process, rather than loaded from a file - one session
|
|
51
|
+
// the terminal's chats share (chats.ts). An older turri installed into every pi loads it itself: cli.ts says so, and
|
|
52
|
+
// it is not built in twice.
|
|
53
|
+
const extensionFactories = process.env[GLOBAL_EXTENSION] === "1" ? [] : [turri];
|
|
54
|
+
// pi's terminal - not its print, json or rpc modes, nor a run given its input on stdin - holds several chats, and
|
|
55
|
+
// records them for `turri --continue`, which opens them all again: the first as pi's own session, the rest beside it.
|
|
56
|
+
const interactive = process.stdin.isTTY === true && !args.some((arg) => ["-p", "--print", "--mode", "--help", "-h", "--version", "-v", "--list-models"].includes(arg) || arg.startsWith("--mode="));
|
|
57
|
+
if (interactive) {
|
|
58
|
+
let restore;
|
|
59
|
+
const asked = args.findIndex((arg) => arg === "--continue" || arg === "-c");
|
|
60
|
+
const saved = asked !== -1 ? lastRun(process.cwd()) : undefined;
|
|
61
|
+
if (saved !== undefined) {
|
|
62
|
+
args.splice(asked, 1, "--session", saved.chats[0].sessionFile);
|
|
63
|
+
restore = saved;
|
|
64
|
+
}
|
|
65
|
+
driveTerminal(processSession(() => createTurriCore({}, { persist: true })), restore !== undefined ? { restore } : {});
|
|
66
|
+
}
|
|
67
|
+
await main(args, { extensionFactories });
|
|
@@ -25,6 +25,8 @@ export interface QuestionDialogOptions {
|
|
|
25
25
|
style: DialogStyle;
|
|
26
26
|
/** The answer - an option, or what was typed - or undefined, dismissed. */
|
|
27
27
|
done: (answer: string | undefined) => void;
|
|
28
|
+
/** ctrl+c: the question declined, the asker deciding what that stops (askUserVia). Without it, dismissed as esc is. */
|
|
29
|
+
interrupt?: () => void;
|
|
28
30
|
/** Draw again: a key changed what shows. */
|
|
29
31
|
requestRender: () => void;
|
|
30
32
|
}
|
|
@@ -42,6 +44,8 @@ export declare class QuestionDialog implements Component, Focusable {
|
|
|
42
44
|
constructor(options: QuestionDialogOptions);
|
|
43
45
|
get focused(): boolean;
|
|
44
46
|
set focused(value: boolean);
|
|
47
|
+
/** Closed, no answer given: the question declined from outside - its turn stopped. */
|
|
48
|
+
dismiss(): void;
|
|
45
49
|
private finish;
|
|
46
50
|
private scrollBy;
|
|
47
51
|
render(width: number): string[];
|
package/dist/question-dialog.js
CHANGED
|
@@ -36,6 +36,10 @@ export class QuestionDialog {
|
|
|
36
36
|
if (this.input !== undefined)
|
|
37
37
|
this.input.focused = value;
|
|
38
38
|
}
|
|
39
|
+
/** Closed, no answer given: the question declined from outside - its turn stopped. */
|
|
40
|
+
dismiss() {
|
|
41
|
+
this.finish(undefined);
|
|
42
|
+
}
|
|
39
43
|
finish(answer) {
|
|
40
44
|
if (this.answered)
|
|
41
45
|
return;
|
|
@@ -65,8 +69,8 @@ export class QuestionDialog {
|
|
|
65
69
|
return index === this.selected ? style.accent(shown) : shown;
|
|
66
70
|
}));
|
|
67
71
|
const hint = this.input !== undefined
|
|
68
|
-
? "enter answers · esc dismisses"
|
|
69
|
-
: "↑↓ choose · enter picks · esc dismisses";
|
|
72
|
+
? "enter answers · esc dismisses · ctrl+c declines"
|
|
73
|
+
: "↑↓ choose · enter picks · esc dismisses · ctrl+c declines";
|
|
70
74
|
// The brief gets what the question, the options and the hints leave of the screen.
|
|
71
75
|
const fixed = question.length + body.length + 5;
|
|
72
76
|
const room = Math.max(3, this.options.rows() - KEPT_ABOVE - fixed);
|
|
@@ -89,6 +93,13 @@ export class QuestionDialog {
|
|
|
89
93
|
return lines;
|
|
90
94
|
}
|
|
91
95
|
handleInput(data) {
|
|
96
|
+
if (matchesKey(data, "ctrl+c")) {
|
|
97
|
+
if (this.options.interrupt !== undefined)
|
|
98
|
+
this.options.interrupt();
|
|
99
|
+
else
|
|
100
|
+
this.finish(undefined);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
92
103
|
// The brief scrolls whatever has the keys.
|
|
93
104
|
if (matchesKey(data, "pageDown"))
|
|
94
105
|
return this.scrollBy(Math.max(1, this.window - 1));
|
|
@@ -84,8 +84,15 @@ export declare function reportSpecConflict(conflict: SpecConflict): void;
|
|
|
84
84
|
export declare class JobStoppedError extends Error {
|
|
85
85
|
constructor();
|
|
86
86
|
}
|
|
87
|
-
/** The stop signal of the job this code is running for
|
|
87
|
+
/** The stop signal of the job this code is running for - else of the work outside a job it runs for - if there is one. */
|
|
88
88
|
export declare function currentStopSignal(): AbortSignal | undefined;
|
|
89
|
+
/** Run work for no job that `signal` stops: every agent under it ends its turn, and the work unwinds with JobStoppedError. */
|
|
90
|
+
export declare function withStopSignal<T>(signal: AbortSignal | undefined, run: () => Promise<T>): Promise<T>;
|
|
91
|
+
/**
|
|
92
|
+
* What work under withStopSignal hands on to the session - the work loop, the job registry - with each of its methods
|
|
93
|
+
* run clear of that signal: a pulse or a plan started from a skill outlives the skill being stopped.
|
|
94
|
+
*/
|
|
95
|
+
export declare function clearOfStop<T extends object>(given: T): T;
|
|
89
96
|
/** The job this code is running for, if it is running for one. */
|
|
90
97
|
export declare function currentJob(): {
|
|
91
98
|
id: string;
|
|
@@ -14,9 +14,30 @@ export class JobStoppedError extends Error {
|
|
|
14
14
|
this.name = "JobStoppedError";
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
|
-
/**
|
|
17
|
+
/** What stops work that runs for no job - a skill run, stopped with the chat's turn that runs it (withStopSignal). */
|
|
18
|
+
const stops = new AsyncLocalStorage();
|
|
19
|
+
/** The stop signal of the job this code is running for - else of the work outside a job it runs for - if there is one. */
|
|
18
20
|
export function currentStopSignal() {
|
|
19
|
-
|
|
21
|
+
const sink = store.getStore();
|
|
22
|
+
return sink !== undefined ? sink.signal : stops.getStore();
|
|
23
|
+
}
|
|
24
|
+
/** Run work for no job that `signal` stops: every agent under it ends its turn, and the work unwinds with JobStoppedError. */
|
|
25
|
+
export function withStopSignal(signal, run) {
|
|
26
|
+
return stops.run(signal, run);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* What work under withStopSignal hands on to the session - the work loop, the job registry - with each of its methods
|
|
30
|
+
* run clear of that signal: a pulse or a plan started from a skill outlives the skill being stopped.
|
|
31
|
+
*/
|
|
32
|
+
export function clearOfStop(given) {
|
|
33
|
+
return new Proxy(given, {
|
|
34
|
+
get(target, key, receiver) {
|
|
35
|
+
const value = Reflect.get(target, key, receiver);
|
|
36
|
+
if (typeof value !== "function")
|
|
37
|
+
return value;
|
|
38
|
+
return (...args) => stops.run(undefined, () => value.apply(target, args));
|
|
39
|
+
},
|
|
40
|
+
});
|
|
20
41
|
}
|
|
21
42
|
/** The job this code is running for, if it is running for one. */
|
|
22
43
|
export function currentJob() {
|
|
@@ -31,6 +31,8 @@ export interface Job {
|
|
|
31
31
|
model?: string;
|
|
32
32
|
/** The stack it is built with - its config.turri.<stack>.yaml over config.turri.yaml - as chosen when it was queued. */
|
|
33
33
|
stack?: string;
|
|
34
|
+
/** The chat it was asked for in (chat-scope.ts): the one told how it went. */
|
|
35
|
+
chat?: string;
|
|
34
36
|
status: JobStatus;
|
|
35
37
|
/** Monotonic start order. Two jobs can share a millisecond, so ordering uses this. */
|
|
36
38
|
sequence: number;
|
package/dist/regenerate/jobs.js
CHANGED
|
@@ -6,6 +6,7 @@ import { addAgentReport, describeAgents, JobStoppedError, withActivity } from ".
|
|
|
6
6
|
import { projectOfLanes, startJobLog } from "./job-log.js";
|
|
7
7
|
import { firstLine, healing, record } from "./ledger.js";
|
|
8
8
|
import { chosenStack, debugMode, withStack } from "../config.js";
|
|
9
|
+
import { currentChat } from "../chat-scope.js";
|
|
9
10
|
import { pulseEnded, pulseWrote } from "./pulse-files.js";
|
|
10
11
|
const DEFAULT_TAIL_CHARS = 4000;
|
|
11
12
|
const ACTIVITY_LINES = 10;
|
|
@@ -57,6 +58,7 @@ export function createJobRegistry(options) {
|
|
|
57
58
|
// The stack chosen as it is queued - or pinned for the work that queues it - not as it starts: a job that waits
|
|
58
59
|
// for a slot is built with the stack it was asked for, whatever is chosen meanwhile.
|
|
59
60
|
const stack = chosenStack();
|
|
61
|
+
const chat = currentChat()?.id;
|
|
60
62
|
const claimed = [...new Set(input.lanes ?? [])];
|
|
61
63
|
const blockers = [
|
|
62
64
|
...new Set(claimed
|
|
@@ -76,6 +78,7 @@ export function createJobRegistry(options) {
|
|
|
76
78
|
diff: input.diff,
|
|
77
79
|
model: input.model,
|
|
78
80
|
...(stack !== undefined ? { stack } : {}),
|
|
81
|
+
...(chat !== undefined ? { chat } : {}),
|
|
79
82
|
...(input.follows !== undefined ? { follows: input.follows } : {}),
|
|
80
83
|
status: blockers.length > 0 || (limit !== undefined && active >= limit) ? "queued" : "running",
|
|
81
84
|
sequence: (sequence += 1),
|
|
@@ -45,6 +45,11 @@ export type LedgerEvent = {
|
|
|
45
45
|
} | {
|
|
46
46
|
kind: "run.ended";
|
|
47
47
|
ms: number;
|
|
48
|
+
} | {
|
|
49
|
+
kind: "chat.opened";
|
|
50
|
+
reopened: boolean;
|
|
51
|
+
} | {
|
|
52
|
+
kind: "chat.closed";
|
|
48
53
|
} | {
|
|
49
54
|
kind: "job.queued";
|
|
50
55
|
jobKind: string;
|
|
@@ -213,6 +218,8 @@ export type LedgerEntry = LedgerEvent & {
|
|
|
213
218
|
job?: string;
|
|
214
219
|
module?: string;
|
|
215
220
|
agent?: AgentRef;
|
|
221
|
+
/** The chat the work was asked for in, in a session of several (chat-scope.ts). */
|
|
222
|
+
chat?: string;
|
|
216
223
|
};
|
|
217
224
|
/** Where an event happened, when not where the code recording it runs: a job's own place is found by itself. */
|
|
218
225
|
export interface LedgerPlace {
|
|
@@ -2,6 +2,7 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
2
2
|
import { appendFileSync, existsSync, readdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
4
|
import { gunzipSync, gzipSync } from "node:zlib";
|
|
5
|
+
import { currentChat } from "../chat-scope.js";
|
|
5
6
|
import { readProjectConfig } from "../config.js";
|
|
6
7
|
import { turriIdentity } from "../environment.js";
|
|
7
8
|
import { currentPlace, onModelEvent } from "./activity.js";
|
|
@@ -110,7 +111,13 @@ export function record(event, place = {}) {
|
|
|
110
111
|
return;
|
|
111
112
|
const job = place.job ?? here.job;
|
|
112
113
|
const module = place.module ?? here.module;
|
|
113
|
-
|
|
114
|
+
const chat = currentChat()?.id;
|
|
115
|
+
write(run, resolve(project), event, {
|
|
116
|
+
...(job !== undefined ? { job } : {}),
|
|
117
|
+
...(module !== undefined ? { module } : {}),
|
|
118
|
+
...(place.agent !== undefined ? { agent: place.agent } : {}),
|
|
119
|
+
...(chat !== undefined ? { chat } : {}),
|
|
120
|
+
});
|
|
114
121
|
}
|
|
115
122
|
/**
|
|
116
123
|
* Run a healer - the debugger, the diagnosis of a failed pulse - and record it: started, ended with what it found, or
|
|
@@ -167,7 +167,7 @@ export declare function createScriptTool(options: {
|
|
|
167
167
|
/** Watches the agent's runs for a loop: told once, then its turn is ended. */
|
|
168
168
|
watch?: LoopWatch;
|
|
169
169
|
}): import("@earendil-works/pi-coding-agent").ToolDefinition<Type.TObject<{}>, {
|
|
170
|
-
loop?: "
|
|
170
|
+
loop?: "stop" | "warn" | undefined;
|
|
171
171
|
command: string;
|
|
172
172
|
exitCode: number;
|
|
173
173
|
}, any> & import("@earendil-works/pi-coding-agent").ToolDefinition<any, any, any>;
|
package/dist/stacks.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Choosing a stack: `config.turri.<stack>.yaml` laid over `config.turri.yaml` for everything built from then on - a
|
|
3
3
|
* backend's testing scripts, then a frontend's. `/stack` and the `stack` tool in the chat, `--stack <name>` on the
|
|
4
|
-
* command line (cli.ts). The choice itself is kept in config.ts (`chooseStack`), for
|
|
4
|
+
* command line (cli.ts). The choice itself is kept in config.ts (`chooseStack`): in the chat it is made in, for that chat
|
|
5
|
+
* alone (chat-scope.ts), each new chat starting from `--stack`.
|
|
5
6
|
*/
|
|
6
7
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
7
8
|
/** The stacks of the projects here: the folder's own, and those of the projects one folder down (specs kept in plain/). */
|
package/dist/stacks.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Choosing a stack: `config.turri.<stack>.yaml` laid over `config.turri.yaml` for everything built from then on - a
|
|
3
3
|
* backend's testing scripts, then a frontend's. `/stack` and the `stack` tool in the chat, `--stack <name>` on the
|
|
4
|
-
* command line (cli.ts). The choice itself is kept in config.ts (`chooseStack`), for
|
|
4
|
+
* command line (cli.ts). The choice itself is kept in config.ts (`chooseStack`): in the chat it is made in, for that chat
|
|
5
|
+
* alone (chat-scope.ts), each new chat starting from `--stack`.
|
|
5
6
|
*/
|
|
6
7
|
import { readdirSync } from "node:fs";
|
|
7
8
|
import { join } from "node:path";
|
|
@@ -74,10 +75,10 @@ export function registerStacks(pi, cwd) {
|
|
|
74
75
|
label: "Stack",
|
|
75
76
|
description: "Choose the stack everything is built with from now on - the project's config.turri.yaml with the stack's " +
|
|
76
77
|
"config.turri.<stack>.yaml over it: its testing scripts, toolchains, models - or none, the project's file alone. " +
|
|
77
|
-
"For
|
|
78
|
+
"For this chat alone - each chat chooses its own; a pulse already running keeps the stack it started with. Without a name, says the stacks and the one chosen.",
|
|
78
79
|
promptSnippet: "choose the stack - its testing scripts and settings - that everything is built with",
|
|
79
80
|
promptGuidelines: [
|
|
80
|
-
"Switch the stack only when the user asks for it, and say which you chose. A module is built with the stack named after it by itself; to build one whose stack has another name, pass that stack as stack to regenerate, conform, glimpse or change_plain instead: it is pinned to that module's pulses, and
|
|
81
|
+
"Switch the stack only when the user asks for it, and say which you chose. A module is built with the stack named after it by itself; to build one whose stack has another name, pass that stack as stack to regenerate, conform, glimpse or change_plain instead: it is pinned to that module's pulses, and this chat's choice stays as it is.",
|
|
81
82
|
],
|
|
82
83
|
parameters: Type.Object({
|
|
83
84
|
name: Type.Optional(Type.String({ description: "The stack to choose, or none. Omitted: the stacks there are, and the one chosen" })),
|
package/docs/README.md
CHANGED
|
@@ -16,6 +16,7 @@ turri is the first coding agent with the power of regenerative software: [\*\*\*
|
|
|
16
16
|
| what my machine needs, and why a check failed | [installation](installation.md) |
|
|
17
17
|
| what turri needs of this machine, and what `turri doctor` checks | [installation](installation.md#what-turri-doctor-checks) |
|
|
18
18
|
| how to start a project, or add a feature, in a conversation | [using](using.md#in-a-conversation) |
|
|
19
|
+
| how to have several chats open at once, and reopen them | [chats](chats.md) |
|
|
19
20
|
| which model codes, and how to change it | [using](using.md#choosing-a-model), [the project config](config.md#models) |
|
|
20
21
|
| what `config.turri.yaml` can set, and what happens without it | [the project config](config.md) |
|
|
21
22
|
| how a backend and a frontend on different stacks share one project | [the project config](config.md#stacks) |
|
|
@@ -31,7 +32,7 @@ turri is the first coding agent with the power of regenerative software: [\*\*\*
|
|
|
31
32
|
| what the agents can and cannot reach | [the sandbox](sandbox.md) |
|
|
32
33
|
| how skill review gates and judges work | [skills and gates](skills-and-gates.md) |
|
|
33
34
|
| how the parser matches the reference implementation | [the \*\*\*plain parser](parser.md) |
|
|
34
|
-
| how a desktop app or an editor runs turri's
|
|
35
|
+
| how a desktop app or an editor runs turri's chats itself | [running turri in a host](hosting.md) |
|
|
35
36
|
| where the code for something lives | [development](development.md#layout) |
|
|
36
37
|
|
|
37
38
|
## All pages
|
|
@@ -40,6 +41,7 @@ turri is the first coding agent with the power of regenerative software: [\*\*\*
|
|
|
40
41
|
| --- | --- |
|
|
41
42
|
| [Installation](installation.md) | doctor, what turri needs of this machine, turri and `pi`, environment variables |
|
|
42
43
|
| [Using turri](using.md) | the conversation, the tools, background jobs, choosing a model, module history, the command line |
|
|
44
|
+
| [Chats](chats.md) | several chats in one session: what each keeps and what they share, `/chat` and its keys, `turri --continue` and `.turri/chats.json` |
|
|
43
45
|
| [The project config](config.md) | `config.turri.yaml` and each stack's `config.turri.<stack>.yaml`: where they are read from, every key, models and their inheritance, conformance, the testing scripts, stacks and choosing one, the old `config.yaml` |
|
|
44
46
|
| [Deploy](deploy.md) | trying what is built: dist/<module>, the last commit, starting it on your machine, `turri release` |
|
|
45
47
|
| [Running unattended](headless.md) | regenerating overnight or in CI: before you leave it, the command, the results, resuming, rebuilding one functionality |
|
|
@@ -49,5 +51,5 @@ turri is the first coding agent with the power of regenerative software: [\*\*\*
|
|
|
49
51
|
| [Skills and gates](skills-and-gates.md) | the gate syntax, what the harness enforces, what a step can do, the rules, trying the gates |
|
|
50
52
|
| [The \*\*\*plain parser](parser.md) | the TypeScript port, `turri validate`, inheritance, the iterator, fidelity |
|
|
51
53
|
| [Theming](theming.md) | the turri themes, contrast, backgrounds, the font |
|
|
52
|
-
| [Running turri in a host](hosting.md) | `@turri-dev/cli/host`: the
|
|
54
|
+
| [Running turri in a host](hosting.md) | `@turri-dev/cli/host`: the session and its chats in a host's own windows - their jobs, their questions - and what a host shows beside them |
|
|
53
55
|
| [Development](development.md) | tests, the source layout, known edges |
|