pi-goal-list-loop-audit 0.16.0 → 0.17.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/README.md +13 -13
- package/docs/DESIGN.md +1 -1
- package/extensions/goal-loop-auditor.ts +1 -1
- package/extensions/goal-loop-core.ts +11 -1
- package/extensions/goal-loop-display.ts +7 -7
- package/extensions/loops/goal.ts +11 -22
- package/package.json +1 -1
- package/prompts/goal-loop-continuation.md +1 -1
- package/scripts/smoke.sh +6 -6
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ Four top-level commands, that's all:
|
|
|
40
40
|
/goal cancel # abort
|
|
41
41
|
/goal tweak "<new objective>" # edit in place (Confirm dialog)
|
|
42
42
|
/goal archive # archived goals, newest first
|
|
43
|
-
/
|
|
43
|
+
/glla # open the settings UI (or /glla key=value)
|
|
44
44
|
/list add # draft a contract (or a whole batch via items[])
|
|
45
45
|
/list add "<objective>" # queue one directly
|
|
46
46
|
/list add plan.md # file detected → bulk import, one Confirm
|
|
@@ -151,7 +151,7 @@ Each loop is a different policy class on the same status machine.
|
|
|
151
151
|
|
|
152
152
|
## Live TUI (always know it's on)
|
|
153
153
|
|
|
154
|
-
A persistent `
|
|
154
|
+
A persistent `glla:` status segment + an above-editor widget show the current
|
|
155
155
|
goal/loop at all times: objective, status, elapsed, tokens, next task or loop
|
|
156
156
|
metric, pause reason, and live auditor progress during audits. If something is
|
|
157
157
|
running, you can see it — no command needed.
|
|
@@ -166,19 +166,19 @@ No external watchdog plugin needed.
|
|
|
166
166
|
## Config (one global place, rarely opened)
|
|
167
167
|
|
|
168
168
|
```
|
|
169
|
-
/
|
|
170
|
-
/
|
|
171
|
-
/
|
|
172
|
-
/
|
|
173
|
-
/
|
|
174
|
-
/
|
|
175
|
-
/
|
|
169
|
+
/glla # open the settings UI
|
|
170
|
+
/glla model=provider/id # auditor model override → GLOBAL
|
|
171
|
+
/glla thinking=high # auditor thinking → GLOBAL
|
|
172
|
+
/glla notify='cmd "$1"' # push on complete/pause/stop → GLOBAL
|
|
173
|
+
/glla tokenlimit=10000000 # per-goal token budget (default: off) → GLOBAL
|
|
174
|
+
/glla tokenlimit=0 # explicitly no cap (the default)
|
|
175
|
+
/glla project tokenlimit=500 # rare per-project override
|
|
176
176
|
```
|
|
177
177
|
|
|
178
178
|
Resolution per key: **project > global > defaults**. The auditor defaults to
|
|
179
179
|
your pi session model. When the session provider is extension-registered the
|
|
180
180
|
auditor can't auth it — you're told once (info level) with the fix:
|
|
181
|
-
`/
|
|
181
|
+
`/glla model=provider/id`, set once, rarely touched again. The plugin never
|
|
182
182
|
picks a model itself. Thinking follows the session too (floor `high`).
|
|
183
183
|
|
|
184
184
|
## Token guard
|
|
@@ -186,7 +186,7 @@ picks a model itself. Thinking follows the session too (floor `high`).
|
|
|
186
186
|
Every goal tracks real token usage; crossing the budget pauses the goal.
|
|
187
187
|
Default 10,000,000 per goal — a runaway threshold, not a big-goal threshold
|
|
188
188
|
(real research/feature goals legitimately burn 2-4M). Tune with
|
|
189
|
-
`/
|
|
189
|
+
`/glla tokenlimit=<n>`. Loop 3 doesn't need this cap — it has its own brakes
|
|
190
190
|
(max iterations + plateau).
|
|
191
191
|
|
|
192
192
|
## Compatibility (what goes well, what conflicts)
|
|
@@ -212,8 +212,8 @@ not requirements).
|
|
|
212
212
|
|
|
213
213
|
**Two footnotes**: (1) extension-registered providers work in the main session
|
|
214
214
|
but not the auditor's extension-less session — if audits fail auth, set the
|
|
215
|
-
override once with `/
|
|
216
|
-
`/
|
|
215
|
+
override once with `/glla model=`. (2) `pi-notify-agent` notifies on every turn;
|
|
216
|
+
`/glla notify=` fires only on goal complete/pause/loop stop.
|
|
217
217
|
|
|
218
218
|
## Files
|
|
219
219
|
|
package/docs/DESIGN.md
CHANGED
|
@@ -179,7 +179,7 @@ active → aborted (user /pi-gla-cancel)
|
|
|
179
179
|
|
|
180
180
|
### Decision 9: JSONL state (deterministic compaction)
|
|
181
181
|
|
|
182
|
-
Goal state lives in `.pi-
|
|
182
|
+
Goal state lives in `.pi-glla/active.jsonl`. Each line is a state transition. On compaction, the summary is rebuilt deterministically from the JSONL (autoresearch pattern).
|
|
183
183
|
|
|
184
184
|
This protects against model-generated summaries losing fidelity.
|
|
185
185
|
|
|
@@ -297,7 +297,7 @@ export async function runGoalCompletionAuditor(args: {
|
|
|
297
297
|
output,
|
|
298
298
|
model: modelLabel(model),
|
|
299
299
|
thinkingLevel,
|
|
300
|
-
error: `Auditor produced no output${streamError ? `: ${streamError}` : " — the auditor session likely failed (check the model's auth/quota, or set a working one with /
|
|
300
|
+
error: `Auditor produced no output${streamError ? `: ${streamError}` : " — the auditor session likely failed (check the model's auth/quota, or set a working one with /glla model=provider/id)"}`,
|
|
301
301
|
};
|
|
302
302
|
}
|
|
303
303
|
|
|
@@ -334,7 +334,17 @@ export const DEFAULT_STATE: State = {
|
|
|
334
334
|
// =================================================================
|
|
335
335
|
|
|
336
336
|
export function piGlaDir(cwd: string): string {
|
|
337
|
-
|
|
337
|
+
const dir = path.join(cwd, ".pi-glla");
|
|
338
|
+
// v0.17.0: one-time migration of the pre-rename state dir (.pi-gla →
|
|
339
|
+
// .pi-glla). Active goals, ledgers, and project settings move with the
|
|
340
|
+
// name — no relics, no lost state.
|
|
341
|
+
const legacy = path.join(cwd, ".pi-gla");
|
|
342
|
+
try {
|
|
343
|
+
if (!fs.existsSync(dir) && fs.existsSync(legacy)) fs.renameSync(legacy, dir);
|
|
344
|
+
} catch {
|
|
345
|
+
// read-only fs or partial state — fall through and use the new dir
|
|
346
|
+
}
|
|
347
|
+
return dir;
|
|
338
348
|
}
|
|
339
349
|
|
|
340
350
|
export function goalMdPath(cwd: string, id: string): string {
|
|
@@ -46,28 +46,28 @@ export interface AuditDisplayProgress {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
|
-
* One-line status for ctx.ui.setStatus("pi-
|
|
49
|
+
* One-line status for ctx.ui.setStatus("pi-glla", …).
|
|
50
50
|
* Returns undefined when nothing is being supervised (clears the segment).
|
|
51
51
|
*/
|
|
52
52
|
export function buildStatusText(state: State, audit?: AuditDisplayProgress | null, now = Date.now()): string | undefined {
|
|
53
53
|
if (state.loop?.active) {
|
|
54
54
|
const l = state.loop;
|
|
55
55
|
const arrow = l.direction === "min" ? "↓" : "↑";
|
|
56
|
-
return `
|
|
56
|
+
return `glla: loop ${arrow} iter ${l.iteration}/${l.maxIterations} · best ${l.bestValue ?? "n/a"} · stall ${l.stallCount}/${l.plateauWindow}`;
|
|
57
57
|
}
|
|
58
58
|
const g = state.goal;
|
|
59
59
|
if (!g) return undefined;
|
|
60
60
|
if (g.status === "auditing") {
|
|
61
61
|
const tool = audit?.currentTool ? ` · ${audit.currentTool}` : "";
|
|
62
|
-
return `
|
|
62
|
+
return `glla: auditing…${tool}`;
|
|
63
63
|
}
|
|
64
64
|
if (g.status === "paused") {
|
|
65
|
-
return `
|
|
65
|
+
return `glla: paused ⏸ ${truncate(g.pauseReason ?? "", 40)}`;
|
|
66
66
|
}
|
|
67
67
|
if (g.status === "active") {
|
|
68
68
|
const queue = state.list?.length ? ` · list ${state.list.length}` : "";
|
|
69
69
|
const tasks = g.taskList ? ` ${countDone(g)}/${countTotal(g)} tasks ·` : "";
|
|
70
|
-
return `
|
|
70
|
+
return `glla: goal ●${tasks} ${fmtElapsed(now - Date.parse(g.createdAt))}${queue}`;
|
|
71
71
|
}
|
|
72
72
|
return undefined; // complete/aborted → clear
|
|
73
73
|
}
|
|
@@ -99,7 +99,7 @@ function countTotal(g: Goal): number {
|
|
|
99
99
|
// ---- above-editor widget (multi-line panel) ----
|
|
100
100
|
|
|
101
101
|
/**
|
|
102
|
-
* Widget lines for ctx.ui.setWidget("pi-
|
|
102
|
+
* Widget lines for ctx.ui.setWidget("pi-glla", lines).
|
|
103
103
|
* Returns undefined when nothing is worth showing.
|
|
104
104
|
*/
|
|
105
105
|
export function buildWidgetLines(state: State, audit?: AuditDisplayProgress | null, now = Date.now()): string[] | undefined {
|
|
@@ -128,7 +128,7 @@ function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | u
|
|
|
128
128
|
const next = nextPending(g);
|
|
129
129
|
if (next) lines.push(`├─ next: ${truncate(next, 56)}`);
|
|
130
130
|
const queue = state.list?.length ?? 0;
|
|
131
|
-
lines.push(`└─ ${queue > 0 ? `list ${queue} · ` : ""}/goal status · /
|
|
131
|
+
lines.push(`└─ ${queue > 0 ? `list ${queue} · ` : ""}/goal status · /glla`);
|
|
132
132
|
return lines;
|
|
133
133
|
}
|
|
134
134
|
|
package/extensions/loops/goal.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* /goal "<objective>" | /goal (draft) | /goal status|pause|resume|cancel|tweak <text>|archive
|
|
12
12
|
* /list add|show|next|remove|clear
|
|
13
13
|
* /loop (draft) | /loop start|status|stop
|
|
14
|
-
* /
|
|
14
|
+
* /glla (settings UI) | /glla key=value | /glla project key=value
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import * as fs from "node:fs";
|
|
@@ -142,8 +142,8 @@ let uiTicker: NodeJS.Timeout | null = null;
|
|
|
142
142
|
function refreshUI(ctx: ExtensionContext): void {
|
|
143
143
|
if (!ctx.hasUI) return;
|
|
144
144
|
try {
|
|
145
|
-
ctx.ui.setStatus("pi-
|
|
146
|
-
ctx.ui.setWidget("pi-
|
|
145
|
+
ctx.ui.setStatus("pi-glla", buildStatusText(state, latestAuditProgress));
|
|
146
|
+
ctx.ui.setWidget("pi-glla", buildWidgetLines(state, latestAuditProgress));
|
|
147
147
|
} catch {
|
|
148
148
|
// stale ctx — next event refreshes
|
|
149
149
|
}
|
|
@@ -545,7 +545,7 @@ async function cmdStatus(ctx: ExtensionContext): Promise<void> {
|
|
|
545
545
|
`Objective: ${g.objective}`,
|
|
546
546
|
`Auto-continue: ${g.autoContinue ? "on" : "off"}`,
|
|
547
547
|
`Iteration: ${iterationCounter}`,
|
|
548
|
-
`Tokens: ${(g.usage?.tokensUsed ?? 0).toLocaleString()}${(g.usage?.tokensLimit ?? 0) > 0 ? ` / ${(g.usage!.tokensLimit).toLocaleString()}` : " (no cap — /
|
|
548
|
+
`Tokens: ${(g.usage?.tokensUsed ?? 0).toLocaleString()}${(g.usage?.tokensLimit ?? 0) > 0 ? ` / ${(g.usage!.tokensLimit).toLocaleString()}` : " (no cap — /glla tokenlimit=<n> to set)"}`,
|
|
549
549
|
];
|
|
550
550
|
if (g.auditHistory && g.auditHistory.length > 0) {
|
|
551
551
|
lines.push(`Audits: ${g.auditHistory.length} (${g.auditHistory.filter((v) => v.approved).length} approved)`);
|
|
@@ -834,7 +834,7 @@ async function cmdList(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
834
834
|
/**
|
|
835
835
|
* Config-gated push notification: if settings.notifyCmd is set, shell out
|
|
836
836
|
* with the message as $1. Fire-and-forget — a broken notify command never
|
|
837
|
-
* blocks the loop. /
|
|
837
|
+
* blocks the loop. /glla notify='<cmd>' to configure.
|
|
838
838
|
*/
|
|
839
839
|
function notifyExternal(ctx: ExtensionContext, message: string): void {
|
|
840
840
|
try {
|
|
@@ -848,7 +848,7 @@ function notifyExternal(ctx: ExtensionContext, message: string): void {
|
|
|
848
848
|
}
|
|
849
849
|
|
|
850
850
|
// =================================================================
|
|
851
|
-
// Loop 3: /loop — metric-driven
|
|
851
|
+
// Loop 3: /loop — metric-driven process loop (never completes)
|
|
852
852
|
//
|
|
853
853
|
// The anti-doorknob law: the loop only believes a number. The orchestrator
|
|
854
854
|
// runs the user's measure command (via pi.exec) after every agent turn;
|
|
@@ -1307,13 +1307,13 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
1307
1307
|
status: "active",
|
|
1308
1308
|
auditHistory: history,
|
|
1309
1309
|
pauseReason: `auditor infrastructure: ${result.error}`,
|
|
1310
|
-
pauseSuggestedAction: "Fix the auditor model (/
|
|
1310
|
+
pauseSuggestedAction: "Fix the auditor model (/glla model=provider/id) and call complete_goal again — your work was NOT judged",
|
|
1311
1311
|
}, ctx);
|
|
1312
1312
|
scheduleContinuation(ctx, true);
|
|
1313
1313
|
return {
|
|
1314
1314
|
content: [{
|
|
1315
1315
|
type: "text",
|
|
1316
|
-
text: `The auditor could not run (infrastructure, NOT a verdict): ${result.error}\nYour completion claim was not evaluated. Fix the auditor model with /
|
|
1316
|
+
text: `The auditor could not run (infrastructure, NOT a verdict): ${result.error}\nYour completion claim was not evaluated. Fix the auditor model with /glla model=provider/id and call complete_goal again — do not change your deliverable for this.`,
|
|
1317
1317
|
}],
|
|
1318
1318
|
details: {},
|
|
1319
1319
|
};
|
|
@@ -1423,7 +1423,7 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
1423
1423
|
parameters: Type.Object({
|
|
1424
1424
|
objective: Type.String({ description: "The clarified, concrete objective (single item) or a summary when items[] is used" }),
|
|
1425
1425
|
verificationContract: Type.Optional(Type.String({ description: "Checkable done-criteria (commands, file states, test outcomes)" })),
|
|
1426
|
-
items: Type.Optional(Type.Array(Type.String(), { description: "LIST drafting only: many objectives at once (e.g. 'queue these 50 things'). Each becomes a
|
|
1426
|
+
items: Type.Optional(Type.Array(Type.String(), { description: "LIST drafting only: many objectives at once (e.g. 'queue these 50 things'). Each becomes a list item; per-item 'Done when:' clauses are honored." })),
|
|
1427
1427
|
}),
|
|
1428
1428
|
async execute(_id, params, _signal, _onUpdate, execCtx) {
|
|
1429
1429
|
const p = params as { objective: string; verificationContract?: string; items?: string[] };
|
|
@@ -2090,7 +2090,7 @@ async function cmdSettings(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
2090
2090
|
// We detect duplicates at session start and warn loudly once.
|
|
2091
2091
|
// =================================================================
|
|
2092
2092
|
|
|
2093
|
-
const OUR_COMMANDS = ["goal", "
|
|
2093
|
+
const OUR_COMMANDS = ["goal", "glla", "list", "loop"];
|
|
2094
2094
|
let collisionWarned = false;
|
|
2095
2095
|
|
|
2096
2096
|
// Providers verified to exist in a bare (extension-less) session. The auditor
|
|
@@ -2168,23 +2168,12 @@ export default function (pi: ExtensionAPI): void {
|
|
|
2168
2168
|
description: "Open the settings UI for goals, loops, lists, and the auditor. Scriptable form: /glla key=value · /glla project key=value",
|
|
2169
2169
|
handler: settingsHandler,
|
|
2170
2170
|
});
|
|
2171
|
-
// /gla kept as an alias (renamed package, v0.15.0) — muscle memory is real.
|
|
2172
|
-
pi.registerCommand("gla", {
|
|
2173
|
-
description: "Alias for /glla (package renamed to pi-goal-list-loop-audit in v0.15.0).",
|
|
2174
|
-
handler: settingsHandler,
|
|
2175
|
-
});
|
|
2176
2171
|
pi.registerCommand("list", {
|
|
2177
2172
|
description: "Loop 2: the list of audited goals — order is the default, not the law. /list add <obj or file or paste> | /list show | /list next [n] | /list remove <n> | /list clear",
|
|
2178
2173
|
handler: (args: string, ctx: ExtensionContext) => { rememberCtx(ctx); return cmdList(args, ctx); },
|
|
2179
2174
|
});
|
|
2180
|
-
// Alias for one release (renamed 0.10.0 back to /list — order isn't
|
|
2181
|
-
// mandatory with subagents; it's a shopping list, not a FIFO).
|
|
2182
|
-
pi.registerCommand("queue", {
|
|
2183
|
-
description: "Alias for /list (renamed in v0.10.0). Same subcommands.",
|
|
2184
|
-
handler: (args: string, ctx: ExtensionContext) => { rememberCtx(ctx); return cmdList(args, ctx); },
|
|
2185
|
-
});
|
|
2186
2175
|
pi.registerCommand("loop", {
|
|
2187
|
-
description: "Loop 3: metric-driven
|
|
2176
|
+
description: "Loop 3: metric-driven process — it never completes. /loop start \"<target>\" measure=\"<cmd>\" direction=min|max [window=5] [max=50] [time=<hours>] [tokens=<budget>] [branch=1] | /loop status | /loop stop. 'Improve until X' is a /goal, not a loop.",
|
|
2188
2177
|
handler: (args: string, ctx: ExtensionContext) => { rememberCtx(ctx); return cmdLoop(args, ctx); },
|
|
2189
2178
|
});
|
|
2190
2179
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-goal-list-loop-audit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.1",
|
|
4
4
|
"description": "Goal. Loop. Audit. Done. — a pi-coding-agent extension that supervises long-running work, with isolated auditor on each completion. Beat bamboozling by design: the auditor runs in a fresh session with no extensions, no skills, no editor — only the read tools needed to verify your goal.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "dracon",
|
|
@@ -43,7 +43,7 @@ You have `read`, `write`, `edit`, `bash`, `grep`, `find`, `ls`, and the goal too
|
|
|
43
43
|
|
|
44
44
|
If the objective decomposes into milestones and no task list exists yet, call `propose_task_list` early — the user confirms it, then you track progress with `complete_task` / `update_task_status` as you go (not in a batch at the end). Limits: 20 tasks, 5 subtasks per task.
|
|
45
45
|
|
|
46
|
-
When the agent calls any of these, the orchestrator tracks the call and persists state to `.pi-
|
|
46
|
+
When the agent calls any of these, the orchestrator tracks the call and persists state to `.pi-glla/active.jsonl`.
|
|
47
47
|
|
|
48
48
|
## TASK WORKFLOW
|
|
49
49
|
|
package/scripts/smoke.sh
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# pi-goal-list-loop-audit — live integration smoke
|
|
3
3
|
#
|
|
4
4
|
# Drives a real pi session in tmux against a scratch dir and asserts on the
|
|
5
|
-
# .pi-
|
|
5
|
+
# .pi-glla ledger. This is the M2 "integration harness": it exercises the full
|
|
6
6
|
# loop (goal → agent work → complete_goal → isolated auditor → archive) with
|
|
7
7
|
# real models, which unit tests cannot do.
|
|
8
8
|
#
|
|
@@ -43,7 +43,7 @@ wait_for() { # wait_for <pattern> <timeout-s>
|
|
|
43
43
|
return 1
|
|
44
44
|
}
|
|
45
45
|
ledger_has() { # ledger_has <jq-ish python expr substring>
|
|
46
|
-
python3 - "$1" "$WORK/.pi-
|
|
46
|
+
python3 - "$1" "$WORK/.pi-glla/active.jsonl" <<'EOF'
|
|
47
47
|
import json, sys
|
|
48
48
|
needle, path = sys.argv[1], sys.argv[2]
|
|
49
49
|
try:
|
|
@@ -81,7 +81,7 @@ case "$SCENARIO" in
|
|
|
81
81
|
sleep 2
|
|
82
82
|
if [ -f "$WORK/smoke.txt" ]; then pass "smoke.txt created"; else fail "smoke.txt missing"; fi
|
|
83
83
|
if ledger_has '"approved":true'; then pass "ledger records approval"; else fail "ledger missing approval"; fi
|
|
84
|
-
if ls "$WORK/.pi-
|
|
84
|
+
if ls "$WORK/.pi-glla/archive/"*.md >/dev/null 2>&1; then pass "goal archived"; else fail "archive empty"; fi
|
|
85
85
|
if ledger_has '"regressionShieldPassed":true'; then pass "regression_shield recorded"; else fail "shield outcome missing"; fi
|
|
86
86
|
;;
|
|
87
87
|
|
|
@@ -93,11 +93,11 @@ case "$SCENARIO" in
|
|
|
93
93
|
if wait_for "approved by auditor" 120; then pass "item 1 approved"; else fail "item 1 not approved"; fi
|
|
94
94
|
# wait for second archive file
|
|
95
95
|
for i in $(seq 1 120); do
|
|
96
|
-
n=$(ls "$WORK/.pi-
|
|
96
|
+
n=$(ls "$WORK/.pi-glla/archive/"*.md 2>/dev/null | wc -l)
|
|
97
97
|
[ "$n" -ge 2 ] && break
|
|
98
98
|
sleep 1
|
|
99
99
|
done
|
|
100
|
-
n=$(ls "$WORK/.pi-
|
|
100
|
+
n=$(ls "$WORK/.pi-glla/archive/"*.md 2>/dev/null | wc -l)
|
|
101
101
|
if [ "$n" -ge 2 ]; then pass "both items archived ($n)"; else fail "only $n archived"; fi
|
|
102
102
|
if [ -f "$WORK/a.txt" ] && [ -f "$WORK/b.txt" ]; then pass "both files created"; else fail "files missing"; fi
|
|
103
103
|
if ledger_has '"list":[]'; then pass "list drained"; else fail "list not empty"; fi
|
|
@@ -119,7 +119,7 @@ case "$SCENARIO" in
|
|
|
119
119
|
echo 5 > "$WORK/num.txt"
|
|
120
120
|
NOTIFY_LOG="$WORK/notify.log"
|
|
121
121
|
# project scope — never write test config into the user's GLOBAL settings
|
|
122
|
-
send "/
|
|
122
|
+
send "/glla project notify='echo \$1 >> $NOTIFY_LOG'"
|
|
123
123
|
sleep 4
|
|
124
124
|
send '/loop start "Reduce the number in num.txt toward zero, never below 0" measure="cat num.txt" direction=min window=3 max=12'
|
|
125
125
|
say "waiting for plateau stop (up to 300s)"
|