pi-cc-loop 0.7.9 → 0.8.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 CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.8.0] - 2026-11-10
6
+
7
+ ### Added
8
+ - Shared native Monitor/Loop task overlay with All, Monitor and Loop tabs, task details, bounded sanitized log tails, and locally confirmed stop actions. It opens during a running turn without invoking the model, changing tools or interrupting the main agent.
9
+ - A compact themed task card with solid backgrounds, selection/tab highlighting, responsive terminal resizing, pagination and detail scrolling, including dark/light and truecolor/256-color coverage.
10
+ - SDK, lifecycle, appearance and sanitization regression tests for the task panel, shared registry and command selection.
11
+
12
+ ### Changed
13
+ - Register exactly one task-panel command after startup discovery: `/tasks` when the name is free, otherwise `/cc-tasks` when already-loaded extension commands or prompt templates own it. No unconditional alias or legacy underscore entry is registered.
14
+ - Scope the scheduler and shared task registry to the owning Pi runtime. Either package works alone; co-installation shares one panel and reload cleans up the old instance.
15
+ - Panel stop actions reuse the existing scheduler, durable-store and monitor lifecycles. Stopping a self-paced loop also retires its bound monitors and prevents late wakeups from reviving it.
16
+
17
+ ### Fixed
18
+ - Persist the actual armed next-fire time when acquiring a durable task and preserve finite stored next-fire times for foreign tasks, including overdue tasks waiting for their owner to become idle.
19
+
5
20
  ## [0.7.9] - 2026-11-03
6
21
 
7
22
  ### Added
package/README.md CHANGED
@@ -37,6 +37,7 @@ ln -sfn "$PWD" ~/.pi/agent/extensions/pi-cc-loop
37
37
  | 过期 | 周期任务 7 天后最后执行一次再删除;一次性任务不过期 | 自动 |
38
38
  | 次数上限 | `maxFires`,到顶自动删除 | `--max N` |
39
39
  | 持久化 | `durable: true` 写入 `.pi/scheduled_tasks.json`,重启后继续;同一项目只由一个 Pi 会话执行 | `--durable` |
40
+ | 任务面板 | 一个原生的 TUI 浮层,统一查看本会话的 Loop 和 Monitor 任务;本地命令,不进对话 | `/tasks`(重名时改用 `/cc-tasks`,二选一) |
40
41
  | 底部摘要 | Pi 底部状态栏一行:`loop 2 · next 3m`,无任务时不占位 | `/pi_cc footer on\|off` |
41
42
  | 界面设置 | 按插件分页的设置面板,本地命令,不进对话 | `/pi_cc` |
42
43
 
@@ -50,9 +51,11 @@ ln -sfn "$PWD" ~/.pi/agent/extensions/pi-cc-loop
50
51
  /loop stop [id] 停止一个或全部
51
52
  /loop help 本地:帮助
52
53
  /pi_cc 本地:pi-cc 设置面板(见下)
