pi-cc-loop 0.7.7 → 0.7.9
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 +13 -0
- package/README.md +4 -2
- package/index.ts +21 -0
- package/package.json +3 -2
- package/prompts.ts +9 -7
- package/settings.ts +3 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog — pi-cc-loop
|
|
2
|
+
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
## [0.7.9] - 2026-11-03
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Repository-side `scripts/compat-check.mjs` checks this package and its siblings against an installed Pi release: all host aliases, runtime imports, real extension loading, tool schemas, and themes. It supports Pi 1.4.0 and 1.5.0, rejects inspection failures instead of silently skipping them, and can be invoked through a symlink.
|
|
9
|
+
- Regression coverage for canonical and legacy host imports, incompatible schemas, Pi 1.4's compatibility exports, and single-release test environments.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- Tool-schema tests use the installed Pi's real `validateToolArguments`, with TypeBox on older releases and `pi-schema` on Pi 1.5.0. They strictly check complete coerced arguments, Pi's rejection messages, JSON-visible data, and symbol metadata, and run in isolated single-release installations.
|
|
13
|
+
- The `/pi_cc` settings panel reports Pi as waiting on input (OSC 7501) while it is open.
|
package/README.md
CHANGED
|
@@ -78,7 +78,9 @@ ln -sfn "$PWD" ~/.pi/agent/extensions/pi-cc-loop
|
|
|
78
78
|
| `cron_list` | `CronList` | 列出任务和实际的下次运行时间(已含错峰) |
|
|
79
79
|
| `schedule_wakeup` | `ScheduleWakeup` | 自定步调:`delaySeconds`(限制在 60–3600)、`reason`(必填,显示在触发块表头)、`prompt`;`stop: true` 结束 |
|
|
80
80
|
|
|
81
|
-
四个工具都声明了 `outputSchema
|
|
81
|
+
四个工具都声明了 `outputSchema`。三个 cron 工具保持可从 codemode 调用:`tools.cron_create(...)`、`tools.cron_list()` 返回结构化结果(`{ id, nextFireAt, … }`、`{ jobs: [...] }`),可以在脚本中批量查询/取消。
|
|
82
|
+
|
|
83
|
+
`schedule_wakeup` 是 **model-only 的回合控制工具**:仍直接向模型声明,但不能通过 codemode 或其他工具间接调用。新建/续期必须是当前 assistant 响应中的唯一工具调用;先完成 Monitor 等操作并向用户更新,再单独调用。运行时无法确认独立调用、或发现同批有其他调用时,会拒绝且不创建/修改 wakeup。`stop: true` 不强制结束回合,仍可向用户说明停止结果;固定 cron tick 的重复 wakeup 仍保持无操作。
|
|
82
84
|
|
|
83
85
|
## 渲染
|
|
84
86
|
|
|
@@ -206,7 +208,7 @@ node scripts/cache-check.mjs # 真实 provider 的缓存检查,默认用免
|
|
|
206
208
|
|
|
207
209
|
- Esc 只取消被打断回合所属的自定步调 loop;CC 按 Esc(包括空闲时、打断无关回合时)会取消所有自定步调 loop。其他情况用 `/loop stop`。
|
|
208
210
|
- 固定间隔的触发里调用 `schedule_wakeup` 会被当作空操作并返回说明,防止同一个任务又被自定步调重复调度;CC 没有这条硬规则。
|
|
209
|
-
- 不带 `/loop` 的请求(比如"每分钟打印一次hello")也会在建好任务后立即做一次。CC 靠模型自己调用 loop 技能做到(`CronCreate` 在 CC
|
|
211
|
+
- 不带 `/loop` 的请求(比如"每分钟打印一次hello")也会在建好任务后立即做一次。CC 靠模型自己调用 loop 技能做到(`CronCreate` 在 CC 里是延迟加载的工具,模型会先走技能);本包默认直接声明 cron 工具,这里改为在 `cron_create` 的结果里加一句提示,只在普通对话里建周期任务时出现。
|
|
210
212
|
- 底部摘要和 `maxFires` 次数上限是额外加的;CC 主会话不显示 loop 状态,也没有次数上限。
|
|
211
213
|
- 自定步调 loop 结束(stop、Esc、`cron_delete`、兜底心跳用完)时,绑定的 Monitor 会被静默停止;CC 靠模型自己调用 `TaskStop`。
|
|
212
214
|
- Monitor 事件不会取消待触发的 wakeup,它始终是兜底心跳(与 CC 一致)。
|
package/index.ts
CHANGED
|
@@ -143,6 +143,22 @@ function normalizePrompt(prompt: string): string {
|
|
|
143
143
|
return prompt.trim().replace(/^\/loop\s+/i, "").trim();
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
/** Pi only terminates a batch when every call agrees; never arm from a mixed or nested call. */
|
|
147
|
+
function requireStandaloneWakeup(toolCallId: string, ctx: Pick<ExtensionContext, "sessionManager">): void {
|
|
148
|
+
const branch = ctx.sessionManager.getBranch?.() ?? [];
|
|
149
|
+
for (let i = branch.length - 1; i >= 0; i--) {
|
|
150
|
+
const entry = branch[i];
|
|
151
|
+
if (entry.type !== "message" || entry.message.role !== "assistant") continue;
|
|
152
|
+
const calls = entry.message.content.filter((block) => block.type === "toolCall");
|
|
153
|
+
if (calls.length === 1 && calls[0].id === toolCallId && calls[0].name === "schedule_wakeup") return;
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
throw new Error(
|
|
157
|
+
"No wakeup was scheduled: schedule_wakeup must be called directly as the only tool call in its assistant response, " +
|
|
158
|
+
"not through codemode or in a batch. Finish other tools and write your update first, then call it alone.",
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
146
162
|
/** A stored job as a codemode script sees it: every field, minus those holding no value (the value is JSON). */
|
|
147
163
|
function jobValue(job: LoopJob): Record<string, string | number | boolean> {
|
|
148
164
|
return Object.fromEntries(Object.entries(storedJob(job)).filter(([, value]) => value !== undefined)) as Record<
|
|
@@ -866,6 +882,8 @@ export default function piCcLoopExtension(pi: ExtensionAPI): void {
|
|
|
866
882
|
pi.registerTool({
|
|
867
883
|
name: "schedule_wakeup",
|
|
868
884
|
label: "ScheduleWakeup",
|
|
885
|
+
// A turn-control tool: codemode cannot propagate its terminate flag.
|
|
886
|
+
exposure: "model-only",
|
|
869
887
|
description: SCHEDULE_WAKEUP_DESCRIPTION,
|
|
870
888
|
promptSnippet: SCHEDULE_WAKEUP_SNIPPET,
|
|
871
889
|
parameters: Type.Object({
|
|
@@ -961,6 +979,9 @@ export default function piCcLoopExtension(pi: ExtensionAPI): void {
|
|
|
961
979
|
};
|
|
962
980
|
}
|
|
963
981
|
|
|
982
|
+
// Check the finalized assistant batch before changing any chain or timer.
|
|
983
|
+
requireStandaloneWakeup(_toolCallId, ctx);
|
|
984
|
+
|
|
964
985
|
// Claude Code rounds and clamps; only a change after rounding counts as clamped.
|
|
965
986
|
const requested = params.delaySeconds;
|
|
966
987
|
const rounded = Number.isFinite(requested) ? Math.round(requested) : requested > 0 ? MAX_DELAY_SECONDS : MIN_DELAY_SECONDS;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-cc-loop",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.9",
|
|
4
4
|
"description": "Claude Code compatible /loop for the Pi coding agent: cron-scheduled fixed intervals, self-paced chains, and the same dispatch semantics as Claude Code.",
|
|
5
5
|
"author": "tarnish233",
|
|
6
6
|
"type": "module",
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
"files": [
|
|
25
25
|
"*.ts",
|
|
26
26
|
"README.md",
|
|
27
|
-
"LICENSE"
|
|
27
|
+
"LICENSE",
|
|
28
|
+
"CHANGELOG.md"
|
|
28
29
|
],
|
|
29
30
|
"pi": {
|
|
30
31
|
"extensions": [
|
package/prompts.ts
CHANGED
|
@@ -69,10 +69,10 @@ const LOOP_COMMAND_GUIDELINES = [
|
|
|
69
69
|
"4. Never call `schedule_wakeup` for this loop: the cron fires its own ticks. Scheduling one would run the task twice.",
|
|
70
70
|
"",
|
|
71
71
|
"## Self-paced (no interval)",
|
|
72
|
-
"1. Do the task now
|
|
73
|
-
"2.
|
|
72
|
+
"1. Do the task now.",
|
|
73
|
+
"2. If the next iteration waits on an external event and no monitor is running for it, arm `Monitor` once with `loopPrompt` set to {quoted}; keep `delaySeconds` at 1200-1800 as the fallback heartbeat.",
|
|
74
|
+
"3. Write your update for the user, then call `schedule_wakeup` directly with `delaySeconds` (60-3600), a one-sentence `reason`, and `prompt` set to the same `/loop` input verbatim ({quoted}). It must be the only tool call in that assistant response, not through codemode or in a batch. The turn ends as soon as it returns, so call it last.",
|
|
74
75
|
"{delayAdvice}",
|
|
75
|
-
"3. If the next iteration waits on an external event and no monitor is running for it, arm `Monitor` once with `loopPrompt` set to {quoted}; keep `delaySeconds` at 1200-1800 as the fallback heartbeat.",
|
|
76
76
|
"4. If the task is complete or cannot progress without the user, call `schedule_wakeup` with `stop: true` and say why.",
|
|
77
77
|
"",
|
|
78
78
|
"## status and stop",
|
|
@@ -94,6 +94,8 @@ const LOOP_GUIDELINES = [
|
|
|
94
94
|
"- To continue, call the `schedule_wakeup` tool with `delaySeconds` (60-3600), a one-sentence `reason`,",
|
|
95
95
|
" and `prompt` set to the same /loop prompt verbatim: {quoted}",
|
|
96
96
|
"- Write your update for the user before you call `schedule_wakeup`: the turn ends as soon as it returns.",
|
|
97
|
+
"- Call it directly as the only tool call in that assistant response, never through codemode or in a batch.",
|
|
98
|
+
" Finish any Monitor or other tool calls in earlier responses, before this final wakeup call.",
|
|
97
99
|
DELAY_ADVICE,
|
|
98
100
|
"- If the task is complete, or the loop cannot move further without the user, call `schedule_wakeup`",
|
|
99
101
|
" with `stop: true` instead and say why.",
|
|
@@ -112,7 +114,7 @@ const MONITOR_GUIDELINES = [
|
|
|
112
114
|
"this step while a monitor for it is still running.",
|
|
113
115
|
"While a monitor is armed, keep `delaySeconds` at 1200-1800: the monitor is the wake signal and the",
|
|
114
116
|
"`schedule_wakeup` delay is only the fallback heartbeat. When a monitor event wakes you, handle the event,",
|
|
115
|
-
"then decide whether the loop continues.",
|
|
117
|
+
"then decide whether the loop continues. Finish monitor calls before the final, standalone `schedule_wakeup` call.",
|
|
116
118
|
].join("\n");
|
|
117
119
|
|
|
118
120
|
/**
|
|
@@ -215,8 +217,8 @@ Returns a job ID you can pass to cron_delete.`;
|
|
|
215
217
|
* /loop request, not a scheduled tick). Claude Code's /loop skill says "Then
|
|
216
218
|
* immediately run it now ... Don't wait for the first cron fire", and its model
|
|
217
219
|
* reaches that skill for "every 5 minutes ..." typed without /loop: the skill is
|
|
218
|
-
* listed every turn and CronCreate is a deferred tool.
|
|
219
|
-
*
|
|
220
|
+
* listed every turn and CronCreate is a deferred tool. This package exposes
|
|
221
|
+
* cron_create directly, so its result supplies that guidance instead. A
|
|
220
222
|
* tool result is appended at the end of the transcript; the cache is untouched.
|
|
221
223
|
*/
|
|
222
224
|
export const RUN_NOW_HINT =
|
|
@@ -229,7 +231,7 @@ export const CRON_DELETE_DESCRIPTION =
|
|
|
229
231
|
export const CRON_LIST_DESCRIPTION =
|
|
230
232
|
"List the scheduled jobs, both durable (.pi/scheduled_tasks.json) and session-only, with their ids, schedules, actual next run times and prompts.";
|
|
231
233
|
|
|
232
|
-
export const SCHEDULE_WAKEUP_DESCRIPTION = `Schedule when to resume work in a self-paced /loop \u2014 the user invoked /loop without an interval, asking you to self-pace iterations of a task. Call it as the last thing in the turn to keep the loop alive: the turn ends as soon as it returns, so write your update for the user first. To end the loop, call it with stop: true (omit every other field).
|
|
234
|
+
export const SCHEDULE_WAKEUP_DESCRIPTION = `Schedule when to resume work in a self-paced /loop \u2014 the user invoked /loop without an interval, asking you to self-pace iterations of a task. Call it as the last thing in the turn to keep the loop alive: the turn ends as soon as it returns, so write your update for the user first. Call it directly as the only tool call in that assistant response, never through codemode or in a batch. Finish Monitor and other tools in earlier responses; unsafe arm calls are rejected without scheduling. To end the loop, call it with stop: true (omit every other field); stopping does not force the turn to end, so explain the outcome afterward.
|
|
233
235
|
|
|
234
236
|
Pass the same /loop prompt back via \`prompt\` each turn so the next firing repeats the task. For a bare /loop (no prompt), pass the literal sentinel "${LOOP_FILE_SENTINEL}": it expands at fire time to the current loop.md tasks, or to the built-in maintenance pass when there is no loop.md.
|
|
235
237
|
|
package/settings.ts
CHANGED
|
@@ -326,6 +326,9 @@ async function openSettingsPanel(ctx: ExtensionCommandContext, apply: ApplySetti
|
|
|
326
326
|
tui.requestRender();
|
|
327
327
|
},
|
|
328
328
|
};
|
|
329
|
+
}, {
|
|
330
|
+
// Inline screen: tell the terminal (OSC 7501) Pi is waiting on the user.
|
|
331
|
+
blocked: { kind: "question", message: "Changing extension settings" },
|
|
329
332
|
});
|
|
330
333
|
return true;
|
|
331
334
|
}
|