@llblab/pi-kit 0.22.2 → 0.23.1
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 +12 -0
- package/README.md +5 -5
- package/node_modules/@llblab/pi-actors/AGENTS.md +4 -1
- package/node_modules/@llblab/pi-actors/CHANGELOG.md +10 -0
- package/node_modules/@llblab/pi-actors/README.md +2 -2
- package/node_modules/@llblab/pi-actors/dist/index.js +4 -1
- package/node_modules/@llblab/pi-actors/dist/lib/inspector-overlay.js +2 -1
- package/node_modules/@llblab/pi-actors/dist/lib/paths.d.ts +6 -0
- package/node_modules/@llblab/pi-actors/dist/lib/paths.js +19 -1
- package/node_modules/@llblab/pi-actors/dist/lib/trace-projection.d.ts +2 -0
- package/node_modules/@llblab/pi-actors/dist/lib/trace-projection.js +11 -7
- package/node_modules/@llblab/pi-actors/dist/scripts/build-dist.mjs +94 -30
- package/node_modules/@llblab/pi-actors/docs/actor-inspector.md +1 -1
- package/node_modules/@llblab/pi-actors/index.ts +6 -3
- package/node_modules/@llblab/pi-actors/lib/inspector-overlay.ts +2 -1
- package/node_modules/@llblab/pi-actors/lib/paths.ts +27 -1
- package/node_modules/@llblab/pi-actors/lib/trace-projection.ts +13 -6
- package/node_modules/@llblab/pi-actors/package.json +3 -8
- package/node_modules/@llblab/pi-actors/scripts/build-dist.mjs +94 -30
- package/node_modules/@llblab/pi-grow-loop/AGENTS.md +10 -6
- package/node_modules/@llblab/pi-grow-loop/CHANGELOG.md +7 -0
- package/node_modules/@llblab/pi-grow-loop/README.md +2 -0
- package/node_modules/@llblab/pi-grow-loop/dist/index.d.ts +33 -0
- package/node_modules/@llblab/pi-grow-loop/dist/index.js +286 -0
- package/node_modules/@llblab/pi-grow-loop/dist/pi-grow-loop/index.d.ts +1 -0
- package/node_modules/@llblab/pi-grow-loop/dist/pi-grow-loop/index.js +1 -0
- package/node_modules/@llblab/pi-grow-loop/dist/skills/grow-loop/SKILL.md +117 -0
- package/node_modules/@llblab/pi-grow-loop/dist/skills/while-true/SKILL.md +233 -0
- package/node_modules/@llblab/pi-grow-loop/index.ts +67 -12
- package/node_modules/@llblab/pi-grow-loop/package.json +9 -8
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +23 -17
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +5 -3
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +11 -0
- package/node_modules/@llblab/pi-state-flow/README.md +18 -6
- package/node_modules/@llblab/pi-state-flow/dist/index.d.ts +2 -1
- package/node_modules/@llblab/pi-state-flow/dist/index.js +1 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.d.ts +2 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.js +5 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/context.d.ts +3 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/context.js +7 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.d.ts +5 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.js +54 -40
- package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +20 -20
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +755 -325
- package/node_modules/@llblab/pi-state-flow/dist/lib/git.d.ts +2 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/git.js +14 -17
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.d.ts +4 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +65 -23
- package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.d.ts +16 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.js +32 -15
- package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +28 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +276 -26
- package/node_modules/@llblab/pi-state-flow/dist/lib/session.d.ts +5 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/session.js +36 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +3 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +3 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +1 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +3 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/storage.d.ts +11 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/storage.js +150 -24
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.d.ts +14 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +52 -18
- package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +8 -8
- package/node_modules/@llblab/pi-state-flow/dist/package.json +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-guide/SKILL.md +3 -1
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +53 -7
- package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +84 -44
- package/node_modules/@llblab/pi-state-flow/docs/filesystem-recovery.md +12 -2
- package/node_modules/@llblab/pi-state-flow/docs/fork-contract.md +6 -4
- package/node_modules/@llblab/pi-state-flow/docs/performance.md +2 -2
- package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +28 -8
- package/node_modules/@llblab/pi-state-flow/docs/usage.md +41 -14
- package/node_modules/@llblab/pi-state-flow/index.ts +3 -0
- package/node_modules/@llblab/pi-state-flow/lib/artifact.ts +5 -5
- package/node_modules/@llblab/pi-state-flow/lib/context.ts +8 -3
- package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +57 -40
- package/node_modules/@llblab/pi-state-flow/lib/durable.ts +20 -20
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +719 -316
- package/node_modules/@llblab/pi-state-flow/lib/git.ts +16 -18
- package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +60 -24
- package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +34 -21
- package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +290 -25
- package/node_modules/@llblab/pi-state-flow/lib/session.ts +37 -2
- package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +3 -0
- package/node_modules/@llblab/pi-state-flow/lib/status.ts +4 -1
- package/node_modules/@llblab/pi-state-flow/lib/storage.ts +141 -22
- package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +60 -19
- package/node_modules/@llblab/pi-state-flow/lib/transition.ts +8 -8
- package/node_modules/@llblab/pi-state-flow/package.json +1 -1
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +3 -1
- package/node_modules/@llblab/pi-telegram/AGENTS.md +1 -1
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +5 -0
- package/node_modules/@llblab/pi-telegram/README.md +2 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/skills.d.ts +7 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/skills.js +32 -7
- package/node_modules/@llblab/pi-telegram/dist/package.json +3 -8
- package/node_modules/@llblab/pi-telegram/lib/skills.ts +43 -7
- package/node_modules/@llblab/pi-telegram/package.json +3 -8
- package/node_modules/@llblab/pi-telegram/scripts/build-dist.mjs +103 -32
- package/package.json +7 -7
|
@@ -1,47 +1,40 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* This is intentionally standalone: it is package/build glue, not reusable
|
|
7
|
-
* actor-domain behavior. It cleans dist, compiles TypeScript, mirrors runtime
|
|
8
|
-
* assets, and syntax-checks packaged scripts.
|
|
4
|
+
* Builds or verifies the JavaScript-only distributive tree without exposing a
|
|
5
|
+
* partial tree to Pi or destroying the previous distributive on failure.
|
|
9
6
|
*/
|
|
10
7
|
|
|
11
8
|
import { spawnSync } from "node:child_process";
|
|
12
9
|
import {
|
|
13
10
|
cpSync,
|
|
11
|
+
existsSync,
|
|
14
12
|
mkdirSync,
|
|
13
|
+
mkdtempSync,
|
|
14
|
+
readFileSync,
|
|
15
15
|
readdirSync,
|
|
16
|
+
renameSync,
|
|
16
17
|
rmSync,
|
|
17
18
|
writeFileSync,
|
|
18
19
|
} from "node:fs";
|
|
19
|
-
import { join } from "node:path";
|
|
20
|
+
import { join, relative } from "node:path";
|
|
21
|
+
|
|
22
|
+
const DIST_DIR = "dist";
|
|
23
|
+
const checkOnly = process.argv.includes("--check");
|
|
20
24
|
|
|
21
25
|
function run(command, args) {
|
|
22
26
|
const result = spawnSync(command, args, { stdio: "inherit" });
|
|
23
27
|
if (result.error) throw result.error;
|
|
24
|
-
if (result.status !== 0)
|
|
28
|
+
if (result.status !== 0) {
|
|
29
|
+
throw new Error(`${command} exited with status ${result.status ?? "unknown"}.`);
|
|
30
|
+
}
|
|
25
31
|
}
|
|
26
32
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"-p",
|
|
33
|
-
"tsconfig.build.json",
|
|
34
|
-
]);
|
|
35
|
-
|
|
36
|
-
mkdirSync(join("dist", "pi-actors"), { recursive: true });
|
|
37
|
-
writeFileSync(
|
|
38
|
-
join("dist", "pi-actors", "index.js"),
|
|
39
|
-
'export { default } from "../index.js";\n',
|
|
40
|
-
"utf8",
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
for (const dir of ["scripts", "fixtures", "skills"]) {
|
|
44
|
-
cpSync(dir, join("dist", dir), { recursive: true });
|
|
33
|
+
function listFiles(root, current = root) {
|
|
34
|
+
return readdirSync(current, { withFileTypes: true }).flatMap((entry) => {
|
|
35
|
+
const path = join(current, entry.name);
|
|
36
|
+
return entry.isDirectory() ? listFiles(root, path) : [relative(root, path)];
|
|
37
|
+
}).sort();
|
|
45
38
|
}
|
|
46
39
|
|
|
47
40
|
function listModuleScripts(root) {
|
|
@@ -52,9 +45,80 @@ function listModuleScripts(root) {
|
|
|
52
45
|
});
|
|
53
46
|
}
|
|
54
47
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
48
|
+
function normalizeTextFiles(root) {
|
|
49
|
+
const textSuffixes = [".js", ".ts", ".mjs", ".json", ".md"];
|
|
50
|
+
for (const path of listFiles(root)) {
|
|
51
|
+
if (!textSuffixes.some((suffix) => path.endsWith(suffix))) continue;
|
|
52
|
+
const absolutePath = join(root, path);
|
|
53
|
+
const source = readFileSync(absolutePath, "utf8");
|
|
54
|
+
const normalized = source.replace(/\r\n?/g, "\n");
|
|
55
|
+
if (normalized !== source) writeFileSync(absolutePath, normalized, "utf8");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function assertTreesEqual(expectedRoot, actualRoot) {
|
|
60
|
+
if (!existsSync(expectedRoot)) {
|
|
61
|
+
throw new Error(`${expectedRoot} is missing; run npm run build.`);
|
|
62
|
+
}
|
|
63
|
+
const expectedFiles = listFiles(expectedRoot);
|
|
64
|
+
const actualFiles = listFiles(actualRoot);
|
|
65
|
+
if (JSON.stringify(expectedFiles) !== JSON.stringify(actualFiles)) {
|
|
66
|
+
throw new Error("dist file inventory is stale; run npm run build.");
|
|
67
|
+
}
|
|
68
|
+
for (const path of expectedFiles) {
|
|
69
|
+
if (!readFileSync(join(expectedRoot, path)).equals(readFileSync(join(actualRoot, path)))) {
|
|
70
|
+
throw new Error(`dist/${path} is stale; run npm run build.`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function replaceDist(candidate) {
|
|
76
|
+
const backup = `.dist-backup-${process.pid}-${Date.now()}`;
|
|
77
|
+
const hadDist = existsSync(DIST_DIR);
|
|
78
|
+
if (hadDist) renameSync(DIST_DIR, backup);
|
|
79
|
+
try {
|
|
80
|
+
renameSync(candidate, DIST_DIR);
|
|
81
|
+
if (hadDist) rmSync(backup, { recursive: true, force: true });
|
|
82
|
+
} catch (error) {
|
|
83
|
+
if (hadDist && existsSync(backup) && !existsSync(DIST_DIR)) {
|
|
84
|
+
renameSync(backup, DIST_DIR);
|
|
85
|
+
}
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
59
89
|
|
|
60
|
-
|
|
90
|
+
const candidate = mkdtempSync(join(process.cwd(), ".dist-build-"));
|
|
91
|
+
try {
|
|
92
|
+
run(process.execPath, [
|
|
93
|
+
join("node_modules", "typescript", "bin", "tsc"),
|
|
94
|
+
"-p",
|
|
95
|
+
"tsconfig.build.json",
|
|
96
|
+
"--outDir",
|
|
97
|
+
candidate,
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
mkdirSync(join(candidate, "pi-actors"), { recursive: true });
|
|
101
|
+
writeFileSync(
|
|
102
|
+
join(candidate, "pi-actors", "index.js"),
|
|
103
|
+
'export { default } from "../index.js";\n',
|
|
104
|
+
"utf8",
|
|
105
|
+
);
|
|
106
|
+
normalizeTextFiles(candidate);
|
|
107
|
+
for (const dir of ["scripts", "fixtures", "skills"]) {
|
|
108
|
+
cpSync(dir, join(candidate, dir), { recursive: true });
|
|
109
|
+
}
|
|
110
|
+
const builtScripts = [
|
|
111
|
+
...listModuleScripts(join(candidate, "scripts")),
|
|
112
|
+
...listModuleScripts(join(candidate, "skills")),
|
|
113
|
+
];
|
|
114
|
+
run(process.execPath, ["--check", ...builtScripts]);
|
|
115
|
+
|
|
116
|
+
if (checkOnly) {
|
|
117
|
+
assertTreesEqual(DIST_DIR, candidate);
|
|
118
|
+
console.log("pi-actors: dist is current");
|
|
119
|
+
} else {
|
|
120
|
+
replaceDist(candidate);
|
|
121
|
+
}
|
|
122
|
+
} finally {
|
|
123
|
+
rmSync(candidate, { recursive: true, force: true });
|
|
124
|
+
}
|
|
@@ -33,12 +33,14 @@
|
|
|
33
33
|
## Topology
|
|
34
34
|
|
|
35
35
|
```text
|
|
36
|
-
index.ts
|
|
37
|
-
|
|
38
|
-
skills
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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.
|