killeros 3.0.0 → 3.0.2
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/CHANGELOG.md +40 -0
- package/README.md +46 -6
- package/killeros/activity.ts +2 -1
- package/killeros/auto-compaction.ts +29 -14
- package/killeros/bounded-text.ts +1 -1
- package/killeros/change-receipt.ts +3 -14
- package/killeros/goal-interface.ts +16 -3
- package/killeros/goal-runtime.ts +6 -2
- package/killeros/goal-settlement.ts +4 -0
- package/killeros/handoff.ts +2 -1
- package/killeros/hooks.ts +24 -3
- package/killeros/passive-git-status.ts +11 -6
- package/killeros/runtime.ts +2 -2
- package/killeros/secret-detector.ts +1 -1
- package/killeros/settings.ts +6 -2
- package/killeros/worked-for.ts +11 -4
- package/package.json +8 -8
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,46 @@ All notable changes to KillerOS are documented here. Releases use New Features f
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [3.0.2] - 2026-10-05
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Raised the minimum supported Pi version and locked Pi development packages to 1.0.2. Compatibility CI now tests the exact 1.0.2 minimum and latest stable 1.x; users on older Pi versions must upgrade.
|
|
12
|
+
- Documented Pi's native thinking-level sampling configuration and trusted-project MCP overrides without adding KillerOS commands or settings.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- Added real Pi contract coverage for capacity-error recovery within one logical goal turn, with task receipts and completion sounds deferred until settlement.
|
|
17
|
+
- Verified that native OpenAI fast mode preserves thinking-level sampling overrides across API-key and ChatGPT subscription requests, extension reloads, and provider retries.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- Kept POSIX process-group cleanup pending after a timed-out or cancelled hook's shell exits, preserving forced termination and reporting bounded uncertainty unless group exit is confirmed. Fixes #42.
|
|
22
|
+
- Preserved goal-request ownership through settlement so automatic compaction cannot restart completed or blocked goals as ordinary work. Active-goal recovery, later ordinary requests, and compaction error reporting remain supported. Fixes #43.
|
|
23
|
+
- Corrected the process-tree timeout fixture's marker argument and delayed descendant writes beyond cleanup. Added a real cleanup-disabled control that detects surviving descendants and checks explicit uncertainty. Fixes #44.
|
|
24
|
+
|
|
25
|
+
## [3.0.1] - 2026-10-02
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- Expanded Pi peer ranges to `>=0.99.2 <2` and pinned development packages to 1.0.0. Compatibility CI now runs the full quality suite against the exact 0.99.2 minimum and latest stable 1.x with matched host packages, without changing the tracked manifest or lockfile.
|
|
30
|
+
- Documented Pi 1.0's fullscreen default and the regular-mode override for terminal-owned scrollback.
|
|
31
|
+
- Documented the protected PR-based release process and merge-commit requirement for automatic `main`-to-`dev` synchronization. CI now validates release metadata before package smoke testing, so mismatched versions, missing release notes, and stale README tags fail before merge.
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- Reused bounded cleanup retries in passive Git footer tests so transient Windows directory locks do not fail CI after the security assertions pass.
|
|
36
|
+
- Preserved an authorized goal continuation when a replacement becomes stale during its idle wait, instead of leaving the surviving goal active but idle. Navigation, clear, turn limits, and competing continuation holds remain respected.
|
|
37
|
+
- Rejected recognized credential assignments in Markdown handoffs, including list items, bold labels, and inline-code labels, before creating a destination session.
|
|
38
|
+
- Discarded pending goal mutations after committed navigation or a competing goal mutation during confirmation, idle waiting, or file-baseline inference, including goals set and then cleared. Overlapping commands retain their own continuation holds. Cancelled navigation preserves valid pending commands.
|
|
39
|
+
- Preserved concurrent changes to independent `/auto-compact` fields by merging the effective preference under the settings lock.
|
|
40
|
+
- Removed C1 terminal controls from custom activity tool names.
|
|
41
|
+
- Clipped collapsed and expanded transcript lines to the terminal width, including wide Unicode characters at one column.
|
|
42
|
+
- Treated failed Git HEAD lookups as unavailable instead of inventing added files, while retaining support for repositories without commits. Footer scans and task receipts share the same HEAD resolution.
|
|
43
|
+
- Paused normally stopped goals without an accepted decision even when automatic compaction succeeds or skips a session-too-small request, preventing repeated same-turn requests from bypassing the turn limit. Interrupted-turn recovery and accepted next-turn decisions remain supported.
|
|
44
|
+
- Saved `/handoff` context before reporting success so the linked session survives immediate exit and resume without another prompt or an automatic agent turn.
|
|
45
|
+
- Discarded pending task receipts after committed tree navigation, preventing late scans from attaching the abandoned branch's receipt to the selected branch. Cancelled navigation still preserves receipts.
|
|
46
|
+
|
|
7
47
|
## [3.0.0] - 2026-10-01
|
|
8
48
|
|
|
9
49
|
### Changed
|
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ A TypeScript extension for the [Pi coding agent](https://github.com/earendil-wor
|
|
|
19
19
|
## Requirements
|
|
20
20
|
|
|
21
21
|
- Node.js 22.19.0+
|
|
22
|
-
- Pi 0.
|
|
22
|
+
- Pi 1.0.2 or later below 2.0.0
|
|
23
23
|
- An interactive TUI session for the custom header, editor, footer, and `question`
|
|
24
24
|
|
|
25
25
|
## Install
|
|
@@ -34,7 +34,7 @@ Or from GitHub:
|
|
|
34
34
|
pi install git:github.com/KyrosHendrix/pi-KillerOS
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
Pin a release by appending its tag, for example `@v3.0.
|
|
37
|
+
Pin a release by appending its tag, for example `@v3.0.2`. Add `-l` to install only for the current project. Restart Pi after installing.
|
|
38
38
|
|
|
39
39
|
## Commands
|
|
40
40
|
|
|
@@ -61,6 +61,8 @@ The TUI footer's `fast` label means the enabled preference applies to the select
|
|
|
61
61
|
|
|
62
62
|
## Behavior by mode
|
|
63
63
|
|
|
64
|
+
Pi 1.0 defaults to fullscreen. Use `--tui-mode regular` for one invocation or set `"tuiMode": "regular"` in Pi's [settings](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/settings.md#terminal-and-display) to restore terminal-owned scrollback. KillerOS leaves Pi's `tuiMode` and `quietStartup` preferences unchanged.
|
|
65
|
+
|
|
64
66
|
| Mode | What works |
|
|
65
67
|
| --- | --- |
|
|
66
68
|
| TUI | Everything |
|
|
@@ -85,7 +87,9 @@ The packaged `killeros` theme activates on TUI start. Compaction triggers by def
|
|
|
85
87
|
|
|
86
88
|
Use `/auto-compact status` to inspect the effective KillerOS preference, `/auto-compact on` or `/auto-compact off` to toggle it, and `/auto-compact <percent>` to set an integer threshold from 0 through 100. A threshold of 0 does not disable Pi's own token reserve.
|
|
87
89
|
|
|
88
|
-
`
|
|
90
|
+
Concurrent `/auto-compact` commands preserve independent changes to the enabled flag and threshold, along with unrelated settings. Malformed settings and failed writes report an error without claiming success or replacing the original file.
|
|
91
|
+
|
|
92
|
+
`handoffMaxTokens` caps the `/handoff` summary output at 8192 tokens by default; raise it when long sessions truncate the summary. The handoff is saved as a visible user-context message before success is reported, so the linked session survives immediate exit and resume without sending another prompt. Creating the handoff does not start an agent turn.
|
|
89
93
|
|
|
90
94
|
State proof in the objective so the agent can verify it with its normal tools:
|
|
91
95
|
|
|
@@ -99,12 +103,34 @@ A direct quoted path-shaped file target binds silent file proof. For an extensio
|
|
|
99
103
|
/goal Fix `killeros/footer.ts`, verified by npm test
|
|
100
104
|
```
|
|
101
105
|
|
|
102
|
-
Bare quoted prose remains model-reported. KillerOS captures the file baseline at goal start and only completes when the file is created or changed. A normal response never continues a goal by itself: the agent must record `continue`, `complete`, or a blocker decision through `killeros_goal_update`. Repeated continuation reports and unavailable goal tools pause the goal. New goals pause after 20 turns without warning. An explicit `/goal resume` on an exhausted goal grants another 20 turns; compaction recovery never grants turns. Restored goals keep their persisted limit.
|
|
106
|
+
Bare quoted prose remains model-reported. KillerOS captures the file baseline at goal start and only completes when the file is created or changed. A normal response never continues a goal by itself: the agent must record `continue`, `complete`, or a blocker decision through `killeros_goal_update`. Repeated continuation reports and unavailable goal tools pause the goal. New goals pause after 20 turns without warning. An explicit `/goal resume` on an exhausted goal grants another 20 turns; compaction recovery never grants turns. Same-turn recovery requires an actual interruption; successful or skipped compaction cannot restart a normally stopped goal without a decision. A request that completes or blocks a goal remains goal-owned through settlement, so automatic compaction cannot restart it as ordinary work. Later ordinary requests still support compaction continuation. Restored goals keep their persisted limit.
|
|
107
|
+
|
|
108
|
+
Session replacement, reload, and committed tree navigation discard unfinished task receipts. Late receipt results do not write or notify through the old session context or attach to a different branch. Cancelled navigation preserves the pending receipt.
|
|
103
109
|
|
|
104
|
-
|
|
110
|
+
Pending `/goal` commands discard their mutation if committed navigation changes the branch or another mutation changes the goal while confirmation, idle waiting, or file-baseline reading is in progress. Cancelled navigation preserves valid pending commands. Discarding a stale replacement after its idle wait preserves the surviving goal's authorized continuation.
|
|
111
|
+
|
|
112
|
+
Failed Git metadata reads keep the footer's last successful file counts and mark task changes unavailable. They are not treated as an empty repository.
|
|
105
113
|
|
|
106
114
|
Completion sounds are off by default; change with `/notification` in TUI mode. The tab-title indicator requires a Nerd Font.
|
|
107
115
|
|
|
116
|
+
### Pi model and MCP settings
|
|
117
|
+
|
|
118
|
+
Use Pi's native `/thinking` selector for reasoning levels. Pi 1.0.2 can also apply different sampling parameters for each level through `samplingParamsByThinkingLevel` in `models.json`. For example, a model entry can contain:
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
{
|
|
122
|
+
"samplingParams": { "temperature": 1.0, "top_p": 0.95 },
|
|
123
|
+
"samplingParamsByThinkingLevel": {
|
|
124
|
+
"off": { "temperature": 0.7, "top_p": 0.8 },
|
|
125
|
+
"high": { "temperature": 0.6 }
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Only configure parameters the endpoint accepts. These settings apply to `openai-completions`, `openai-responses`, and `azure-openai-responses`, not legacy `openai-codex`. Missing levels inherit model defaults; request-level sampling parameters take precedence. `/codex-fast` preserves sampling parameters when it adds the priority service tier. See Pi's [sampling configuration](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/models.md#configure-sampling-by-thinking-level) for the full file format and merge rules.
|
|
131
|
+
|
|
132
|
+
Use Pi's `/mcp` command and `.pi/mcp.json` to enable, disable, or change the exposure of a user-level MCP server for a trusted project without copying its credentials. See [project MCP overrides](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/mcp.md#configure-servers). KillerOS does not manage MCP configuration.
|
|
133
|
+
|
|
108
134
|
## Development
|
|
109
135
|
|
|
110
136
|
Strict TypeScript throughout. Tests run on Node's built-in test runner:
|
|
@@ -113,12 +139,26 @@ Strict TypeScript throughout. Tests run on Node's built-in test runner:
|
|
|
113
139
|
npm ci && npm run check && npm test
|
|
114
140
|
```
|
|
115
141
|
|
|
116
|
-
|
|
142
|
+
### Release process
|
|
143
|
+
|
|
144
|
+
1. Prepare the release on `dev`: update `package.json` and both root versions in `package-lock.json`, update the README's pinned tag, and move completed changelog entries into a dated version section under `[Unreleased]`.
|
|
145
|
+
2. Run `npm run check` and `npm test`, commit and push to `dev`, then open a pull request into `main`. Push later fixes to the same PR so CI reruns.
|
|
146
|
+
3. Wait for every required check to pass, including CodeQL and dependency review, and resolve review conversations. The PR must be up to date with `main`.
|
|
147
|
+
4. Merge using **Create a merge commit**. Squash and rebase merging are disabled because the automated `main`-to-`dev` sync requires shared ancestry. Avoid advancing `dev` until that sync finishes.
|
|
148
|
+
5. Check the `main` CI run and the following Release run. Successful CI on the current `main` commit triggers npm publication with provenance, creates the GitHub release, and fast-forwards `dev` to the released commit. A green PR alone never publishes.
|
|
149
|
+
|
|
150
|
+
`main` requires pull requests and the CI workflow's required checks, including for administrators. Force pushes and branch deletion are blocked. If CI job names change, update GitHub's required status checks to match.
|
|
151
|
+
|
|
152
|
+
Releases go through CI on `main`; do not push version tags manually. The package smoke job validates release metadata before packing, and the release workflow checks it again before publication. The prepublish check rejects ordinary direct `npm publish`, but `--ignore-scripts` can bypass it. Configure npm's trusted publisher for `release.yml`, set package publishing access to "Require two-factor authentication and disallow tokens", and revoke unused publish tokens. npm maintainers can still publish interactively with 2FA, so workflow-only publishing also depends on maintainer policy.
|
|
117
153
|
|
|
118
154
|
## Security
|
|
119
155
|
|
|
120
156
|
Pi extensions run with your user permissions. Review the source before installing globally. Hook commands run only for projects Pi marks as trusted; check `.pi/killeros-hooks.json` before enabling project trust. KillerOS accepts that configuration only as a regular, non-linked file no larger than 64 KiB in the project's real `.pi` directory.
|
|
121
157
|
|
|
158
|
+
On POSIX systems, timed-out or cancelled hooks receive `SIGTERM`, followed by `SIGKILL` if cleanup remains pending. Shell exit alone does not confirm that the original process group stopped. Cleanup uses a two-second window and reports uncertainty if group exit cannot be confirmed.
|
|
159
|
+
|
|
160
|
+
Handoff validation rejects recognized credential assignments in plain text, Markdown lists, bold labels, and inline-code labels before creating a destination session. This pattern check cannot detect every secret or personally identifying value. Rejected values are not included in the error notification.
|
|
161
|
+
|
|
122
162
|
## License
|
|
123
163
|
|
|
124
164
|
[MIT](LICENSE) © 2026 KyrosHendrix
|
package/killeros/activity.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
3
|
import { modelDisplayName } from "./display.ts";
|
|
4
|
+
import { safeTerminalText } from "./safe-terminal-text.ts";
|
|
4
5
|
|
|
5
6
|
export type ActivityMessage =
|
|
6
7
|
| { kind: "prompt" }
|
|
@@ -9,7 +10,7 @@ export type ActivityMessage =
|
|
|
9
10
|
| { kind: "responding" };
|
|
10
11
|
|
|
11
12
|
function safeToolName(toolName: string): string {
|
|
12
|
-
const normalized = toolName.replace(/[\
|
|
13
|
+
const normalized = safeTerminalText(toolName.replace(/[\r\n\t]+/gu, " ")).replace(/\s+/gu, " ").trim();
|
|
13
14
|
return truncateToWidth(normalized || "tool", 32, "…");
|
|
14
15
|
}
|
|
15
16
|
|
|
@@ -121,12 +121,14 @@ export function registerAutoCompaction(
|
|
|
121
121
|
const loadPreference = dependencies.loadPreference ?? (() => defaultPreference(settingsStore));
|
|
122
122
|
const getCompactionSettings = dependencies.getCompactionSettings ?? defaultCompactionSettings;
|
|
123
123
|
let request: AutoCompactionRequest | undefined;
|
|
124
|
+
let goalRequestOwned = false;
|
|
124
125
|
let armed = true;
|
|
125
126
|
let rearmOnNextLogicalRequest = false;
|
|
126
127
|
let settingsErrorReported = false;
|
|
127
128
|
|
|
128
129
|
const resetForLifecycle = (): void => {
|
|
129
130
|
request = undefined;
|
|
131
|
+
goalRequestOwned = false;
|
|
130
132
|
armed = true;
|
|
131
133
|
rearmOnNextLogicalRequest = false;
|
|
132
134
|
settingsErrorReported = false;
|
|
@@ -155,11 +157,15 @@ export function registerAutoCompaction(
|
|
|
155
157
|
}
|
|
156
158
|
|
|
157
159
|
try {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
160
|
+
let updated: AutoCompactionPreference | undefined;
|
|
161
|
+
settingsStore.update((current) => {
|
|
162
|
+
const preference = readAutoCompactionPreference(current);
|
|
163
|
+
updated = percent === undefined
|
|
164
|
+
? { ...preference, enabled: argument === "on" }
|
|
165
|
+
: { ...preference, percentRemaining: percent };
|
|
166
|
+
return { autoCompaction: updated };
|
|
167
|
+
});
|
|
168
|
+
if (!updated) throw new Error("Settings update did not apply the preference");
|
|
163
169
|
ctx.ui.notify(percent === undefined
|
|
164
170
|
? updated.enabled
|
|
165
171
|
? `Automatic compaction: on at ${updated.percentRemaining}% remaining`
|
|
@@ -235,6 +241,11 @@ export function registerAutoCompaction(
|
|
|
235
241
|
}
|
|
236
242
|
};
|
|
237
243
|
|
|
244
|
+
pi.on("agent_start", (_event, ctx) => {
|
|
245
|
+
// Terminal decisions clear active status before turn_end, but still own this run.
|
|
246
|
+
goalRequestOwned = dependencies.goal?.isActive(ctx) === true;
|
|
247
|
+
});
|
|
248
|
+
|
|
238
249
|
pi.on("turn_end", (_event, ctx) => {
|
|
239
250
|
if (!supportedMode(ctx) || request) return;
|
|
240
251
|
|
|
@@ -273,7 +284,8 @@ export function registerAutoCompaction(
|
|
|
273
284
|
|
|
274
285
|
armed = false;
|
|
275
286
|
const token = Symbol();
|
|
276
|
-
const
|
|
287
|
+
const activeGoal = dependencies.goal?.isActive(ctx) === true;
|
|
288
|
+
const goal = goalRequestOwned || activeGoal;
|
|
277
289
|
request = {
|
|
278
290
|
phase: "compacting",
|
|
279
291
|
goal,
|
|
@@ -281,7 +293,7 @@ export function registerAutoCompaction(
|
|
|
281
293
|
turnSettled: false,
|
|
282
294
|
compactionCompleted: false,
|
|
283
295
|
};
|
|
284
|
-
if (
|
|
296
|
+
if (activeGoal && dependencies.goal) {
|
|
285
297
|
try {
|
|
286
298
|
dependencies.goal.onRequested();
|
|
287
299
|
} catch (error) {
|
|
@@ -295,26 +307,29 @@ export function registerAutoCompaction(
|
|
|
295
307
|
onComplete: () => {
|
|
296
308
|
if (request?.phase !== "compacting" || request.token !== token) return;
|
|
297
309
|
rearmOnNextLogicalRequest = preference.percentRemaining === 100;
|
|
298
|
-
if (goal
|
|
310
|
+
if (goal) {
|
|
299
311
|
request = undefined;
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
312
|
+
if (activeGoal && dependencies.goal) {
|
|
313
|
+
try {
|
|
314
|
+
dependencies.goal.onCompleted(ctx);
|
|
315
|
+
} catch (error) {
|
|
316
|
+
notifyFailure(ctx, error);
|
|
317
|
+
}
|
|
304
318
|
}
|
|
305
319
|
return;
|
|
306
320
|
}
|
|
307
321
|
request.compactionCompleted = true;
|
|
308
322
|
finalizeOrdinaryContinuation(ctx, token);
|
|
309
323
|
},
|
|
310
|
-
onError: (error) => finishRequestError(ctx, token,
|
|
324
|
+
onError: (error) => finishRequestError(ctx, token, activeGoal, error),
|
|
311
325
|
});
|
|
312
326
|
} catch (error) {
|
|
313
|
-
finishRequestError(ctx, token,
|
|
327
|
+
finishRequestError(ctx, token, activeGoal, error);
|
|
314
328
|
}
|
|
315
329
|
});
|
|
316
330
|
|
|
317
331
|
pi.on("agent_settled", (_event, ctx) => {
|
|
332
|
+
goalRequestOwned = false;
|
|
318
333
|
if (request?.phase === "continuation-dispatched") {
|
|
319
334
|
request = undefined;
|
|
320
335
|
notifyFailure(ctx, new Error("continuation was not accepted"));
|
package/killeros/bounded-text.ts
CHANGED
|
@@ -11,7 +11,7 @@ export class BoundedText implements Component {
|
|
|
11
11
|
|
|
12
12
|
render(width: number): string[] {
|
|
13
13
|
if (width <= 0) return [];
|
|
14
|
-
const lines = new Text(this.text, 0, 0).render(width);
|
|
14
|
+
const lines = new Text(this.text, 0, 0).render(width).map((line) => truncateToWidth(line, width, ""));
|
|
15
15
|
if (this.maxRows === undefined || lines.length <= this.maxRows) return lines;
|
|
16
16
|
const rowLimit = Math.max(1, this.maxRows);
|
|
17
17
|
const visible = lines.slice(0, rowLimit);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { createReadStream, watch } from "node:fs";
|
|
3
|
-
import { inspectWorktreeWithoutFilters, passiveGitCommand, passiveGitEnv, passiveWorktreeMode, passiveWorktreeSettings, type PassiveWorktreeFile as DirtyFile, type PassiveWorktreeSnapshot } from "./passive-git-status.ts";
|
|
3
|
+
import { inspectWorktreeWithoutFilters, passiveGitCommand, passiveGitEnv, passiveWorktreeMode, passiveWorktreeSettings, resolvePassiveHead, type PassiveWorktreeFile as DirtyFile, type PassiveWorktreeSnapshot } from "./passive-git-status.ts";
|
|
4
4
|
import { lstat, open, readlink } from "node:fs/promises";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { createInflate } from "node:zlib";
|
|
@@ -237,19 +237,8 @@ async function snapshotKnownPaths(repo: Repository, baseline: Snapshot, paths: r
|
|
|
237
237
|
return { ...baseline, files };
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
try {
|
|
243
|
-
resolved = await runGit(repo.root, ["rev-parse", "--verify", "HEAD"], undefined, 4_096);
|
|
244
|
-
} catch (error) {
|
|
245
|
-
if (!(error instanceof GitFailure) || error.reason !== "error") throw error;
|
|
246
|
-
const reference = decode(await runGit(repo.root, ["symbolic-ref", "--quiet", "HEAD"], undefined, 4_096)).trim();
|
|
247
|
-
if (!/^refs\/[^\0\r\n]+$/u.test(reference)) throw new GitFailure("error");
|
|
248
|
-
return "(initial)";
|
|
249
|
-
}
|
|
250
|
-
const head = decode(resolved).trim();
|
|
251
|
-
if (!/^[0-9a-f]{40}(?:[0-9a-f]{24})?$/u.test(head)) throw new GitFailure("error");
|
|
252
|
-
return head;
|
|
240
|
+
function currentHead(repo: Repository): Promise<string> {
|
|
241
|
+
return resolvePassiveHead((args) => runGit(repo.root, args, undefined, 4_096));
|
|
253
242
|
}
|
|
254
243
|
|
|
255
244
|
function observes(filePath: string, observedPaths: readonly string[]): boolean {
|
|
@@ -311,6 +311,9 @@ export function registerGoalInterface(
|
|
|
311
311
|
ctx.ui.notify("/goal requires a saved session", "error");
|
|
312
312
|
return;
|
|
313
313
|
}
|
|
314
|
+
const generation = runtime.lifecycleGeneration;
|
|
315
|
+
const initiatingState = runtime.state;
|
|
316
|
+
const ownsGoal = (): boolean => runtime.lifecycleGeneration === generation && runtime.state === initiatingState;
|
|
314
317
|
|
|
315
318
|
if (command.kind === "status") {
|
|
316
319
|
if (!runtime.state) {
|
|
@@ -323,9 +326,11 @@ export function registerGoalInterface(
|
|
|
323
326
|
}
|
|
324
327
|
const actions = goalPanelActions(runtime.state.status);
|
|
325
328
|
const selected = await ctx.ui.select(goalStatusSummary(runtime.state, ctx), actions.map((action) => action.label));
|
|
329
|
+
if (!ownsGoal()) return;
|
|
326
330
|
const action = actions.find((candidate) => candidate.label === selected);
|
|
327
331
|
if (!action) return;
|
|
328
332
|
if (action.control === "clear" && !await ctx.ui.confirm("Clear goal?", safeTerminalText(runtime.state.objective))) return;
|
|
333
|
+
if (!ownsGoal()) return;
|
|
329
334
|
await handleGoalCommand(action.control, ctx);
|
|
330
335
|
return;
|
|
331
336
|
}
|
|
@@ -487,17 +492,23 @@ export function registerGoalInterface(
|
|
|
487
492
|
return;
|
|
488
493
|
}
|
|
489
494
|
const replace = await ctx.ui.confirm("Replace active goal", "Replace the current unfinished goal and discard its continuation state?");
|
|
490
|
-
if (!replace) return;
|
|
495
|
+
if (!replace || !ownsGoal()) return;
|
|
491
496
|
}
|
|
492
497
|
|
|
493
|
-
|
|
498
|
+
const hold = Symbol();
|
|
499
|
+
runtime.continuationHeld = hold;
|
|
494
500
|
let waitError: unknown;
|
|
495
501
|
try {
|
|
496
502
|
await ctx.waitForIdle();
|
|
497
503
|
} catch (error) {
|
|
498
504
|
waitError = error;
|
|
499
505
|
} finally {
|
|
500
|
-
runtime.continuationHeld =
|
|
506
|
+
if (runtime.continuationHeld === hold) runtime.continuationHeld = undefined;
|
|
507
|
+
}
|
|
508
|
+
if (!ownsGoal()) {
|
|
509
|
+
// Settlement may have authorized a turn while this command held continuation.
|
|
510
|
+
if (runtime.lifecycleGeneration === generation) scheduleGoalContinuation(pi, runtime, ctx);
|
|
511
|
+
return;
|
|
501
512
|
}
|
|
502
513
|
if (waitError) {
|
|
503
514
|
reportError(ctx, "Goal could not wait for the active turn", waitError);
|
|
@@ -508,6 +519,7 @@ export function registerGoalInterface(
|
|
|
508
519
|
try {
|
|
509
520
|
verification = await inferGoalVerification(objective, ctx.cwd);
|
|
510
521
|
} catch (error) {
|
|
522
|
+
if (!ownsGoal()) return;
|
|
511
523
|
if (!unfinished) {
|
|
512
524
|
reportError(ctx, "Goal could not be started", error);
|
|
513
525
|
} else {
|
|
@@ -516,6 +528,7 @@ export function registerGoalInterface(
|
|
|
516
528
|
}
|
|
517
529
|
return;
|
|
518
530
|
}
|
|
531
|
+
if (!ownsGoal()) return;
|
|
519
532
|
try {
|
|
520
533
|
const state = createNewGoalState(objective, sumGoalTokens(ctx), verification, Date.now(), {
|
|
521
534
|
maxTurns: DEFAULT_GOAL_MAX_TURNS,
|
package/killeros/goal-runtime.ts
CHANGED
|
@@ -119,6 +119,10 @@ export function persistGoalState(
|
|
|
119
119
|
const data: GoalEntryData = { version: GOAL_VERSION, event, state: state ?? null };
|
|
120
120
|
pi.appendEntry(GOAL_ENTRY_TYPE, data);
|
|
121
121
|
runtime.state = state;
|
|
122
|
+
if (event === "clear") {
|
|
123
|
+
runtime.lifecycleGeneration += 1;
|
|
124
|
+
runtime.continuationHeld = undefined;
|
|
125
|
+
}
|
|
122
126
|
syncGoalUpdateTool(pi, runtime);
|
|
123
127
|
runtime.persistenceRetryNeeded = false;
|
|
124
128
|
runtime.requestRender?.();
|
|
@@ -404,7 +408,7 @@ export function registerGoalRuntime(
|
|
|
404
408
|
runtime.state = isGoalModeSupported(ctx) ? restored.state : undefined;
|
|
405
409
|
syncGoalUpdateTool(pi, runtime);
|
|
406
410
|
runtime.continuationScheduled = false;
|
|
407
|
-
runtime.continuationHeld =
|
|
411
|
+
runtime.continuationHeld = undefined;
|
|
408
412
|
runtime.goalTurnInFlight = false;
|
|
409
413
|
runtime.goalTurn = undefined;
|
|
410
414
|
runtime.agentEndObserved = false;
|
|
@@ -441,7 +445,7 @@ export function registerGoalRuntime(
|
|
|
441
445
|
runtime.state = undefined;
|
|
442
446
|
syncGoalUpdateTool(pi, runtime);
|
|
443
447
|
clearGoalExecutionFlags(runtime);
|
|
444
|
-
runtime.continuationHeld =
|
|
448
|
+
runtime.continuationHeld = undefined;
|
|
445
449
|
runtime.persistenceRetryNeeded = false;
|
|
446
450
|
});
|
|
447
451
|
|
|
@@ -254,6 +254,10 @@ export function registerGoalSettlement(
|
|
|
254
254
|
);
|
|
255
255
|
return;
|
|
256
256
|
}
|
|
257
|
+
if (!expectedInterruption && runtime.automaticCompaction.resumeSameTurn) {
|
|
258
|
+
stopAutomaticCompactionRecovery(pi, runtime, ctx, "no turn decision");
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
257
261
|
if (settledTurn !== undefined && runtime.automaticCompaction.turn === settledTurn) {
|
|
258
262
|
runtime.automaticCompaction.turnSettled = true;
|
|
259
263
|
} else {
|
package/killeros/handoff.ts
CHANGED
|
@@ -300,7 +300,8 @@ export function registerHandoff(pi: ExtensionAPI, goalRuntime: GoalRuntime, hand
|
|
|
300
300
|
parentSession: sourceSession,
|
|
301
301
|
setup: async (sessionManager) => {
|
|
302
302
|
try {
|
|
303
|
-
|
|
303
|
+
// A regular user message makes Pi persist the otherwise setup-only child session.
|
|
304
|
+
sessionManager.appendMessage({ role: "user", content: document, timestamp: Date.now() });
|
|
304
305
|
sessionManager.appendSessionInfo(sessionName(sourceName, focus, document));
|
|
305
306
|
} catch (error) {
|
|
306
307
|
setupFailure = { error };
|
package/killeros/hooks.ts
CHANGED
|
@@ -236,6 +236,17 @@ function terminateHookProcess(child: HookChildProcess, force: boolean): void {
|
|
|
236
236
|
}
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
+
function hookProcessGroupExited(child: HookChildProcess): boolean {
|
|
240
|
+
if (!child.pid) return false;
|
|
241
|
+
try {
|
|
242
|
+
process.kill(-child.pid, 0);
|
|
243
|
+
return false;
|
|
244
|
+
} catch (error) {
|
|
245
|
+
// Permission and other failures are uncertainty, not proof of exit.
|
|
246
|
+
return error instanceof Error && "code" in error && error.code === "ESRCH";
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
239
250
|
export function executeHook(options: ExecuteHookOptions): Promise<HookExecutionResult>;
|
|
240
251
|
/** @deprecated Use the object-argument form. The positional adapter will be removed in the next major release. */
|
|
241
252
|
export function executeHook(
|
|
@@ -297,6 +308,8 @@ export function executeHook(
|
|
|
297
308
|
let forceTimer: NodeJS.Timeout | undefined;
|
|
298
309
|
let settleTimer: NodeJS.Timeout | undefined;
|
|
299
310
|
let windowsCleanupPending = false;
|
|
311
|
+
let posixCleanupPending = false;
|
|
312
|
+
let shellClosed = false;
|
|
300
313
|
const finish = (code: number, exitUnconfirmed = false): void => {
|
|
301
314
|
if (completed) return;
|
|
302
315
|
completed = true;
|
|
@@ -334,10 +347,15 @@ export function executeHook(
|
|
|
334
347
|
}, 2_000);
|
|
335
348
|
return;
|
|
336
349
|
}
|
|
350
|
+
posixCleanupPending = process.platform !== "win32" && child.pid !== undefined;
|
|
337
351
|
forceTimer = setTimeout(() => {
|
|
338
352
|
if (completed) return;
|
|
339
353
|
terminateHookProcess(child, true);
|
|
340
|
-
|
|
354
|
+
if (completed) return;
|
|
355
|
+
settleTimer = setTimeout(() => {
|
|
356
|
+
const confirmed = posixCleanupPending && shellClosed && hookProcessGroupExited(child);
|
|
357
|
+
finish(terminationCode(), !confirmed);
|
|
358
|
+
}, 1_000);
|
|
341
359
|
}, 1_000);
|
|
342
360
|
terminateHookProcess(child, false);
|
|
343
361
|
};
|
|
@@ -349,10 +367,13 @@ export function executeHook(
|
|
|
349
367
|
if (child.stderr instanceof EventEmitter) child.stderr.on("error", captureStreamError);
|
|
350
368
|
child.on("error", (error) => {
|
|
351
369
|
appendBounded(stderr, error.message);
|
|
352
|
-
if (!windowsCleanupPending) finish(termination ? terminationCode() : 1);
|
|
370
|
+
if (!windowsCleanupPending && !posixCleanupPending) finish(termination ? terminationCode() : 1);
|
|
353
371
|
});
|
|
354
372
|
child.once("close", (code) => {
|
|
355
|
-
|
|
373
|
+
shellClosed = true;
|
|
374
|
+
if (!windowsCleanupPending && (!posixCleanupPending || hookProcessGroupExited(child))) {
|
|
375
|
+
finish(termination ? terminationCode() : code ?? 1);
|
|
376
|
+
}
|
|
356
377
|
});
|
|
357
378
|
if (completed) return;
|
|
358
379
|
const boundedTimeoutMs = Number.isNaN(timeoutMs)
|
|
@@ -8,6 +8,13 @@ const PASSIVE_METADATA_ARGS = ["-c", "core.fsmonitor=false"] as const;
|
|
|
8
8
|
|
|
9
9
|
export type PassiveGitRunner = (args: readonly string[]) => Promise<Buffer>;
|
|
10
10
|
|
|
11
|
+
/** Revs-only returns empty output for an unborn HEAD; command failures still reject. */
|
|
12
|
+
export async function resolvePassiveHead(runGit: PassiveGitRunner): Promise<string> {
|
|
13
|
+
const head = decode(await runGit(["rev-parse", "--revs-only", "--end-of-options", "HEAD"])).trim();
|
|
14
|
+
if (head && !/^[0-9a-f]{40}(?:[0-9a-f]{24})?$/u.test(head)) throw new Error("invalid HEAD");
|
|
15
|
+
return head || "(initial)";
|
|
16
|
+
}
|
|
17
|
+
|
|
11
18
|
export type PassiveWorktreeFile = {
|
|
12
19
|
path: string;
|
|
13
20
|
headMode?: string;
|
|
@@ -343,19 +350,17 @@ function matchesIndex(content: Buffer, entry: IndexEntry, autoCrlf: string): boo
|
|
|
343
350
|
* No Git command in this path asks Git to convert worktree content, so clean and process filters cannot start.
|
|
344
351
|
*/
|
|
345
352
|
export async function inspectWorktreeWithoutFilters(root: string, runGit: PassiveGitRunner): Promise<PassiveWorktreeSnapshot> {
|
|
346
|
-
const [indexOutput, untrackedOutput,
|
|
353
|
+
const [indexOutput, untrackedOutput, head, autoCrlfOutput, settings] = await Promise.all([
|
|
347
354
|
runGit([...PASSIVE_METADATA_ARGS, "ls-files", "--cached", "--debug", "-z", "--full-name",
|
|
348
355
|
"--format=%(objectmode) %(objectname) %(stage)%x09%(eolattr)%x09%(path)"]),
|
|
349
356
|
runGit([...PASSIVE_METADATA_ARGS, "ls-files", "--others", "--exclude-standard", "-z", "--full-name"]),
|
|
350
|
-
runGit
|
|
357
|
+
resolvePassiveHead(runGit),
|
|
351
358
|
runGit(["config", "--type=bool-or-str", "--default", "false", "--get", "core.autocrlf"]),
|
|
352
359
|
passiveWorktreeSettings(runGit),
|
|
353
360
|
]);
|
|
354
361
|
const autoCrlf = decode(autoCrlfOutput).trim().toLowerCase();
|
|
355
|
-
const head = resolvedHead.trim();
|
|
356
|
-
if (head && !/^[0-9a-f]{40}(?:[0-9a-f]{24})?$/u.test(head)) throw new Error("invalid HEAD");
|
|
357
362
|
const index = parseIndex(indexOutput);
|
|
358
|
-
const tree = head ? parseTree(await runGit(["ls-tree", "-r", "-z", "--full-tree", head])) : new Map<string, TreeEntry>();
|
|
363
|
+
const tree = head !== "(initial)" ? parseTree(await runGit(["ls-tree", "-r", "-z", "--full-tree", head])) : new Map<string, TreeEntry>();
|
|
359
364
|
const files = new Map<string, PassiveWorktreeFile>();
|
|
360
365
|
let contentBytes = 0;
|
|
361
366
|
|
|
@@ -398,5 +403,5 @@ export async function inspectWorktreeWithoutFilters(root: string, runGit: Passiv
|
|
|
398
403
|
const previous = files.get(filePath);
|
|
399
404
|
files.set(filePath, { ...previous, path: filePath, mode: state.mode, content: state.content, contentObjectId: undefined });
|
|
400
405
|
}
|
|
401
|
-
return { head
|
|
406
|
+
return { head, files, ...settings };
|
|
402
407
|
}
|
package/killeros/runtime.ts
CHANGED
|
@@ -124,7 +124,7 @@ export interface GoalTurnExecution {
|
|
|
124
124
|
export interface GoalRuntime {
|
|
125
125
|
state?: GoalState;
|
|
126
126
|
continuationScheduled: boolean;
|
|
127
|
-
continuationHeld
|
|
127
|
+
continuationHeld?: symbol;
|
|
128
128
|
goalTurnInFlight: boolean;
|
|
129
129
|
agentEndObserved: boolean;
|
|
130
130
|
goalTurn?: GoalTurnExecution;
|
|
@@ -139,7 +139,7 @@ export interface GoalRuntime {
|
|
|
139
139
|
export function createGoalRuntime(): GoalRuntime {
|
|
140
140
|
return {
|
|
141
141
|
continuationScheduled: false,
|
|
142
|
-
continuationHeld:
|
|
142
|
+
continuationHeld: undefined,
|
|
143
143
|
goalTurnInFlight: false,
|
|
144
144
|
agentEndObserved: false,
|
|
145
145
|
goalTurn: undefined,
|
|
@@ -10,7 +10,7 @@ const SECRET_PATTERNS = [
|
|
|
10
10
|
/\bsk_live_[A-Za-z0-9]{16,255}\b/u,
|
|
11
11
|
/\bxox[baprs]-[A-Za-z0-9-]{10,255}\b/u,
|
|
12
12
|
/\b[a-z][a-z0-9+.-]*:\/\/[^/\s:@]+:[^/\s@]+@/iu,
|
|
13
|
-
/^[\t ]*["']?[\w.-]*(?:api_key|apikey|password|passwd|secret|token)[\w.-]*["']?[\t ]*(?:=|:)[\t ]*(?:"[^"\r\n]+"|'[^'\r\n]+'|[^\s#][^\r\n]*)/imu,
|
|
13
|
+
/^[\t ]*(?:(?:[-*+]|\d+[.)])[\t ]+)?(?:\*\*|`)?["']?[\w.-]*(?:api_key|apikey|password|passwd|secret|token)[\w.-]*["']?(?:\*\*|`)?[\t ]*(?:=|:)[\t ]*(?:"[^"\r\n]+"|'[^'\r\n]+'|[^\s#][^\r\n]*)/imu,
|
|
14
14
|
] as const;
|
|
15
15
|
|
|
16
16
|
/** Detects high-confidence credential material without returning the matched value. */
|
package/killeros/settings.ts
CHANGED
|
@@ -5,10 +5,13 @@ import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
|
5
5
|
import { hasErrorCode } from "./errors.ts";
|
|
6
6
|
|
|
7
7
|
export type KillerosSettings = Record<string, unknown>;
|
|
8
|
+
export type KillerosSettingsUpdate = Readonly<Record<string, unknown>>
|
|
9
|
+
| ((current: Readonly<KillerosSettings>) => Readonly<Record<string, unknown>>);
|
|
8
10
|
|
|
9
11
|
export interface KillerosSettingsStore {
|
|
10
12
|
load(): KillerosSettings;
|
|
11
|
-
update
|
|
13
|
+
/** Derive a field update from the latest settings while holding the writer lock. */
|
|
14
|
+
update(patch: KillerosSettingsUpdate): void;
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
function isSettings(value: unknown): value is KillerosSettings {
|
|
@@ -123,9 +126,10 @@ export function createKillerosSettingsStore(
|
|
|
123
126
|
const temporaryPath = `${settingsPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
124
127
|
try {
|
|
125
128
|
const current = readStoredSettings(settingsPath);
|
|
129
|
+
const changes = typeof patch === "function" ? patch(current) : patch;
|
|
126
130
|
writeFileSync(
|
|
127
131
|
temporaryPath,
|
|
128
|
-
`${JSON.stringify({ ...current, ...
|
|
132
|
+
`${JSON.stringify({ ...current, ...changes }, null, 2)}\n`,
|
|
129
133
|
{ encoding: "utf8", mode: 0o600 },
|
|
130
134
|
);
|
|
131
135
|
renameSync(temporaryPath, settingsPath);
|
package/killeros/worked-for.ts
CHANGED
|
@@ -302,6 +302,7 @@ export function workedForOutcome(stopReason: StopReason | undefined): WorkedForO
|
|
|
302
302
|
}
|
|
303
303
|
|
|
304
304
|
type ActiveReceipt = {
|
|
305
|
+
leafId: string | null;
|
|
305
306
|
startedAt: number;
|
|
306
307
|
startedTokens: number | undefined;
|
|
307
308
|
stopReason: StopReason | undefined;
|
|
@@ -369,17 +370,20 @@ export function registerWorkedFor(
|
|
|
369
370
|
return new Text(`${headline}${theme.fg("dim", ` · ${formatWorkedForDuration(data.milliseconds)}${tokens}`)}`, 1, 0);
|
|
370
371
|
});
|
|
371
372
|
|
|
372
|
-
|
|
373
|
+
const resetForBranch = async (): Promise<void> => {
|
|
373
374
|
lifecycleGeneration += 1;
|
|
374
375
|
const stale = active;
|
|
375
376
|
active = undefined;
|
|
376
377
|
collectionNoticeShown = false;
|
|
377
378
|
if (stale) await (await stale.collection).dispose();
|
|
378
|
-
}
|
|
379
|
+
};
|
|
380
|
+
pi.on("session_start", resetForBranch);
|
|
381
|
+
pi.on("session_tree", resetForBranch);
|
|
379
382
|
|
|
380
383
|
pi.on("agent_start", async (_event, ctx) => {
|
|
381
384
|
if (ctx.mode !== "tui" || active || !ctx.isProjectTrusted()) return;
|
|
382
385
|
const state: ActiveReceipt = {
|
|
386
|
+
leafId: ctx.sessionManager.getLeafId(),
|
|
383
387
|
startedAt: now(),
|
|
384
388
|
startedTokens: sessionTokenTotal(ctx),
|
|
385
389
|
stopReason: undefined,
|
|
@@ -440,6 +444,8 @@ export function registerWorkedFor(
|
|
|
440
444
|
|
|
441
445
|
pi.on("agent_end", (event, ctx) => {
|
|
442
446
|
if (ctx.mode !== "tui" || !active) return;
|
|
447
|
+
// Pi is not idle yet; later settlement handlers can overlap tree navigation.
|
|
448
|
+
active.leafId = ctx.sessionManager.getLeafId();
|
|
443
449
|
for (let index = event.messages.length - 1; index >= 0; index -= 1) {
|
|
444
450
|
const message = event.messages[index];
|
|
445
451
|
if (message?.role !== "assistant") continue;
|
|
@@ -455,8 +461,9 @@ export function registerWorkedFor(
|
|
|
455
461
|
active = undefined;
|
|
456
462
|
const model = receiptModelName(settled, ctx.model);
|
|
457
463
|
const changes = await (await settled.collection).finish();
|
|
458
|
-
//
|
|
459
|
-
if (generation !== lifecycleGeneration
|
|
464
|
+
// Earlier async session_tree handlers can delay our reset after Pi switches branches.
|
|
465
|
+
if (generation !== lifecycleGeneration
|
|
466
|
+
|| !ctx.sessionManager.getBranch().some((entry) => entry.id === settled.leafId)) return;
|
|
460
467
|
if (changes.state === "unavailable" && changes.reason !== "not-git" && changes.reason !== "timeout" && !collectionNoticeShown) {
|
|
461
468
|
collectionNoticeShown = true;
|
|
462
469
|
ctx.ui.notify(`Change receipt unavailable: ${changes.reason}`, "warning");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "killeros",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.2",
|
|
4
4
|
"description": "TUI, goals, and workflow automation for the Pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -45,16 +45,16 @@
|
|
|
45
45
|
]
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
|
-
"@earendil-works/pi-ai": ">=0.
|
|
49
|
-
"@earendil-works/pi-coding-agent": ">=0.
|
|
50
|
-
"@earendil-works/pi-tui": ">=0.
|
|
48
|
+
"@earendil-works/pi-ai": ">=1.0.2 <2",
|
|
49
|
+
"@earendil-works/pi-coding-agent": ">=1.0.2 <2",
|
|
50
|
+
"@earendil-works/pi-tui": ">=1.0.2 <2",
|
|
51
51
|
"typebox": ">=1.1.38 <2"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
-
"@earendil-works/pi-ai": "0.
|
|
55
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
56
|
-
"@earendil-works/pi-server": "0.
|
|
57
|
-
"@earendil-works/pi-tui": "0.
|
|
54
|
+
"@earendil-works/pi-ai": "1.0.2",
|
|
55
|
+
"@earendil-works/pi-coding-agent": "1.0.2",
|
|
56
|
+
"@earendil-works/pi-server": "1.0.2",
|
|
57
|
+
"@earendil-works/pi-tui": "1.0.2",
|
|
58
58
|
"@types/node": "24.12.4",
|
|
59
59
|
"eslint": "^10.9.1",
|
|
60
60
|
"typebox": "1.3.20",
|