54
+ /tasks 本地:任务面板(All / Monitor / Loop),名称空闲时注册
55
+ /cc-tasks 本地:/tasks 已占用时改用;不与 /tasks 同时注册
53
56
  ```
54
57
 
55
- 前五个会作为普通 user 消息进入对话(内容就是你输入的那行),模型再按技能说明调用工具;后两个只改界面,不进对话。
58
+ 前五个会作为普通 user 消息进入对话(内容就是你输入的那行),模型再按技能说明调用工具;`/loop help`、`/pi_cc` 和选中的任务面板入口(`/tasks` 或 `/cc-tasks`)只改界面,不进对话。
56
59
  旧的 `/loop footer`、`/loop settings` 会提示改用 `/pi_cc`,不会当成 loop 请求。
57
60
 
58
61
  间隔写法没有约束,模型自己解析:`30s`、`5m`、`2h`、`1d`、`check the deploy every 20m`、
@@ -69,6 +72,39 @@ ln -sfn "$PWD" ~/.pi/agent/extensions/pi-cc-loop
69
72
  `/reload` 先由旧实例释放锁,新实例再获取;持锁进程退出或心跳超过 60 秒未更新时,其他实例可以接管。
70
73
  旧实例不会续写或删除新实例的锁。旧版没有实例令牌的锁仍被尊重,释放或过期后才换成新版格式;任务文件无需迁移。
71
74
 
75
+ ## 任务面板 `/tasks`
76
+
77
+ 启动时二选一注册:`/tasks` 未被占用就只注册 `/tasks`;已被占用就只注册 `/cc-tasks`(连字符),不再注册旧的 `/cc_tasks`(下划线)。选中的入口打开原生的 Pi 浮层(`ctx.ui.custom(..., { overlay: true })`),不是编辑器里的快捷键,也不经过模型:
78
+ 打开、刷新、关闭都不会发消息、不会开新一轮、不会改工具集,模型正在回复时也可以打开。RPC 模式通过 `ctx.ui.notify` 输出一段文字摘要;print/JSON 模式是安全 no-op(不调用 TUI 方法,也不输出摘要)。
79
+
80
+ ```text
81
+ ╭──────────────────────────────────────────────────────────────────╮
82
+ │ Tasks Esc close │
83
+ │ All 3 / Monitor 2 / Loop 1 │
84
+ ├──────────────────────────────────────────────────────────────────┤
85
+ │ ▸ M running deploy errors · 4 ev · 1m 12s │
86
+ │ M exited health check · 2 ev · 5s │
87
+ │ L scheduled watch CI · next 2m · 3 fires │
88
+ ├──────────────────────────────────────────────────────────────────┤
89
+ │ Tab switch · ↑↓ select · Enter detail · x stop · Esc close │
90
+ ╰──────────────────────────────────────────────────────────────────╯
91
+ ```
92
+
93
+ - **范围**:只列本会话的任务;本项目的 durable 任务按锁的归属显示,别的会话执行的任务标成 foreign,不会说成“正在这里运行”。不做跨会话的全局面板。
94
+ - **Tab / Shift+Tab**:All / Monitor / Loop 三页。上下键走 Pi 的 `tui.select.up/down` 命名动作,所以会跟随你的 keybindings;PgUp/PgDn 翻页。选中项用 `provider + id` 记录,刷新或增删任务后不会跳到别的任务上。
95
+ - **Enter**:详情。Esc 先回列表,再按一次才关面板;关面板只关 UI,不取消任何任务,也不会把 Esc 传给 app.interrupt。
96
+ - **Loop 详情**:cron 还是自定步调、实际的下次时间(`nextFireAt` / `scheduledFor`,已含错峰,不是从 `cron_list` 文本里猜的)、模型给的 reason、触发次数、是否 pending 等待 agent 空闲、是否 active / 等待重新调度、durable 归属;还会区分 `--durable` 里别的会话执行或从别的目录复制来的任务。
97
+ - **Monitor 详情**:状态、命令或 WebSocket、事件数与限流丢弃数、已运行时间、超时/常驻、退出码、绑定的 loop;stdout/stderr 只读**有上限的尾部**(约最后 8KB / 60 行),ANSI 和控制字符会被剥掉。日志不存在或读不了只显示说明,不影响 monitor;读日志也不会喂给 Monitor 的事件流。
98
+ - **刷新**:任务变化会通知面板,但会按 250ms 合并;时间显示每秒走一次。不会每来一行 stdout 就重绘。窗口窄、调整终端大小、宽字符(中文/emoji)、主题切换都有处理,正文高度有上限并可滚动。
99
+ - **`x` 停止**:先本地确认(`y` / `n`),不经过模型。它复用各包自己的停止逻辑:
100
+ - Monitor 走 `endMonitor`:结束进程树或关闭 socket,并照常发一条 `[Monitor stopped]`。
101
+ - 自定步调 loop 走 `retireChain`:记下已停止的 prompt 防止迟到的 `schedule_wakeup` 复活,并通过 `loop:stopped` 连带停止绑定的 Monitor。
102
+ - cron / durable 走 `removeJob` / durable store 删除;别的会话执行的 durable 任务只删存储里的那一条。
103
+ - **停止的是计划,不是主 agent**:不会 abort 当前回复,也不会结束正在运行的 turn;要中断模型请照常按 Esc 或用 Pi 的 abort。
104
+ - 已结束的 Monitor 记录仍会列出,但停止动作禁用。v1 不做暂停/恢复、不改 cron、不做每个 loop 的独立 transcript,也不保留已结束 loop 的历史。
105
+
106
+ 两个包共用一套命令注册:只装 loop 或只装 monitor 都能使用;两个都装时也只注册一个面板入口。所有 `session_start` 处理完成、构建补全列表之前,会检查当时已加载的扩展命令和 prompt 模板(包括已重名的 `/tasks:1` 等入口):名称空闲时只注册 `/tasks`;已占用时不抢占、不新增 `/tasks:1`,只注册 `/cc-tasks`。无法安全查询命令列表的旧版宿主也只提供 `/cc-tasks`。同一运行时不因再次资源发现而切换或补注册别名;`/reload` 会为新的扩展实例重新检查。检查不覆盖其他插件之后动态新增的命令。任务面板入口不是 `/pi_cc`(设置面板),也不改变 `/loop` 的语义。
107
+
72
108
  ## 工具
73
109
 
74
110
  | 工具 | 界面显示 | 说明 |
@@ -175,7 +211,7 @@ Loop & Monitor / Plugins / Compact ~/.pi/agent/pi-cc.json
175
211
  ## 兼容性
176
212
 
177
213
  - Node.js `>=22.19.0`
178
- - Pi 1.3.0、1.4.0(测试通过)
214
+ - Pi 1.5.0、1.5.1(本次发版检查通过);Pi 1.3.0 / 1.4.0 本次未重新验收
179
215
  - `pi-cc-monitor` 可选:自定步调 loop 需要等外部事件时会用它,loop 结束时连带停止;两者通过 `pi.events` 通信
180
216
  - 与 `pi-cc-extensions` 共存。本扩展不写其他插件的配置文件;旧版本在 `pi-cc-extensions.json` 里加过的
181
217
  `cron_create`、`cron_delete`、`cron_list`、`schedule_wakeup` 这几条 `excludeRenderers` 可以删掉
@@ -188,7 +224,7 @@ npx --yes -p typescript@5.9.3 tsc --noEmit -p tsconfig.json
188
224
  node scripts/cache-check.mjs # 真实 provider 的缓存检查,默认用免费模型,发版前跑
189
225
  ```
