@llblab/pi-kit 0.22.1 → 0.23.0

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.
Files changed (87) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +4 -4
  3. package/node_modules/@llblab/pi-grow-loop/AGENTS.md +10 -6
  4. package/node_modules/@llblab/pi-grow-loop/CHANGELOG.md +7 -0
  5. package/node_modules/@llblab/pi-grow-loop/README.md +2 -0
  6. package/node_modules/@llblab/pi-grow-loop/dist/index.d.ts +33 -0
  7. package/node_modules/@llblab/pi-grow-loop/dist/index.js +286 -0
  8. package/node_modules/@llblab/pi-grow-loop/dist/pi-grow-loop/index.d.ts +1 -0
  9. package/node_modules/@llblab/pi-grow-loop/dist/pi-grow-loop/index.js +1 -0
  10. package/node_modules/@llblab/pi-grow-loop/dist/skills/grow-loop/SKILL.md +117 -0
  11. package/node_modules/@llblab/pi-grow-loop/dist/skills/while-true/SKILL.md +233 -0
  12. package/node_modules/@llblab/pi-grow-loop/index.ts +67 -12
  13. package/node_modules/@llblab/pi-grow-loop/package.json +9 -8
  14. package/node_modules/@llblab/pi-state-flow/AGENTS.md +23 -17
  15. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +5 -3
  16. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +11 -0
  17. package/node_modules/@llblab/pi-state-flow/README.md +18 -6
  18. package/node_modules/@llblab/pi-state-flow/dist/index.d.ts +2 -1
  19. package/node_modules/@llblab/pi-state-flow/dist/index.js +1 -0
  20. package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.d.ts +2 -2
  21. package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.js +5 -5
  22. package/node_modules/@llblab/pi-state-flow/dist/lib/context.d.ts +3 -2
  23. package/node_modules/@llblab/pi-state-flow/dist/lib/context.js +7 -2
  24. package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.d.ts +5 -5
  25. package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.js +54 -40
  26. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +20 -20
  27. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +755 -325
  28. package/node_modules/@llblab/pi-state-flow/dist/lib/git.d.ts +2 -2
  29. package/node_modules/@llblab/pi-state-flow/dist/lib/git.js +14 -17
  30. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.d.ts +4 -0
  31. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +65 -23
  32. package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.d.ts +16 -5
  33. package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.js +32 -15
  34. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +28 -2
  35. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +276 -26
  36. package/node_modules/@llblab/pi-state-flow/dist/lib/session.d.ts +5 -0
  37. package/node_modules/@llblab/pi-state-flow/dist/lib/session.js +36 -2
  38. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +3 -0
  39. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +3 -0
  40. package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +1 -0
  41. package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +3 -1
  42. package/node_modules/@llblab/pi-state-flow/dist/lib/storage.d.ts +11 -0
  43. package/node_modules/@llblab/pi-state-flow/dist/lib/storage.js +150 -24
  44. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.d.ts +14 -1
  45. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +52 -18
  46. package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +8 -8
  47. package/node_modules/@llblab/pi-state-flow/dist/package.json +1 -1
  48. package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-guide/SKILL.md +3 -1
  49. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +53 -7
  50. package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +84 -44
  51. package/node_modules/@llblab/pi-state-flow/docs/filesystem-recovery.md +12 -2
  52. package/node_modules/@llblab/pi-state-flow/docs/fork-contract.md +6 -4
  53. package/node_modules/@llblab/pi-state-flow/docs/performance.md +2 -2
  54. package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +28 -8
  55. package/node_modules/@llblab/pi-state-flow/docs/usage.md +41 -14
  56. package/node_modules/@llblab/pi-state-flow/index.ts +3 -0
  57. package/node_modules/@llblab/pi-state-flow/lib/artifact.ts +5 -5
  58. package/node_modules/@llblab/pi-state-flow/lib/context.ts +8 -3
  59. package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +57 -40
  60. package/node_modules/@llblab/pi-state-flow/lib/durable.ts +20 -20
  61. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +719 -316
  62. package/node_modules/@llblab/pi-state-flow/lib/git.ts +16 -18
  63. package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +60 -24
  64. package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +34 -21
  65. package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +290 -25
  66. package/node_modules/@llblab/pi-state-flow/lib/session.ts +37 -2
  67. package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +3 -0
  68. package/node_modules/@llblab/pi-state-flow/lib/status.ts +4 -1
  69. package/node_modules/@llblab/pi-state-flow/lib/storage.ts +141 -22
  70. package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +60 -19
  71. package/node_modules/@llblab/pi-state-flow/lib/transition.ts +8 -8
  72. package/node_modules/@llblab/pi-state-flow/package.json +1 -1
  73. package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +3 -1
  74. package/node_modules/@llblab/pi-telegram/AGENTS.md +3 -2
  75. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +10 -0
  76. package/node_modules/@llblab/pi-telegram/README.md +3 -1
  77. package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +22 -3
  78. package/node_modules/@llblab/pi-telegram/dist/lib/skills.d.ts +8 -2
  79. package/node_modules/@llblab/pi-telegram/dist/lib/skills.js +36 -4
  80. package/node_modules/@llblab/pi-telegram/dist/package.json +3 -8
  81. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +1 -1
  82. package/node_modules/@llblab/pi-telegram/docs/public-api.md +1 -1
  83. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +27 -3
  84. package/node_modules/@llblab/pi-telegram/lib/skills.ts +49 -5
  85. package/node_modules/@llblab/pi-telegram/package.json +3 -8
  86. package/node_modules/@llblab/pi-telegram/scripts/build-dist.mjs +103 -32
  87. package/package.json +6 -6
