@alphacoder-v0/pi-loops 0.17.5 → 0.18.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.
- package/CHANGELOG.md +53 -0
- package/README.md +11 -3
- package/README.zh-CN.md +17 -3
- package/docs/agents/issue-tracker.md +34 -0
- package/docs/cli.md +16 -0
- package/docs/design.md +43 -0
- package/docs/recipes.md +239 -0
- package/docs/web-ui-parity.md +2 -0
- package/package.json +3 -2
- package/recipes/_tracker/issue-tracker-github.md +31 -0
- package/recipes/_tracker/issue-tracker-local.md +38 -0
- package/recipes/_tracker-setup.md +12 -0
- package/recipes/autoresearch/README.md +27 -0
- package/recipes/autoresearch/RESEARCH.template.md +37 -0
- package/recipes/autoresearch/example/RESEARCH.md +31 -0
- package/recipes/autoresearch/example/eval.py +60 -0
- package/recipes/autoresearch/example/eval.sh +3 -0
- package/recipes/autoresearch/example/solution.py +20 -0
- package/recipes/autoresearch/experiment.md +82 -0
- package/recipes/autoresearch/recipe.toml +15 -0
- package/recipes/changelog-draft/recipe.toml +9 -0
- package/recipes/changelog-draft/release.md +35 -0
- package/recipes/issue-loop/README.md +16 -0
- package/recipes/issue-loop/implement.md +135 -0
- package/recipes/issue-loop/labels.sh +21 -0
- package/recipes/issue-loop/recipe.toml +18 -0
- package/recipes/issue-loop/triage.md +147 -0
- package/skills/pi-loops/SKILL.md +1 -0
- package/src/cli.ts +66 -1
- package/src/extension-entry.ts +24 -0
- package/src/pi-floor.ts +37 -0
- package/src/pi-loops.ts +321 -0
- package/src/recipe.ts +651 -0
- package/src/store.ts +2 -0
- package/src/tools.ts +3 -0
- package/src/web.mjs +5 -1
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: issue-triage
|
|
3
|
+
description: One run of the triage loop. Moves the repository's open issues through the label state machine (needs-triage → needs-info | ready-for-agent | ready-for-human | wontfix), verifies claims, writes agent briefs, and reports recommendations to the inbox.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Triage: one run
|
|
7
|
+
|
|
8
|
+
Autonomy: propose
|
|
9
|
+
|
|
10
|
+
The line above is the installed autonomy level; `/recipe add` wrote it and a person may change it.
|
|
11
|
+
- `propose`: you evaluate, verify, comment, and label `needs-triage` / `needs-info`. Moving an
|
|
12
|
+
issue to `ready-for-agent`, `ready-for-human` or `wontfix` is the maintainer's click; you
|
|
13
|
+
recommend it in the inbox.
|
|
14
|
+
- `act`: additionally, you move a **reproduced bug** with a brief to `ready-for-agent` yourself,
|
|
15
|
+
and close an issue that is already implemented or a duplicate (with the comment). Enhancements
|
|
16
|
+
and rejections still wait for the maintainer.
|
|
17
|
+
|
|
18
|
+
You are the triage loop. Nobody is watching. Everything you post on the tracker is read by people
|
|
19
|
+
later, and your final reply is read by a program (the loop protocol at the end of your prompt).
|
|
20
|
+
|
|
21
|
+
## The state machine
|
|
22
|
+
|
|
23
|
+
Every open issue carries one **category** label (`bug` | `enhancement`) and exactly one **state**
|
|
24
|
+
label:
|
|
25
|
+
|
|
26
|
+
| state | meaning | who moves an issue out of it |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| *(none)* | never looked at | you → `needs-triage`, first sighting |
|
|
29
|
+
| `needs-triage` | being evaluated | you → `needs-info`; maintainer → `ready-for-agent`, `ready-for-human`, `wontfix` |
|
|
30
|
+
| `needs-info` | waiting on the reporter | you → `needs-triage` once the reporter has replied |
|
|
31
|
+
| `ready-for-agent` | brief accepted, an agent may build it | the implement loop |
|
|
32
|
+
| `ready-for-human` | needs a person | the person |
|
|
33
|
+
| `agent-working`, `in-review`, `agent-blocked` | the implement loop's states | not yours to touch |
|
|
34
|
+
| `wontfix` | closed, will not be actioned | — |
|
|
35
|
+
|
|
36
|
+
**The tracker is the truth; your notes are a cache.** Read labels and comments fresh every run.
|
|
37
|
+
Your notes carry only what the tracker cannot tell you: which comment timestamp you last handled
|
|
38
|
+
per issue, so you do not re-triage or re-ask.
|
|
39
|
+
|
|
40
|
+
Every comment you post starts with this line, on its own:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
> *This was generated by AI during triage.*
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## What to do
|
|
47
|
+
|
|
48
|
+
**First, read `docs/agents/issue-tracker.md`.** It says which tracker this repository uses and how
|
|
49
|
+
to list, read, comment, change state, claim and close. The `gh` commands in this playbook are the
|
|
50
|
+
GitHub spelling; if the file describes local Markdown files, do each step the way it describes
|
|
51
|
+
instead (the states and the checkpoints are the same).
|
|
52
|
+
|
|
53
|
+
1. **List the work.** Open issues that are (a) unlabeled, (b) `needs-triage`, or (c) `needs-info`
|
|
54
|
+
with a comment by someone other than you that is newer than the timestamp in your notes for
|
|
55
|
+
that issue. Oldest first. Take at most **3** this run; the rest wait for the next one.
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
gh issue list --state open --limit 200 --json number,title,labels,updatedAt,author
|
|
59
|
+
gh issue view <n> --comments --json number,title,body,labels,author,createdAt,comments
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
2. **For each issue, gather context.** Read the whole issue and every comment; parse any earlier
|
|
63
|
+
*Triage Notes* or *Agent Brief* of yours so nothing already settled is asked again. Then:
|
|
64
|
+
- **Redundancy:** search the codebase for an existing implementation of the request — by the
|
|
65
|
+
concept, not the reporter's words — and remember where you looked.
|
|
66
|
+
- **Prior rejection:** read every `.out-of-scope/*.md` in the repository (if the directory
|
|
67
|
+
exists) and note any that resembles this request.
|
|
68
|
+
- **Duplicates:** compare against the other open issues' titles.
|
|
69
|
+
|
|
70
|
+
3. **Verify the claim.** For a bug, reproduce it from the reporter's steps: run the command, the
|
|
71
|
+
test, the script. For an enhancement, confirm it is not already there. Record what happened:
|
|
72
|
+
confirmed (with the code path), not reproduced, or not enough detail to try.
|
|
73
|
+
|
|
74
|
+
4. **Apply the outcome.**
|
|
75
|
+
- **Fully specified and verified** → post an *Agent Brief* (template below), make sure the
|
|
76
|
+
labels are `needs-triage` plus the category, and report to the inbox:
|
|
77
|
+
`#<n> brief posted — recommend ready-for-agent (<category>, <verified how>)`.
|
|
78
|
+
- **Needs a person** (design decision, external access, manual testing, taste) → post the brief
|
|
79
|
+
anyway with a line saying why it cannot be delegated; recommend `ready-for-human`.
|
|
80
|
+
- **Not enough to act on** → post *Triage Notes* (template below) with specific questions,
|
|
81
|
+
move to `needs-info`, report: `#<n> → needs-info (asked: <one phrase per question>)`.
|
|
82
|
+
- **Already implemented** → say where it lives in a comment; recommend `wontfix (already
|
|
83
|
+
implemented)`. Do not write to `.out-of-scope/`.
|
|
84
|
+
- **Out of scope / duplicate** → a polite comment with the reason (and the earlier issue, or
|
|
85
|
+
the `.out-of-scope/` file it matches); recommend `wontfix` or `duplicate of #<m>`.
|
|
86
|
+
- **Reporter replied on a `needs-info` issue** → move it back to `needs-triage` and evaluate
|
|
87
|
+
it again from step 2 in this same run.
|
|
88
|
+
|
|
89
|
+
Labels: `gh issue edit <n> --add-label "<state>" --remove-label "<old state>"`. Never leave an
|
|
90
|
+
issue with two state labels. Under `propose`, never close an issue.
|
|
91
|
+
|
|
92
|
+
5. **Do not** edit code, open branches, or touch issues in `ready-for-agent`, `agent-working`,
|
|
93
|
+
`in-review` or `agent-blocked` — those belong to the implement loop and the maintainer.
|
|
94
|
+
|
|
95
|
+
## Agent Brief
|
|
96
|
+
|
|
97
|
+
The brief is the contract the implement loop works from; the issue body and discussion are only
|
|
98
|
+
context. Write it to survive weeks of unrelated change: name types, functions, commands and
|
|
99
|
+
behaviour, never file paths or line numbers. Behavioural, not procedural. Acceptance criteria that
|
|
100
|
+
can each be checked on their own. Say what is out of scope.
|
|
101
|
+
|
|
102
|
+
```markdown
|
|
103
|
+
> *This was generated by AI during triage.*
|
|
104
|
+
|
|
105
|
+
## Agent Brief
|
|
106
|
+
|
|
107
|
+
**Category:** bug / enhancement
|
|
108
|
+
**Summary:** one line
|
|
109
|
+
|
|
110
|
+
**Current behavior:** what happens now (for a bug: the broken behaviour, reproduced how)
|
|
111
|
+
**Desired behavior:** what should happen, including edge cases and errors
|
|
112
|
+
|
|
113
|
+
**Key interfaces:**
|
|
114
|
+
- `TypeName` / `functionName()` / config shape: what changes and why
|
|
115
|
+
|
|
116
|
+
**Acceptance criteria:**
|
|
117
|
+
- [ ] specific, independently checkable
|
|
118
|
+
- [ ] …
|
|
119
|
+
|
|
120
|
+
**Out of scope:**
|
|
121
|
+
- what must not change in this issue
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Triage Notes (needs-info)
|
|
125
|
+
|
|
126
|
+
```markdown
|
|
127
|
+
> *This was generated by AI during triage.*
|
|
128
|
+
|
|
129
|
+
## Triage Notes
|
|
130
|
+
|
|
131
|
+
**What we've established so far:**
|
|
132
|
+
- …
|
|
133
|
+
|
|
134
|
+
**What we still need from you (@reporter):**
|
|
135
|
+
- a specific, answerable question
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Notes for the next run
|
|
139
|
+
|
|
140
|
+
One line per open issue you have handled: `#<n> <state you left it in> last-comment=<ISO timestamp
|
|
141
|
+
with offset> brief=yes|no`. Drop closed issues. Stay under 2000 characters — if you must cut, cut
|
|
142
|
+
the oldest `needs-info` lines; the tracker still has everything.
|
|
143
|
+
|
|
144
|
+
## Inbox
|
|
145
|
+
|
|
146
|
+
One line per issue you acted on, in the shapes above. No issues needing attention → no inbox
|
|
147
|
+
lines. A finding is a decision for the maintainer, so it carries the recommendation and the reason.
|
package/skills/pi-loops/SKILL.md
CHANGED
|
@@ -16,6 +16,7 @@ The user's pi has pi-loops installed. Prefer its tools over ad-hoc `sleep` loops
|
|
|
16
16
|
| the findings must be double-checked before the user sees them | `cron_create` with `stateful: true, verify: true` (a second adversarial sub-agent reviews each finding) |
|
|
17
17
|
| "when <condition> happens, do <action>" (a file appears, a PR merges, a build finishes) | `new_trigger` — condition + action, fires once unless the user asks for repeating |
|
|
18
18
|
| a reminder in N minutes | `cron_create` with schedule `in 10m` |
|
|
19
|
+
| run the project's issue tracker as a standing process (triage, build, review), or an experiment loop against a research contract | tell the user to run `/recipe list` and `/recipe add <name>` — a packaged set of loops installed with one confirmation; do not recreate it with `cron_create` |
|
|
19
20
|
|
|
20
21
|
Never use `new_trigger` for time-based schedules; never use `cron_create` for conditions.
|
|
21
22
|
|
package/src/cli.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { askHost, renderHostSnapshot } from "./host-control-channel.ts";
|
|
|
20
20
|
import { liveHost, stopHost } from "./host-control.ts";
|
|
21
21
|
import { type UiPrefs, readUiPrefs } from "./ui-prefs.ts";
|
|
22
22
|
import { JobStore, defaultLoopsDir } from "./store.ts";
|
|
23
|
+
import { AUTONOMY_LEVELS, type AutonomyLevel, INSTALL_ROOT, TRACKER_FILE, addLineFor, ensureExcluded, installDirFor, installFiles, installedRecipes, listRecipes, loadRecipe, planInstall, playbookFiles, readRecord, resolveRecipeRef } from "./recipe.ts";
|
|
23
24
|
import { TriggerStore } from "./triggers.ts";
|
|
24
25
|
import { PI_LOOPS_VERSION } from "./version.ts";
|
|
25
26
|
import { stamp } from "./schedule.ts";
|
|
@@ -104,6 +105,10 @@ export const CLI_USAGE = [
|
|
|
104
105
|
"",
|
|
105
106
|
"pi-loops host status | abort <run-id|trace-id> | stop",
|
|
106
107
|
" Look in on the background host that keeps the clock while no pi is open, or interrupt it.",
|
|
108
|
+
"",
|
|
109
|
+
"pi-loops recipe list | show <name|path> | add <name|path> [--cwd <dir>] [--level report|propose|act] [--overwrite]",
|
|
110
|
+
" The packaged recipes; what one installs; copy its playbooks into a project (.agents/skills/<name>/).",
|
|
111
|
+
" The setup script and the jobs need a pi open in the project: /recipe add <name> finds the copies and finishes.",
|
|
107
112
|
].join("\n");
|
|
108
113
|
|
|
109
114
|
/**
|
|
@@ -239,6 +244,7 @@ export async function runCli(argv: string[], out: (line: string) => void = conso
|
|
|
239
244
|
if (command === "web" || command === "tui") return launch(argv, out);
|
|
240
245
|
if (command === "upgrade") return upgrade(flags.has("check"), out);
|
|
241
246
|
if (command === "install-launcher") return installLauncher(str("dir"), out);
|
|
247
|
+
if (command === "recipe") return recipeCommand(positional, { level: str("level"), overwrite: flags.has("overwrite") }, cwd, loopsDir, out);
|
|
242
248
|
|
|
243
249
|
if (command === "sessions") {
|
|
244
250
|
// `pickSession` refuses an unknown id, and there was no way to discover one.
|
|
@@ -375,7 +381,66 @@ export async function runCli(argv: string[], out: (line: string) => void = conso
|
|
|
375
381
|
return asked ? 2 : 0;
|
|
376
382
|
}
|
|
377
383
|
|
|
378
|
-
const SUBCOMMANDS = new Set(["export", "import", "sessions", "inspect", "host", "web", "upgrade", "install-launcher", "help"]);
|
|
384
|
+
const SUBCOMMANDS = new Set(["export", "import", "sessions", "inspect", "host", "recipe", "web", "upgrade", "install-launcher", "help"]);
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* `pi-loops recipe`: the deterministic part of `/recipe`, for a shell with no pi open. `add` copies
|
|
388
|
+
* the playbooks, lists the directory in `.git/info/exclude` and writes the record — and stops there:
|
|
389
|
+
* the setup script is shown to a person before it runs and the jobs are created after a
|
|
390
|
+
* confirmation, both of which need the pi that `/recipe add` runs in. It finds the copies in place.
|
|
391
|
+
*/
|
|
392
|
+
async function recipeCommand(positional: string[], opts: { level?: string; overwrite: boolean }, cwd: string, loopsDir: string, out: (line: string) => void): Promise<number> {
|
|
393
|
+
const sub = positional[0] ?? "list";
|
|
394
|
+
if (sub === "list") {
|
|
395
|
+
const packaged = listRecipes();
|
|
396
|
+
const installed = new Map(installedRecipes(cwd).map((r) => [r.recipe, r]));
|
|
397
|
+
const jobs = new JobStore(loopsDir).load().filter((j) => j.recipe && j.cwd === cwd);
|
|
398
|
+
for (const r of packaged) {
|
|
399
|
+
const rec = installed.get(r.manifest.name);
|
|
400
|
+
const n = jobs.filter((j) => j.recipe === r.manifest.name).length;
|
|
401
|
+
out(`${r.manifest.name.padEnd(17)} ${(rec ? `installed: ${rec.level}, ${n} job(s)` : "—").padEnd(28)} ${r.manifest.summary}`);
|
|
402
|
+
}
|
|
403
|
+
for (const rec of installed.values()) if (!packaged.some((r) => r.manifest.name === rec.recipe)) out(`${rec.recipe.padEnd(17)} installed from ${rec.source} (${rec.level})`);
|
|
404
|
+
if (!packaged.length && !installed.size) out("no recipes packaged, none installed here");
|
|
405
|
+
return 0;
|
|
406
|
+
}
|
|
407
|
+
if (sub === "show" || sub === "add") {
|
|
408
|
+
const ref = positional[1];
|
|
409
|
+
if (!ref) throw new Error(`recipe ${sub} needs a recipe name or a directory`);
|
|
410
|
+
const resolved = resolveRecipeRef(ref, { cwd });
|
|
411
|
+
const recipe = loadRecipe(resolved.dir);
|
|
412
|
+
const m = recipe.manifest;
|
|
413
|
+
const rel = path.join(INSTALL_ROOT, m.name);
|
|
414
|
+
if (sub === "show") {
|
|
415
|
+
out(`${m.name} (${recipe.dir})`);
|
|
416
|
+
out(` ${m.summary}`);
|
|
417
|
+
out(` levels: ${m.levels.join(", ")} tracker: ${m.needsTracker ? "needs docs/agents/issue-tracker.md" : "not needed"}${m.setup ? ` setup: ${m.setup}` : ""}`);
|
|
418
|
+
out(` files → ${rel}/: ${playbookFiles(m).join(", ")}`);
|
|
419
|
+
for (const j of m.jobs) out(` /cron add ${addLineFor(j, rel)}`);
|
|
420
|
+
const record = readRecord(installDirFor(cwd, m.name));
|
|
421
|
+
if (record) out(` installed in ${cwd}: ${record.level}, ${record.installedAt}, pi-loops ${record.version}`);
|
|
422
|
+
return 0;
|
|
423
|
+
}
|
|
424
|
+
let level = m.levels[0];
|
|
425
|
+
if (opts.level) {
|
|
426
|
+
if (!m.levels.includes(opts.level as AutonomyLevel)) throw new Error(`${m.name} supports ${m.levels.join(", ")}, not "${opts.level}" (levels are ${AUTONOMY_LEVELS.join(", ")})`);
|
|
427
|
+
level = opts.level as AutonomyLevel;
|
|
428
|
+
}
|
|
429
|
+
const plan = planInstall(recipe, cwd, level);
|
|
430
|
+
const record = installFiles(recipe, cwd, level, { overwrite: opts.overwrite, source: resolved.source });
|
|
431
|
+
const excluded = ensureExcluded(cwd, rel);
|
|
432
|
+
if (m.needsTracker && fs.existsSync(path.join(cwd, TRACKER_FILE))) ensureExcluded(cwd, TRACKER_FILE, "file");
|
|
433
|
+
out(`copied ${record.files.length} file(s) → ${plan.targetDir}/ (level ${level}${opts.level ? "" : ", the lowest; --level to choose"})`);
|
|
434
|
+
for (const f of plan.changed) out(` ${f}: ${opts.overwrite ? "overwritten" : "kept as edited (--overwrite replaces it)"}`);
|
|
435
|
+
out(excluded === "no-git" ? "not a git repository: nothing to exclude" : `.git/info/exclude: ${excluded === "added" ? "added" : "already listed"}`);
|
|
436
|
+
if (m.needsTracker && !fs.existsSync(path.join(cwd, TRACKER_FILE))) out(`${m.name} reads docs/agents/issue-tracker.md and this project has none: /recipe add ${m.name} in pi writes it with you first`);
|
|
437
|
+
out(`no jobs created: open pi in ${cwd} and run /recipe add ${m.name} — it shows ${m.setup ? `${m.setup} and ` : ""}these lines, then creates them:`);
|
|
438
|
+
for (const line of plan.addLines) out(` /cron add ${line}`);
|
|
439
|
+
return 0;
|
|
440
|
+
}
|
|
441
|
+
out(`unknown recipe command ${JSON.stringify(sub)}: pi-loops recipe list | show <name|path> | add <name|path>`);
|
|
442
|
+
return 2;
|
|
443
|
+
}
|
|
379
444
|
|
|
380
445
|
/** `v1.2.3` → comparable parts; anything else is not a release and is ignored. */
|
|
381
446
|
function semver(tag: string): [number, number, number] | undefined {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What pi loads (`pi.extensions` in package.json): a version check, then the extension.
|
|
3
|
+
*
|
|
4
|
+
* The check cannot sit in `src/pi-loops.ts`. That file and the modules under it import names pi
|
|
5
|
+
* did not always export (`ModelRuntime`, `readStoredCredential`, `createSyntheticSourceInfo`) and
|
|
6
|
+
* subscribe to events it did not always emit. ESM links a whole import graph before running a line
|
|
7
|
+
* of it, so on an older pi the failure is a link error naming one export — "does not provide an
|
|
8
|
+
* export named ModelRuntime" — and nothing in it says "your pi is too old". This file imports only
|
|
9
|
+
* what every pi has exported for a long time, reads the version, and imports the rest afterwards,
|
|
10
|
+
* so what an old pi prints is the sentence that says which pi it needs.
|
|
11
|
+
*
|
|
12
|
+
* A namespace import on purpose: a name missing from an old pi is then `undefined` rather than a
|
|
13
|
+
* link error, which is exactly the case the check exists for.
|
|
14
|
+
*/
|
|
15
|
+
import * as sdk from "@earendil-works/pi-coding-agent";
|
|
16
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
17
|
+
import { piTooOld } from "./pi-floor.ts";
|
|
18
|
+
|
|
19
|
+
export default async function (pi: ExtensionAPI) {
|
|
20
|
+
const refusal = piTooOld(sdk.VERSION);
|
|
21
|
+
if (refusal) throw new Error(refusal);
|
|
22
|
+
const { default: piLoops } = await import("./pi-loops.ts");
|
|
23
|
+
return piLoops(pi);
|
|
24
|
+
}
|
package/src/pi-floor.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The oldest pi this extension loads into. `src/extension-entry.ts` reads it before anything else
|
|
3
|
+
* is imported, both READMEs state it, and `test/pi-floor.test.ts` keeps the three the same.
|
|
4
|
+
*
|
|
5
|
+
* 0.84.3 is where the API this code is written against became complete: `session_compact_failed`
|
|
6
|
+
* arrived there, `ModelRuntime` and `readStoredCredential` in 0.80.8, `ctx.thinkingLevel` in
|
|
7
|
+
* between. It was found by type-checking `src/` against older releases rather than by reading
|
|
8
|
+
* changelogs — 0.80.8 fails on the two newest names, 0.84.3 passes clean — and it is the version
|
|
9
|
+
* to re-check the same way when a newer name is used. Nothing enforces it at install time: pi
|
|
10
|
+
* installs packages with `--legacy-peer-deps`, so `peerDependencies` stays `*` as pi's docs ask
|
|
11
|
+
* and the range there would not be read anyway.
|
|
12
|
+
*/
|
|
13
|
+
export const PI_MIN_VERSION = "0.84.3";
|
|
14
|
+
|
|
15
|
+
/** `[major, minor, patch]`, or undefined for anything that does not start `N.N.N` (a `v` in front is fine). */
|
|
16
|
+
function parse(version: string): [number, number, number] | undefined {
|
|
17
|
+
const m = /^v?(\d+)\.(\d+)\.(\d+)/.exec(version.trim());
|
|
18
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Why this pi cannot load pi-loops, as the one sentence pi prints after "Failed to load extension",
|
|
23
|
+
* or undefined when it can. A pi that reports no version is refused as well: every pi new enough
|
|
24
|
+
* to load this exports `VERSION`, so a missing one is an old one, not an unknown one.
|
|
25
|
+
*/
|
|
26
|
+
export function piTooOld(version: string | undefined, min: string = PI_MIN_VERSION): string | undefined {
|
|
27
|
+
const have = version === undefined ? undefined : parse(version);
|
|
28
|
+
const need = parse(min);
|
|
29
|
+
if (!need) throw new Error(`PI_MIN_VERSION is not a version: ${JSON.stringify(min)}`);
|
|
30
|
+
const upgrade = "upgrade pi (`pi update`, or `npm i -g @earendil-works/pi-coding-agent`)";
|
|
31
|
+
if (!have) return `pi-loops needs pi ${min} or newer and this pi reports ${version === undefined ? "no version" : `version ${JSON.stringify(version)}`}: ${upgrade}`;
|
|
32
|
+
for (let i = 0; i < 3; i++) {
|
|
33
|
+
if (have[i] > need[i]) return undefined;
|
|
34
|
+
if (have[i] < need[i]) return `pi-loops needs pi ${min} or newer and this is pi ${version}: ${upgrade}`;
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|