190
226
 
191
- 四个仓库共同使用的 `config.ts`、`settings.ts`(以及 loop/monitor 的 `render.ts`)以本仓库为维护源。修改后在本仓库运行 `node scripts/sync-shared.mjs` 同步到同级仓库;`node scripts/sync-shared.mjs --check` 只检查,不改文件。各仓库的 `tests/shared.test.mjs` 也会检查副本一致。
227
+ 四个仓库共同使用的 `config.ts`、`settings.ts`(以及 loop/monitor 的 `render.ts`、`tasks.ts`)以本仓库为维护源。修改后在本仓库运行 `node scripts/sync-shared.mjs` 同步到同级仓库;`node scripts/sync-shared.mjs --check` 只检查,不改文件。各仓库的 `tests/shared.test.mjs` 也会检查副本一致。
192
228
 
193
229
  ### 缓存约束
194
230
 
package/durable.ts CHANGED
@@ -402,6 +402,11 @@ export class DurableRuntime {
402
402
  this.save(job);
403
403
  } else {
404
404
  this.acquireAndLoad(now);
405
+ // The new job was loaded from the file and armed in the scheduler.
406
+ // Persist that already-jittered time so another session's read-only
407
+ // view does not have to recompute it from a later clock.
408
+ const loaded = this.options.scheduler.get(job.id);
409
+ if (loaded) this.save(loaded);
405
410
  }
406
411
  return this.owner ? { runsHere: true } : { runsHere: false, holderPid: this.holderPid() };
407
412
  }
@@ -433,11 +438,22 @@ export class DurableRuntime {
433
438
  return this.store
434
439
  .read()
435
440
  .filter((job) => !this.options.scheduler.get(job.id))
436
- .map((job) => ({
437
- ...job,
438
- nextFireAt: nextRunTime(job, job.recurring ? Date.now() : job.createdAt, this.options.timing()) ?? undefined,
439
- foreignReason: this.ownsProject(job) ? ("other-session" as const) : ("other-folder" as const),
440
- }));
441
+ .map((job) => {
442
+ // Prefer the owner's persisted, already-jittered run time. It may be
443
+ // in the past when the owner is busy and the fire is waiting for
444
+ // idle: keeping it is the honest read-only snapshot. Only old or
445
+ // broken files need a computed fallback.
446
+ const storedNext = typeof job.nextFireAt === "number" && Number.isFinite(job.nextFireAt) ? job.nextFireAt : undefined;
447
+ const nextFireAt =
448
+ storedNext ??
449
+ nextRunTime(job, job.recurring ? Date.now() : job.createdAt, this.options.timing()) ??
450
+ undefined;
451
+ return {
452
+ ...job,
453
+ nextFireAt,
454
+ foreignReason: this.ownsProject(job) ? ("other-session" as const) : ("other-folder" as const),
455
+ };
456
+ });
441
457
  }