package/CHANGELOG.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  All notable changes to `@llblab/pi-kit` are documented here.
4
4
 
5
+ ## 0.23.0 - 2026-09-24
6
+
7
+ - `Awaited State Flow Memory`: Pins State Flow to `0.19.0`. Shared patches preserve independent changes, repeats avoid extra revisions, and state acceptance precedes the next provider request. Start/Stop select workflow policy without cancelling restoration or fork copying; unfinished context and private session ownership survive mode changes. Library callers must await inspection results; six synchronous runtime methods remain supported.
8
+ - `Compiled Grow Loop Resources`: Pins Grow Loop to `0.8.2` and loads its declared `dist/pi-grow-loop/index.js` and `dist/skills` resources. Manifest-owned Skill discovery preserves package filters and provenance, while the package supplies drift-checked compiled output.
9
+ - `Resolver-Owned Telegram Skills`: Pins Telegram to `0.51.4`. Manifest-loaded kit resources retain Skill filters and provenance without duplicate checkout discovery; the owning package supplies a committed, drift-checked runtime and Skill distribution.
10
+ - `Package Cohort`: Preserves all seven package members, exact pins for the other four packages, extension load order and the Pi minimum. Runtime behavior and Skill ownership remain with their independently released packages.
11
+
12
+ ## 0.22.2 - 2026-09-24
13
+
14
+ - `Follower Registration Recovery`: Advances the exact Telegram pin to `0.51.3`. Followers whose retained target record contains a stale Workspace slot now reconcile to the authenticated canonical claim before binding commit, durably repairing their record without disturbing an unrelated binding that owns the old letter or making unnecessary Bot API calls.
15
+ - `Filterable Packaged Skills`: Installed npm/git packages now leave Telegram Skill discovery to the manifest so Pi resource filters are honored. Raw TypeScript checkouts under Pi's extensions directory retain adjacent source-Skill discovery without creating a duplicate packaged discovery path.
16
+ - `Package Cohort`: Keeps every other bundled package at its current exact version. Package membership, resource paths, load order, Pi minimum, and bundled Skill inventory remain unchanged.
17
+
5
18
  ## 0.22.1 - 2026-09-24
6
19
 
7
20
  - `In-Flight Model Switching`: Advances the exact Telegram pin to `0.51.2`. Telegram model selection can stop, switch, and continue any interruptible run in the current Pi session, including local/TUI work, while preserving the authorized chat, Thread, and reply target and deferring abort until active tools settle.
package/README.md CHANGED
@@ -13,16 +13,16 @@ Package links lead to the owning repositories for usage, documentation, issues,
13
13
  | [`@llblab/pi-actors`](https://github.com/llblab/pi-actors) | `0.53.0` | Inspectable local Runs, reusable Recipes, persistent tools, and delegation Skills |
14
14
  | [`@llblab/pi-clean-room`](https://github.com/llblab/pi-clean-room) | `0.2.0` | Isolated nested Pi TUI with named npm extensions and compatible model selection |
15
15
  | [`@llblab/pi-codex-usage`](https://github.com/llblab/pi-codex-usage) | `0.10.0` | Compact Codex/Spark subscription-limit and Business credit-usage status |
16
- | [`@llblab/pi-grow-loop`](https://github.com/llblab/pi-grow-loop) | `0.8.1` | Visible continuation scheduling and bounded worker Skills |
17
- | [`@llblab/pi-state-flow`](https://github.com/llblab/pi-state-flow) | `0.18.1` | Incremental scoped context/memory compiler with independent scope revisions, canonical file persistence, bounded and diagnosable Git backup replication, native working context within each run, and targeted historical reads |
18
- | [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.51.2` | Telegram companion with in-flight model switching, continuous exact-Thread typing, truthful transport status, pressure-safe Workspace rotation, files, voice, controls, and bundled Telegram interaction Skills |
16
+ | [`@llblab/pi-grow-loop`](https://github.com/llblab/pi-grow-loop) | `0.8.2` | Visible continuation scheduling and bounded worker Skills through compiled, manifest-owned resources |
17
+ | [`@llblab/pi-state-flow`](https://github.com/llblab/pi-state-flow) | `0.19.0` | Incremental scoped context/memory compiler with awaited publication, independent scope revisions, lossless mode changes, private fork memory, and optional Git backup |
18
+ | [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.51.4` | Telegram companion with resolver-owned filterable Skills, compiled distribution, self-healing follower registration, in-flight model switching, files, voice, and controls |
19
19
  | [`@llblab/skills`](https://github.com/llblab/skills) | `1.15.0` | Portable workflows for engineering, review, design, context maintenance, and other focused tasks |
20
20
 
21
21
  Versions are exact by design. An upstream release does not change an installed kit until this repository explicitly advances the dependency and publishes a new kit version. Runtime defects and package-specific feature requests belong in the linked repository; package selection and kit installation issues belong here.
22
22
 
23
23
  ## Install
24
24
 
25
- Requires **Pi 0.87.0+** and **Node.js 22.19.0+**. State Flow 0.17 introduces a breaking storage-format boundary with no in-place predecessor converter. Preserve existing State Flow stores and review the [owning package's storage guidance](https://github.com/llblab/pi-state-flow/blob/v0.18.1/docs/usage.md#moving-a-store-and-the-017-format-boundary) before upgrading from an earlier kit.
25
+ Requires **Pi 0.87.0+** and **Node.js 22.19.0+**. State Flow requires its canonical checkpoint/tail storage format and does not convert unsupported stores in place. Preserve existing stores and consult the [owning package's storage guidance](https://github.com/llblab/pi-state-flow/blob/v0.19.0/docs/usage.md#moving-a-store-and-the-017-format-boundary) before changing installations.
26
26
 
27
27
  From npm:
28
28
 
@@ -33,12 +33,14 @@
33
33
  ## Topology
34
34
 
35
35
  ```text
36
- index.ts optionally delayed grow_loop tool
37
- skills/grow-loop/SKILL.md meta-loop protocol
38
- skills/while-true/SKILL.md worker-loop protocol baseline
39
- README.md human entrypoint
40
- BACKLOG.md open work
41
- CHANGELOG.md completed delivery history
36
+ index.ts source-checkout composition root
37
+ dist/pi-grow-loop/index.js packaged compiled entrypoint
38
+ dist/skills packaged Skill root
39
+ skills/grow-loop/SKILL.md meta-loop protocol
40
+ skills/while-true/SKILL.md worker-loop protocol baseline
41
+ README.md human entrypoint
42
+ BACKLOG.md open work
43
+ CHANGELOG.md completed delivery history
42
44
  ```
43
45
 
44
46
  ## Evolution Path
@@ -51,6 +53,8 @@ CHANGELOG.md completed delivery history
51
53
 
52
54
  ## Style
53
55
 
56
+ - Manifest-loaded packages expose bundled Skills only through `pi.skills`, preserving package filters and package-owned provenance. Only a checkout auto-discovered directly below a user or project Pi `extensions/` root may contribute its source Skill root through `resources_discover`; compiled-vs-source filename shape does not determine ownership.
57
+ - Keep `dist/` synchronized and committed after source changes because Pi git-package installation does not run the npm `prepack` lifecycle. Run `npm run build` to atomically replace it; validation uses `build:check` to reject drift without rewriting the working tree, while npm publication rebuilds the same tree.
54
58
  - Prefer small, inspectable TypeScript over framework or actor dependencies.
55
59
  - Use concise operator copy.
56
60
  - Keep prompt contracts explicit and bounded.
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ## 0.8.2: Filterable Skills and drift-safe Git installs
6
+
7
+ - `Filterable Packaged Skills`: Manifest-loaded installs leave both bundled Skills to `pi.skills`, preserving filters and package provenance. Auto-discovered checkouts contribute source Skills even when Pi selects compiled code; filename shape no longer misclassifies ownership, and unsupported source manifest aliases are removed.
8
+ - `Drift-Safe Distribution`: Ships the compiled entrypoint and Skill tree for npm/git installs, builds through a temporary candidate plus rollback-safe swap, and makes validation reject any committed `dist` drift without silently repairing it. Package filters that named source paths must target the new `dist/pi-grow-loop/index.js` and `dist/skills` paths.
9
+
3
10
  ## 0.8.1: Telegram Status Text Hotfix
4
11
 
5
12
  - `Status Text`: The Telegram Status screen now copies the compact terminal identity exactly: `Grow Loop: ∞N` for the armed or running iteration, without countdown, phase, or separator text. The row still hides whenever the terminal status is hidden.
@@ -22,6 +22,8 @@ Or install from git:
22
22
  pi install git:github.com/llblab/pi-grow-loop
23
23
  ```
24
24
 
25
+ Manifest-loaded packages load the compiled extension and bundled Skills through the package manifest, so Pi resource filters and package provenance remain authoritative. A checkout auto-discovered directly under Pi's user or project extensions directory contributes its source Skills for local development even when Pi selects the checkout's compiled entrypoint. Existing package filters that name resource paths should use `dist/pi-grow-loop/index.js` and `dist/skills`; filters by Skill name remain unchanged.
26
+
25
27
  Then focus Pi on trustworthy open work or provide a concrete multi-slice outcome and ask for continuation:
26
28
 
27
29
  ```text
@@ -0,0 +1,33 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ export interface GrowLoopTelegramProgress {
3
+ iteration: number;
4
+ state: "waiting" | "countdown" | "running";
5
+ remainingSeconds?: number;
6
+ }
7
+ export interface GrowLoopTelegramStatusLine {
8
+ label: string;
9
+ value: string;
10
+ }
11
+ export type GrowLoopTelegramStatusProvider = () => GrowLoopTelegramStatusLine | undefined;
12
+ export type GrowLoopTelegramStatusRegistrar = (provider: GrowLoopTelegramStatusProvider) => (() => void) | undefined;
13
+ type GrowLoopOptions = {
14
+ followUpDelayMs?: number;
15
+ countdownTickMs?: number;
16
+ /** Injection seam for the optional pi-telegram status line; defaults to the public pi-telegram membrane. */
17
+ registerTelegramStatusLine?: GrowLoopTelegramStatusRegistrar;
18
+ };
19
+ export declare function buildGrowLoopPrompt(): string;
20
+ export declare function getAgentDir(env?: Record<string, string | undefined>): string;
21
+ export declare function getExtensionPackageRoot(extensionUrl: string): string;
22
+ export interface RawExtensionCheckoutOptions {
23
+ agentDir?: string;
24
+ cwd?: string;
25
+ }
26
+ export declare function isRawExtensionCheckout(extensionUrl: string, options?: RawExtensionCheckoutOptions): boolean;
27
+ export declare function getExtensionSkillsDir(extensionUrl: string): string;
28
+ export declare function getExistingExtensionSkillPaths(extensionUrl: string): string[];
29
+ export declare function getTelegramStatusImportSpecifiers(extensionUrl: string): string[];
30
+ export declare function registerGrowLoopSkillDiscovery(pi: Pick<ExtensionAPI, "on">, extensionUrl?: string, options?: RawExtensionCheckoutOptions): boolean;
31
+ export declare function formatGrowLoopTelegramValue(progress: GrowLoopTelegramProgress): string;
32
+ export default function growLoopExtension(pi: ExtensionAPI, partialOptions?: GrowLoopOptions): void;
33
+ export {};
@@ -0,0 +1,286 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ import { existsSync } from "node:fs";
10
+ import { homedir } from "node:os";
11
+ import { dirname, join, resolve } from "node:path";
12
+ import { fileURLToPath, pathToFileURL } from "node:url";
13
+ import { Type } from "typebox";
14
+ const STATUS_KEY = "pi-grow-loop";
15
+ const STATUS_LABEL = "grow-loop";
16
+ const DEFAULT_FOLLOW_UP_DELAY_MS = 3000;
17
+ const DEFAULT_COUNTDOWN_TICK_MS = 100;
18
+ const MIN_AFTER_SECONDS = 3;
19
+ const MAX_AFTER_SECONDS = 3600;
20
+ const TELEGRAM_STATUS_IMPORT_SPECIFIERS = getTelegramStatusImportSpecifiers(import.meta.url);
21
+ export function buildGrowLoopPrompt() {
22
+ return "while true | grow loop";
23
+ }
24
+ export function getAgentDir(env = process.env) {
25
+ return env.PI_CODING_AGENT_DIR
26
+ ? resolve(env.PI_CODING_AGENT_DIR)
27
+ : join(homedir(), ".pi", "agent");
28
+ }
29
+ export function getExtensionPackageRoot(extensionUrl) {
30
+ let current = dirname(fileURLToPath(extensionUrl));
31
+ while (true) {
32
+ if (existsSync(join(current, "package.json")))
33
+ return current;
34
+ const parent = dirname(current);
35
+ if (parent === current)
36
+ return dirname(fileURLToPath(extensionUrl));
37
+ current = parent;
38
+ }
39
+ }
40
+ export function isRawExtensionCheckout(extensionUrl, options = {}) {
41
+ const packageRoot = resolve(getExtensionPackageRoot(extensionUrl));
42
+ const agentDir = resolve(options.agentDir ?? getAgentDir());
43
+ const cwd = resolve(options.cwd ?? process.cwd());
44
+ return dirname(packageRoot) === join(agentDir, "extensions") ||
45
+ dirname(packageRoot) === join(cwd, ".pi", "extensions");
46
+ }
47
+ export function getExtensionSkillsDir(extensionUrl) {
48
+ return join(getExtensionPackageRoot(extensionUrl), "skills");
49
+ }
50
+ export function getExistingExtensionSkillPaths(extensionUrl) {
51
+ const skillsDir = getExtensionSkillsDir(extensionUrl);
52
+ return existsSync(skillsDir) ? [skillsDir] : [];
53
+ }
54
+ export function getTelegramStatusImportSpecifiers(extensionUrl) {
55
+ const siblingPath = join(dirname(getExtensionPackageRoot(extensionUrl)), "pi-telegram", "api", "status.ts");
56
+ return [
57
+ "@llblab/pi-telegram/status",
58
+ pathToFileURL(siblingPath).href,
59
+ ];
60
+ }
61
+ export function registerGrowLoopSkillDiscovery(pi, extensionUrl = import.meta.url, options = {}) {
62
+ if (!isRawExtensionCheckout(extensionUrl, options))
63
+ return false;
64
+ pi.on("resources_discover", async () => {
65
+ const skillPaths = getExistingExtensionSkillPaths(extensionUrl);
66
+ if (skillPaths.length === 0)
67
+ return;
68
+ return { skillPaths };
69
+ });
70
+ return true;
71
+ }
72
+ export function formatGrowLoopTelegramValue(progress) {
73
+ return `∞${progress.iteration}`;
74
+ }
75
+ async function registerGrowLoopTelegramStatus(provider) {
76
+ for (const specifier of TELEGRAM_STATUS_IMPORT_SPECIFIERS) {
77
+ try {
78
+ const imported = (await import(__rewriteRelativeImportExtension(specifier)));
79
+ if (typeof imported.registerTelegramStatusLineProvider === "function") {
80
+ return imported.registerTelegramStatusLineProvider(provider, { id: "@llblab/pi-grow-loop" });
81
+ }
82
+ }
83
+ catch {
84
+ // pi-telegram is optional; its absence only disables the Telegram status line.
85
+ }
86
+ }
87
+ return undefined;
88
+ }
89
+ function statusCountdown(ctx, seconds) {
90
+ const theme = ctx.ui.theme;
91
+ ctx.ui.setStatus(STATUS_KEY, theme.fg("accent", STATUS_LABEL) +
92
+ theme.fg("dim", ` ${seconds.toFixed(1)}s`));
93
+ }
94
+ function statusRunning(ctx, iteration) {
95
+ const theme = ctx.ui.theme;
96
+ ctx.ui.setStatus(STATUS_KEY, theme.fg("accent", STATUS_LABEL) + theme.fg("dim", ` ∞${iteration}`));
97
+ }
98
+ function statusDeferred(ctx, iteration) {
99
+ const theme = ctx.ui.theme;
100
+ ctx.ui.setStatus(STATUS_KEY, theme.fg("accent", STATUS_LABEL) +
101
+ theme.fg("warning", ` ∞${iteration}`));
102
+ }
103
+ function sendIteration(pi, ctx, iteration, expectOwnPrompt) {
104
+ statusRunning(ctx, iteration);
105
+ expectOwnPrompt();
106
+ pi.sendUserMessage(buildGrowLoopPrompt());
107
+ }
108
+ function scheduleIteration(pi, ctx, iteration, clearPending, expectOwnPrompt, options) {
109
+ statusDeferred(ctx, iteration);
110
+ const pending = {};
111
+ pending.interval = setInterval(() => {
112
+ if (pending.countdownStartedAt === undefined) {
113
+ if (!ctx.isIdle() || ctx.hasPendingMessages())
114
+ return;
115
+ pending.countdownStartedAt = Date.now();
116
+ pending.countdownDelayMs = options.followUpDelayMs;
117
+ statusCountdown(ctx, options.followUpDelayMs / 1000);
118
+ pending.timeout = setTimeout(() => {
119
+ pending.timeout = undefined;
120
+ if (!ctx.isIdle() || ctx.hasPendingMessages()) {
121
+ pending.countdownStartedAt = undefined;
122
+ pending.countdownDelayMs = undefined;
123
+ statusDeferred(ctx, iteration);
124
+ return;
125
+ }
126
+ clearPending();
127
+ sendIteration(pi, ctx, iteration, expectOwnPrompt);
128
+ }, options.followUpDelayMs);
129
+ pending.timeout.unref?.();
130
+ return;
131
+ }
132
+ const elapsed = Date.now() - pending.countdownStartedAt;
133
+ const remainingMs = Math.max(options.followUpDelayMs - elapsed, 0);
134
+ if (remainingMs > 0)
135
+ statusCountdown(ctx, remainingMs / 1000);
136
+ }, options.countdownTickMs);
137
+ pending.interval.unref?.();
138
+ return pending;
139
+ }
140
+ export default function growLoopExtension(pi, partialOptions = {}) {
141
+ const options = {
142
+ followUpDelayMs: partialOptions.followUpDelayMs ?? DEFAULT_FOLLOW_UP_DELAY_MS,
143
+ countdownTickMs: partialOptions.countdownTickMs ?? DEFAULT_COUNTDOWN_TICK_MS,
144
+ registerTelegramStatusLine: partialOptions.registerTelegramStatusLine,
145
+ };
146
+ let iteration = 0;
147
+ let lastCtx;
148
+ let pendingIteration;
149
+ let ownPromptPending = false;
150
+ let scheduledThisTurn = false;
151
+ let runningIteration;
152
+ let unregisterTelegramStatus;
153
+ let telegramRegistration;
154
+ let telegramGeneration = 0;
155
+ const clearPending = () => {
156
+ if (!pendingIteration)
157
+ return;
158
+ if (pendingIteration.timeout)
159
+ clearTimeout(pendingIteration.timeout);
160
+ clearInterval(pendingIteration.interval);
161
+ pendingIteration = undefined;
162
+ };
163
+ const telegramStatusProvider = () => {
164
+ if (pendingIteration) {
165
+ const state = pendingIteration.countdownStartedAt === undefined ? "waiting" : "countdown";
166
+ return { label: "Grow Loop", value: formatGrowLoopTelegramValue({ iteration, state }) };
167
+ }
168
+ if (runningIteration !== undefined) {
169
+ return { label: "Grow Loop", value: formatGrowLoopTelegramValue({ iteration: runningIteration, state: "running" }) };
170
+ }
171
+ return undefined;
172
+ };
173
+ const ensureTelegramStatusRegistered = () => {
174
+ if (unregisterTelegramStatus || telegramRegistration)
175
+ return;
176
+ if (options.registerTelegramStatusLine) {
177
+ unregisterTelegramStatus = options.registerTelegramStatusLine(telegramStatusProvider) ?? undefined;
178
+ return;
179
+ }
180
+ const generation = telegramGeneration;
181
+ telegramRegistration = registerGrowLoopTelegramStatus(telegramStatusProvider)
182
+ .then((unregister) => {
183
+ if (generation !== telegramGeneration) {
184
+ unregister?.();
185
+ return;
186
+ }
187
+ unregisterTelegramStatus = unregister;
188
+ })
189
+ .finally(() => {
190
+ if (generation === telegramGeneration)
191
+ telegramRegistration = undefined;
192
+ });
193
+ };
194
+ const hideLoopStatus = (ctx) => {
195
+ ownPromptPending = false;
196
+ runningIteration = undefined;
197
+ clearPending();
198
+ ctx.ui.setStatus(STATUS_KEY, undefined);
199
+ };
200
+ ensureTelegramStatusRegistered();
201
+ registerGrowLoopSkillDiscovery(pi);
202
+ pi.on("session_shutdown", async () => {
203
+ ownPromptPending = false;
204
+ scheduledThisTurn = false;
205
+ runningIteration = undefined;
206
+ clearPending();
207
+ telegramGeneration += 1;
208
+ unregisterTelegramStatus?.();
209
+ unregisterTelegramStatus = undefined;
210
+ telegramRegistration = undefined;
211
+ lastCtx?.ui.setStatus(STATUS_KEY, undefined);
212
+ });
213
+ pi.on("session_start", async () => {
214
+ ensureTelegramStatusRegistered();
215
+ });
216
+ pi.on("agent_settled", async (_event, ctx) => {
217
+ lastCtx = ctx;
218
+ if (!pendingIteration) {
219
+ runningIteration = undefined;
220
+ ctx.ui.setStatus(STATUS_KEY, undefined);
221
+ }
222
+ });
223
+ pi.on("input", async (event, ctx) => {
224
+ lastCtx = ctx;
225
+ scheduledThisTurn = false;
226
+ const isOwnPrompt = event.source === "extension" &&
227
+ ownPromptPending &&
228
+ event.text === buildGrowLoopPrompt();
229
+ if (isOwnPrompt) {
230
+ ownPromptPending = false;
231
+ return { action: "continue" };
232
+ }
233
+ hideLoopStatus(ctx);
234
+ return { action: "continue" };
235
+ });
236
+ pi.registerTool({
237
+ name: "grow_loop",
238
+ label: "Grow Loop",
239
+ description: "Schedule the next visible Grow Loop iteration after an optional delay in seconds (default: 3).",
240
+ promptSnippet: "Schedule the next Grow Loop iteration after an optional delay.",
241
+ promptGuidelines: [
242
+ "Use grow_loop when the Grow Loop skill decides another while-true iteration should run; omit after_seconds for the default 3-second delay.",
243
+ "Increase grow_loop after_seconds from 3 up to 3600 when continuation should wait for asynchronous work; never shorten the 3-second operator-interrupt window.",
244
+ "Choose grow_loop after_seconds from evidence about the expected remaining wait, then reassess after each wake instead of repeating the previous delay mechanically.",
245
+ "To stop, do not call grow_loop; finish with a concise stop proof.",
246
+ ],
247
+ parameters: Type.Object({
248
+ after_seconds: Type.Optional(Type.Number({
249
+ minimum: MIN_AFTER_SECONDS,
250
+ maximum: MAX_AFTER_SECONDS,
251
+ default: 3,
252
+ description: "Seconds to wait after Pi becomes idle before starting the next iteration (maximum: 3600)",
253
+ })),
254
+ }),
255
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
256
+ lastCtx = ctx;
257
+ ownPromptPending = false;
258
+ runningIteration = undefined;
259
+ clearPending();
260
+ const isReschedule = scheduledThisTurn;
261
+ if (!isReschedule) {
262
+ iteration += 1;
263
+ scheduledThisTurn = true;
264
+ }
265
+ const nextIteration = iteration;
266
+ const delayMs = params.after_seconds === undefined
267
+ ? options.followUpDelayMs
268
+ : params.after_seconds * 1000;
269
+ pendingIteration = scheduleIteration(pi, ctx, nextIteration, clearPending, () => {
270
+ ownPromptPending = true;
271
+ runningIteration = nextIteration;
272
+ }, { followUpDelayMs: delayMs, countdownTickMs: options.countdownTickMs });
273
+ return {
274
+ content: [
275
+ {
276
+ type: "text",
277
+ text: isReschedule
278
+ ? `\nTool grow_loop was already called this turn. Iteration #${nextIteration} remains scheduled; delay updated to ${delayMs / 1000}s`
279
+ : `\nGrow Loop iteration #${nextIteration} deferred until idle, then scheduled after ${delayMs / 1000}s delay`,
280
+ },
281
+ ],
282
+ details: { iteration: nextIteration, delayMs },
283
+ };
284
+ },
285
+ });
286
+ }
@@ -0,0 +1 @@
1
+ export { default } from "../index.js";
@@ -0,0 +1 @@
1
+ export { default } from "../index.js";
@@ -0,0 +1,117 @@
1
+ ---
2
+ name: grow-loop
3
+ description: Meta-protocol for autonomous, scope-locked continuation through visible bounded worker iterations. Use when the user explicitly names `grow-loop`, or when no protocol is named and a concrete scoped outcome benefits from multiple independently useful, validated slices with operator-visible continuation checkpoints. Existing plans are optional, and an explicit scoped outcome may bootstrap a canonical backlog. Do not activate for an explicit standalone `while-true` request, ordinary one-shot work with one natural validation boundary, informational answers, unrelated plans, or work with no safe actionable or preparable slice.
4
+ ---
5
+
6
+ # Grow Loop
7
+
8
+ Own one decision: after a bounded `while-true` worker invocation, either schedule exactly one next visible invocation with `grow_loop` or stop with proof.
9
+
10
+ ```text
11
+ lock scope → run one worker invocation → consume handoff → decide once
12
+ ├─ continue: call grow_loop once, then end the turn
13
+ └─ stop: do not call grow_loop; return proof
14
+ ```
15
+
16
+ Do not own implementation details. `while-true` owns one portable worker pass; `grow_loop` owns only idle-deferred runtime scheduling.
17
+
18
+ There is no goal object, budget, cycle count, hidden process, background agent, or slash-command control surface. For generic prompts such as `go`, `continue`, or `do it`, infer intent from context; explicit protocol names remain exact overrides.
19
+
20
+ ## Entrypoint Routing
21
+
22
+ Apply lexical intent before execution-shape inference:
23
+
24
+ - Explicit standalone `while-true` selects only the portable worker. Do not activate Grow Loop or call `grow_loop` from its handoff unless the user later requests Grow Loop continuation.
25
+ - Explicit `grow-loop` selects this meta-protocol.
26
+ - The combined `while true | grow loop` prompt is reserved for internal continuation of an already selected Grow Loop sequence.
27
+ - When no protocol is named, use ordinary one-shot execution for a coherent change even when it needs multiple internal steps. Use Grow Loop for a concrete outcome that benefits from multiple independently validated slices, plan reconciliation, and operator-visible checkpoints.
28
+
29
+ Natural routing into Grow Loop requires a concrete outcome or scope, a truthful backlog that exists or can be bootstrapped, safe actionable or preparable work, and no one-shot instruction. The distinction is the value of independent checkpoint boundaries, not task size, keyword matching, or a confirmation ritual. Routing happens before invoking `while-true`; the worker never escalates itself.
30
+
31
+ ## Scope Lock
32
+
33
+ Lock the project, directory, file, issue, task, or scoped outcome selected by the user. Conversation context may supply it only when it identifies the next safe slice precisely.
34
+
35
+ Pass that scope to `while-true`, which alone resolves and validates the canonical open-work surface, including any `Canonical open work:` declaration. Retain the surface returned in its handoff; do not run a second discovery algorithm here.
36
+
37
+ Ignore unrelated repositories, temporary or generated directories, dependencies, caches, archives, and stale plans.
38
+
39
+ Keep the selected scope and declared work-surface ownership stable across iterations. Re-select only when the user redirects the work or verified reality proves another surface governs the same scope. A stale or moved declaration requires repair or an explicit ambiguity stop, not harvesting other available work to preserve momentum.
40
+
41
+ If no trustworthy scope exists, do not invoke the worker or call `grow_loop`; request the smallest missing input.
42
+
43
+ ## Intent Precedence
44
+
45
+ Interpret the latest context in this order:
46
+
47
+ 1. Latest user direction or change of scope.
48
+ 2. Explicit continuation-break intent or durable stop marker.
49
+ 3. Worker evidence, safety gates, and blockers.
50
+ 4. Remaining backlog availability.
51
+
52
+ Any user prompt except the runtime's exact expected continuation prompt exits the runtime rhythm and is authoritative context, including operator input injected through another extension. Decide whether it means answer, stop, restart, continue, or change direction; do not infer continuation from backlog availability alone.
53
+
54
+ Escape remains baseline Pi behavior, not a Grow Loop control. Treat it as a continuation break only when session context exposes that intent or a durable stop marker.
55
+
56
+ If a queued `while true | grow loop` prompt arrives after continuation-break intent, do no repository work, run no validation for momentum, and do not call `grow_loop`. Acknowledge the break and provide the current stop proof when useful. Resume only after explicit restart intent clears the stop context.
57
+
58
+ ## Continuation Checkpoint
59
+
60
+ After one `while-true` invocation, consume its [Handoff](../while-true/SKILL.md#handoff) and checkpoint signature without redoing worker implementation analysis. Combine that evidence with the latest user intent; `while-true` owns the handoff fields, while this Skill owns the continuation decision.
61
+
62
+ A useful invocation must change an artifact, increase validation confidence, narrow a blocker, improve plan truth, or remove a risky assumption. The worker may batch independent low-coupling tasks into one validation cohort; Grow Loop evaluates the cohort handoff as one checkpoint and does not reinterpret its batching. Otherwise treat the invocation as a possible no-op.
63
+
64
+ Compare the checkpoint signature with the previous invocation. A repeated signature with only unchanged reads, checks, or blocker restatement is terminal no-op evidence.
65
+
66
+ ## Decide Once
67
+
68
+ ### Continue
69
+
70
+ Continue only when every condition holds:
71
+
72
+ - Latest user intent permits continuation and no stop marker is active.
73
+ - The locked scope and canonical work surface remain trustworthy.
74
+ - The previous invocation produced useful evidence.
75
+ - A high-value `local-actionable` or useful `gated-but-preparable` slice remains.
76
+ - Continuing crosses no destructive, publishing, credential, account, external, or approval gate.
77
+ - The checkpoint signature is not a repeated no-op.
78
+ - Validation has not regressed enough to require a strategy change or human decision.
79
+
80
+ Approval- or externally gated scopes may continue only through safe preparation that materially reduces future risk. Stop when preparation is exhausted; never cross the gate.
81
+
82
+ When all conditions hold, call `grow_loop` exactly once, then end the turn. Omit `after_seconds` for the default 3-second operator-interrupt delay, or increase it from 3 up to 3600 when continuation should wait for known asynchronous work. Never shorten the 3-second minimum because it preserves the operator's chance to redirect the next iteration. Choose a longer delay from concrete evidence about expected remaining duration, use the one-hour limit only for genuinely long-running work, and reassess after every wake so the next delay tracks the latest state rather than mechanically repeating the previous value. The tool waits until Pi is idle with no pending messages, shows the configured countdown, and sends the next visible `while true | grow loop` prompt only if the session remains idle.
83
+
84
+ ### Stop
85
+
86
+ Stop and do not call `grow_loop` when any condition holds:
87
+
88
+ - User intent means stop, answer, wait, or change direction.
89
+ - Scope is missing, ambiguous, redirected, or untrustworthy.
90
+ - Work is complete or no high-value actionable or preparable slice remains.
91
+ - Remaining work is gated and useful preparation is complete.
92
+ - The checkpoint signature repeats.
93
+ - Validation requires a strategy change or human decision.
94
+ - Continuing would be unsafe, destructive, speculative, or outside scope.
95
+
96
+ Stopping with exact evidence is progress. Do not schedule speculatively and do not call `grow_loop` more than once per decision.
97
+
98
+ ## Stop Proof
99
+
100
+ Return a compact terminal handoff:
101
+
102
+ - Locked scope and what was closed or narrowed.
103
+ - Validation or evidence proving the state.
104
+ - Terminal checkpoint signature in concise form.
105
+ - What remains done, gated, or non-actionable.
106
+ - Exact input or state change that would make restart useful, if any.
107
+
108
+ ## Invariants
109
+
110
+ 1. Scope remains locked until user intent or verified reality changes it.
111
+ 2. `while-true` owns worker execution; Grow Loop consumes its handoff and owns continuation only.
112
+ 3. User intent outranks repository availability.
113
+ 4. Each checkpoint produces one decision and at most one `grow_loop` call.
114
+ 5. Safe preparation may approach a gate but never cross it.
115
+ 6. Repeated no-op evidence stops the loop.
116
+ 7. No trustworthy scope or evidence means no continuation.
117
+ 8. Explicit protocol naming overrides automatic routing; the worker never self-escalates.