442
458
 
443
459
  private ownsProject(job: LoopJob): boolean {
package/index.ts CHANGED
@@ -58,6 +58,7 @@ import {
58
58
  } from "./prompts.ts";
59
59
  import { formatClock, formatEta, renderScheduledMessage } from "./renderers.ts";
60
60
  import { CONFIG_CHANNEL, registerPiCcCommand } from "./settings.ts";
61
+ import { registerTaskProvider, type TaskDetail, type TaskItem, type TaskStopResult } from "./tasks.ts";
61
62
  import {
62
63
  DEFAULT_TIMING,
63
64
  generateId,
@@ -143,6 +144,15 @@ function normalizePrompt(prompt: string): string {
143
144
  return prompt.trim().replace(/^\/loop\s+/i, "").trim();
144
145
  }
145
146
 
147
+ /** A clock time for the task panel: today gets "6:16pm", an older date its month and day. */
148
+ function dateTime(epochMs: number | undefined): string {
149
+ if (epochMs === undefined || !Number.isFinite(epochMs)) return "\u2014";
150
+ const date = new Date(epochMs);
151
+ if (Number.isNaN(date.getTime())) return "?";
152
+ if (date.toDateString() === new Date().toDateString()) return formatClock(epochMs);
153
+ return `${date.toLocaleDateString(undefined, { month: "short", day: "numeric" })} ${formatClock(epochMs)}`;
154
+ }
155
+
146
156
  /** Pi only terminates a batch when every call agrees; never arm from a mixed or nested call. */
147
157
  function requireStandaloneWakeup(toolCallId: string, ctx: Pick<ExtensionContext, "sessionManager">): void {
148
158
  const branch = ctx.sessionManager.getBranch?.() ?? [];
@@ -167,11 +177,15 @@ function jobValue(job: LoopJob): Record<string, string | number | boolean> {
167
177
  >;
168
178
  }
169
179
 
170
- /** The live scheduler, for tests. */
171
- let activeScheduler: LoopScheduler | null = null;
180
+ /**
181
+ * The scheduler of one extension instance, keyed by its own `pi` object. Tests
182
+ * and the task-panel provider use this instead of a process-global "last
183
+ * scheduler": several sessions or subagents in one process must not mix.
184
+ */
185
+ const schedulers = new WeakMap<ExtensionAPI, LoopScheduler>();
172
186
 
173
- export function getScheduler(): LoopScheduler | null {
174
- return activeScheduler;
187
+ export function getScheduler(pi: ExtensionAPI): LoopScheduler | undefined {
188
+ return schedulers.get(pi);
175
189
  }
176
190
 
177
191
  export default function piCcLoopExtension(pi: ExtensionAPI): void {
@@ -209,6 +223,7 @@ export default function piCcLoopExtension(pi: ExtensionAPI): void {
209
223
  onChange: (jobs) => {
210
224
  persist(jobs);
211
225
  refreshStatus(jobs);
226
+ tasks.changed();
212
227
  },
213
228
  canDispatch: (job) => {
214
229
  const ctx = lastCtx;
@@ -236,7 +251,7 @@ export default function piCcLoopExtension(pi: ExtensionAPI): void {
236
251
  refreshStatus();
237
252
  },
238
253
  });
239
- activeScheduler = scheduler;
254
+ schedulers.set(pi, scheduler);
240
255
 
241
256
  // ---------------------------------------------------------------- helpers
242
257
 
@@ -662,6 +677,151 @@ export default function piCcLoopExtension(pi: ExtensionAPI): void {
662
677
  notify(ctx, help, "info");
663
678
  }
664
679
 
680
+ // ------------------------------------------------------------- task panel
681
+
682
+ /** One job by id, including a durable job another session runs. */
683
+ function jobFor(id: string): (LoopJob & { foreignReason?: "other-session" | "other-folder" }) | undefined {
684
+ return scheduler.get(id) ?? durable?.foreignJobs().find((job) => job.id === id);
685
+ }
686
+
687
+ /** The live state a pending/wakeup job is in right now. */
688
+ function jobState(job: LoopJob & { foreignReason?: string }, now: number): string {
689
+ if (job.foreignReason) return job.foreignReason === "other-folder" ? "foreign (other folder)" : "foreign (other session)";
690
+ if (job.pending) return "pending (waiting for idle)";
691
+ if (job.kind === "wakeup") {
692
+ if (job.keepaliveTurn) return "keepalive turn";
693
+ if (job.awaitingReschedule) {
694
+ try {
695
+ return lastCtx?.isIdle() === false ? "active (turn running)" : "active / awaiting reschedule";
696
+ } catch {
697
+ return "active / awaiting reschedule";
698
+ }
699
+ }
700
+ }
701
+ const at = job.kind === "cron" ? job.nextFireAt : job.scheduledFor;
702
+ if (at !== undefined && at <= now) return "due";
703
+ return at !== undefined ? "scheduled" : "armed";
704
+ }
705
+
706
+ function jobItem(job: LoopJob & { foreignReason?: string }, now: number): TaskItem {
707
+ const at = job.kind === "cron" ? job.nextFireAt : job.scheduledFor;
708
+ const meta: string[] = [];
709
+ if (job.pending) meta.push("waiting for idle");
710
+ else if (job.foreignReason) meta.push(job.foreignReason === "other-folder" ? "other folder" : "other session");
711
+ else if (at !== undefined) meta.push(`${at <= now ? "due" : "next"} ${at <= now ? "now" : formatEta(at - now)}`);
712
+ meta.push(`${job.fireCount ?? 0} fire${job.fireCount === 1 ? "" : "s"}`);
713
+ if (job.durable) meta.push("durable");
714
+ return {
715
+ id: job.id,
716
+ title: displayPrompt(job.prompt),
717
+ state: jobState(job, now).replace(/ \(.*$/, ""),
718
+ active: true,
719
+ meta: meta.join(" · "),
720
+ order: job.createdAt,
721
+ };
722
+ }
723
+
724
+ function jobDetail(job: LoopJob & { foreignReason?: string }, now: number): TaskDetail {
725
+ const at = job.kind === "cron" ? job.nextFireAt : job.scheduledFor;
726
+ const state = jobState(job, now);
727
+ const ownership = job.foreignReason
728
+ ? job.foreignReason === "other-folder"
729
+ ? "durable · copied from another folder (listed, not run here)"
730
+ : "durable · run by another Pi session in this project"
731
+ : job.durable
732
+ ? durable?.owner
733
+ ? "durable · run by this session"
734
+ : "durable · loaded, but another session holds the scheduler lock"
735
+ : "session only";
736
+ const fires = `${job.fireCount ?? 0}${job.maxFires !== undefined ? `/${job.maxFires}` : ""}` +
737
+ (job.lastFiredAt !== undefined ? ` · last ${dateTime(job.lastFiredAt)}` : "");
738
+ const rows = [
739
+ { label: "Kind", value: job.kind === "cron" ? "fixed interval (cron)" : "self-paced wakeup" },
740
+ { label: "State", value: state },
741
+ {
742
+ label: "Schedule",
743
+ value: job.kind === "cron" ? `${job.cron ?? "?"} (${humanSchedule(job.cron ?? "")})` : "self-paced: the model picks the next delay",
744
+ },
745
+ {
746
+ label: "Next run",
747
+ value:
748
+ at === undefined
749
+ ? "unscheduled"
750
+ : `${dateTime(at)} · ${at <= now ? "due now" : `in ${formatEta(at - now)}`} · ${at} ms`,
751
+ },
752
+ { label: "Reason", value: job.reason ?? "—" },
753
+ { label: "Fires", value: fires },
754
+ {
755
+ label: "Pending",
756
+ value: job.pending
757
+ ? `yes · since ${dateTime(job.dueAt)} · waiting for the agent to go idle`
758
+ : "no",
759
+ },
760
+ {
761
+ label: "Chain",
762
+ value:
763
+ job.kind === "wakeup"
764
+ ? job.awaitingReschedule
765
+ ? "active · the turn is running or waiting to reschedule"
766
+ : job.keepaliveUsed
767
+ ? "keepalive used"
768
+ : "armed"
769
+ : "fixed interval · fires its own next tick",
770
+ },
771
+ { label: "Ownership", value: ownership },
772
+ { label: "Prompt", value: displayPrompt(job.prompt) },
773
+ { label: "Source", value: job.source },
774
+ { label: "Created", value: dateTime(job.createdAt) },
775
+ { label: "Project", value: job.createdInProject ?? "—" },
776
+ { label: "Keepalive", value: job.keepaliveUsed ? (job.keepaliveTurn ? "this turn is the keepalive" : "used once") : "not used" },
777
+ ] satisfies TaskDetail["rows"];
778
+ return {
779
+ rows,
780
+ note: job.foreignReason
781
+ ? "Not running here. x removes it from .pi/scheduled_tasks.json; it does not abort another session's turn."
782
+ : "x removes the schedule (and any Monitor bound to a stopped self-paced loop). It does not abort the main agent.",
783
+ };
784
+ }
785
+
786
+ function stopTask(id: string): TaskStopResult {
787
+ const job = scheduler.get(id);
788
+ if (job) {
789
+ if (job.kind === "wakeup") retireChain(job, "stopped from task panel");
790
+ else removeJob(job);
791
+ tasks.changed();
792
+ return { stopped: true, message: `Cancelled ${job.kind === "wakeup" ? "self-paced loop" : "scheduled job"} ${id}.` };
793
+ }
794
+ // A durable job this session does not run: cancel its store entry, as
795
+ // cron_delete does, without touching any session's current turn.
796
+ const foreign = durable?.foreignJobs().find((candidate) => candidate.id === id);
797
+ if (foreign && durable?.remove(id)) {
798
+ tasks.changed();
799
+ return {
800
+ stopped: true,
801
+ message: `Cancelled durable job ${id} (${foreign.foreignReason === "other-folder" ? "copied from another folder" : "run by another session"}).`,
802
+ };
803
+ }
804
+ return { stopped: false, message: `No scheduled job with id ${id}.` };
805
+ }
806
+
807
+ // The shared task overlay. The provider reads the live LoopJob objects
808
+ // (never cron_list's text, which drops pending/awaitingReschedule), so the
809
+ // next run is the already-jittered time the scheduler will actually use.
810
+ const tasks = registerTaskProvider(pi, {
811
+ id: "loop",
812
+ label: "Loop",
813
+ list(): TaskItem[] {
814
+ const now = Date.now();
815
+ return listJobs().map((job) => jobItem(job, now));
816
+ },
817
+ detail(item: TaskItem): TaskDetail {
818
+ const job = jobFor(item.id);
819
+ if (!job) throw new Error("this job is no longer listed");
820
+ return jobDetail(job, Date.now());
821
+ },
822
+ stop: stopTask,
823
+ });
824
+
665
825
  // The shared pi-cc settings and their /pi_cc command. The first pi-cc
666
826
  // package to load registers it; this one re-claims its own claim on /reload.
667
827
  registerPiCcCommand(pi, "pi-cc-loop");
@@ -1168,6 +1328,8 @@ export default function piCcLoopExtension(pi: ExtensionAPI): void {
1168
1328
  });
1169
1329
 
1170
1330
  pi.on("session_shutdown", async () => {
1331
+ // Close an open task overlay while the context is still valid.
1332
+ tasks.close();
1171
1333
  // Pi clears extension statuses when it unloads, but a reload should not
1172
1334
  // leave a stale count behind until then.
1173
1335
  try {
@@ -1179,6 +1341,7 @@ export default function piCcLoopExtension(pi: ExtensionAPI): void {
1179
1341
  durable?.stop();
1180
1342
  durable = undefined;
1181
1343
  scheduler.clear(false);
1344
+ schedulers.delete(pi);
1182
1345
  });
1183
1346
 
1184
1347
  // -------------------------------------------------------------- restoring
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cc-loop",
3
- "version": "0.7.9",
3
+ "version": "0.8.0",
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",