shariq-pi-extensions 0.2.22 → 0.2.24
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 +1 -0
- package/docs/ARCHITECTURE.md +1 -0
- package/docs/EXTENSIONS.md +6 -0
- package/extensions/antigravity-provider/README.md +1 -1
- package/extensions/antigravity-provider/antigravity/accounts.ts +45 -6
- package/extensions/antigravity-provider/antigravity/dashboard.ts +11 -4
- package/extensions/antigravity-provider/antigravity/index.ts +52 -17
- package/extensions/subagents/README.md +2 -2
- package/extensions/subagents/src/backends/pi.ts +1 -0
- package/extensions/task-list/README.md +107 -0
- package/extensions/task-list/index.ts +431 -0
- package/extensions/task-list/state.ts +225 -0
- package/extensions/task-list/types.ts +58 -0
- package/extensions/task-list/ui.ts +263 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -31,6 +31,7 @@ The package contains:
|
|
|
31
31
|
- Firecrawl search and scraping
|
|
32
32
|
- Git status UI
|
|
33
33
|
- persistent task goals
|
|
34
|
+
- branch-safe model-maintained task lists with live progress UI and compaction continuity
|
|
34
35
|
- configurable steer, interrupt, or follow-up input behavior
|
|
35
36
|
- dedicated multi-agent orchestration
|
|
36
37
|
- Smart Compaction with high-fidelity checkpointing, delta-merging, and custom model routing
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -43,6 +43,7 @@ Installed package directories are treated as immutable. Extensions resolve writa
|
|
|
43
43
|
- Pi Memory state: `<agent-dir>/pi-memory/`
|
|
44
44
|
- Subagent configuration and catalog: paths derived from `getAgentDir()`
|
|
45
45
|
- Orchestration settings and run ledgers: `<agent-dir>/orchestration/`
|
|
46
|
+
- Goal and Task List state: branch-local Pi session entries; neither writes a separate runtime-state file
|
|
46
47
|
- project configuration: paths derived from Pi's `CONFIG_DIR_NAME`
|
|
47
48
|
|
|
48
49
|
Credentials remain in Pi auth storage, environment variables, service credential stores, or ignored machine-local files. Package source never contains an API key, OAuth token, database, cache, or session.
|
package/docs/EXTENSIONS.md
CHANGED
|
@@ -34,6 +34,12 @@ The curated catalog intentionally excludes `-fast` variants and the removals doc
|
|
|
34
34
|
|
|
35
35
|
The goal extension adds persistent, branch-safe objectives, progress evidence, budgets, pause/resume controls, and strict completion/blocker gates. Use `/goal` for the operator UI and the `create_goal`, `get_goal`, `update_goal_progress`, and `update_goal` tools for agent-controlled state.
|
|
36
36
|
|
|
37
|
+
### [Task List](../extensions/task-list/README.md)
|
|
38
|
+
|
|
39
|
+
`task_list` gives the active model a branch-safe ordered checklist for ordinary multi-step work; it is separate from persistent Goals and may be used alongside them. Writes replace the full list, retain stable IDs, support pending/in-progress/completed/blocked/cancelled states and priorities, and require an active item while pending work remains. The prompt contract requires same-message list/action calls and verified updates only at task-level transitions—not after routine file reads, edits, commands, or other tool calls. A runtime reminder catches multi-action work that starts without a list; final reconciliation catches unfinished bookkeeping.
|
|
40
|
+
|
|
41
|
+
State is stored in Pi session entries and reconstructed on resume, reload, and tree navigation. Active work is re-injected when compaction removes the latest snapshot from model context. `/tasks` opens the interactive dashboard and direct editor; the compact live widget auto-hides after all work finishes. Subagents receive the same tool under every capability policy, but each child maintains its own session-local list rather than changing the parent's list.
|
|
42
|
+
|
|
37
43
|
### [Subagents](../extensions/subagents/README.md)
|
|
38
44
|
|
|
39
45
|
The subagent extension runs flat Pi child agents with profiles, capability policies, continuation, result delivery, optional worktrees, pre-warmed task dispatch, instant cascading cancellation, cross-session persistence, and a dashboard. Configuration lives in `<agent-dir>/subagents.json`; trusted projects may override it through their Pi config directory. The configured concurrency ceiling is 50.
|
|
@@ -10,7 +10,7 @@ Local persistent Pi provider for Google Antigravity-compatible models.
|
|
|
10
10
|
|
|
11
11
|
The provider includes an IPv4 OAuth token-exchange fallback for Node environments where the default request fails. Its curated catalog follows current Antigravity model identifiers and runtime behavior.
|
|
12
12
|
|
|
13
|
-
Successful logins are added to `<agent-dir>/antigravity/accounts.json`, written with owner-only permissions.
|
|
13
|
+
Successful logins are added to `<agent-dir>/antigravity/accounts.json`, written with owner-only permissions. A missing account file triggers one-time migration of an existing Pi OAuth credential; after that, the valid account file is authoritative, so removing an account cannot be undone by a stale credential in `auth.json`. Requests select the least recently used eligible account, skip disabled/cooling/exhausted accounts, refresh expiring OAuth tokens, and rotate to another account when an auth, rate, quota, or capacity failure occurs before response streaming begins. Cached per-model remaining quota and reset times guide selection; `/antigravity` refreshes the authoritative catalog, uses `d` to reversibly enable or disable an account, and uses `x` plus confirmation to remove one permanently.
|
|
14
14
|
|
|
15
15
|
Current public model IDs:
|
|
16
16
|
- `antigravity/gemini-3.7-flash`
|
|
@@ -48,6 +48,22 @@ interface AntigravityAccountFile {
|
|
|
48
48
|
accounts: AntigravityAccount[];
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
export type AntigravityAccountStore = {
|
|
52
|
+
status: "missing" | "invalid" | "valid";
|
|
53
|
+
accounts: AntigravityAccount[];
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type AntigravityStoredCredential = Pick<OAuthCredentials, "refresh" | "access" | "expires"> & {
|
|
57
|
+
projectId?: string;
|
|
58
|
+
email?: string;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export type AntigravityCredentialReconciliation =
|
|
62
|
+
| { action: "none" }
|
|
63
|
+
| { action: "migrate"; credentials: AntigravityStoredCredential }
|
|
64
|
+
| { action: "replace"; account: AntigravityAccount }
|
|
65
|
+
| { action: "delete" };
|
|
66
|
+
|
|
51
67
|
export interface AntigravityAccountStatus extends AntigravityAccount {
|
|
52
68
|
active: boolean;
|
|
53
69
|
}
|
|
@@ -105,17 +121,40 @@ function normalizeAccount(value: unknown): AntigravityAccount | undefined {
|
|
|
105
121
|
};
|
|
106
122
|
}
|
|
107
123
|
|
|
108
|
-
export function
|
|
124
|
+
export function inspectAntigravityAccountStore(): AntigravityAccountStore {
|
|
109
125
|
try {
|
|
110
126
|
const parsed = JSON.parse(fs.readFileSync(ANTIGRAVITY_ACCOUNTS_PATH, "utf8")) as Partial<AntigravityAccountFile>;
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
:
|
|
114
|
-
|
|
115
|
-
|
|
127
|
+
if (!Array.isArray(parsed.accounts)) return { status: "invalid", accounts: [] };
|
|
128
|
+
return {
|
|
129
|
+
status: "valid",
|
|
130
|
+
accounts: parsed.accounts.map(normalizeAccount).filter((account): account is AntigravityAccount => Boolean(account)),
|
|
131
|
+
};
|
|
132
|
+
} catch (error) {
|
|
133
|
+
const code = error && typeof error === "object" && "code" in error ? (error as { code?: unknown }).code : undefined;
|
|
134
|
+
return { status: code === "ENOENT" ? "missing" : "invalid", accounts: [] };
|
|
116
135
|
}
|
|
117
136
|
}
|
|
118
137
|
|
|
138
|
+
export function loadAntigravityAccounts(): AntigravityAccount[] {
|
|
139
|
+
return inspectAntigravityAccountStore().accounts;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function matchesStoredCredential(account: AntigravityAccount, credentials: AntigravityStoredCredential) {
|
|
143
|
+
return account.refresh === credentials.refresh || Boolean(account.email && credentials.email && account.email === credentials.email);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function reconcileAntigravityStoredCredential(
|
|
147
|
+
store: AntigravityAccountStore,
|
|
148
|
+
credentials: AntigravityStoredCredential | undefined,
|
|
149
|
+
): AntigravityCredentialReconciliation {
|
|
150
|
+
if (store.status === "invalid") return { action: "none" };
|
|
151
|
+
if (store.status === "missing") return credentials ? { action: "migrate", credentials } : { action: "none" };
|
|
152
|
+
if (credentials && store.accounts.some((account) => matchesStoredCredential(account, credentials))) return { action: "none" };
|
|
153
|
+
const replacement = store.accounts.find((account) => !account.disabled) ?? store.accounts[0];
|
|
154
|
+
if (replacement) return { action: "replace", account: replacement };
|
|
155
|
+
return credentials ? { action: "delete" } : { action: "none" };
|
|
156
|
+
}
|
|
157
|
+
|
|
119
158
|
function saveAntigravityAccounts(accounts: AntigravityAccount[]) {
|
|
120
159
|
fs.mkdirSync(ANTIGRAVITY_STATE_DIR, { recursive: true, mode: 0o700 });
|
|
121
160
|
fs.chmodSync(ANTIGRAVITY_STATE_DIR, 0o700);
|
|
@@ -78,6 +78,7 @@ export class AntigravityDashboard implements Component {
|
|
|
78
78
|
private snapshot: AntigravityDashboardSnapshot;
|
|
79
79
|
private readonly refreshData: (force: boolean) => Promise<AntigravityDashboardSnapshot>;
|
|
80
80
|
private readonly toggleAccount: (id: string, enabled: boolean) => Promise<AntigravityDashboardSnapshot>;
|
|
81
|
+
private readonly removeAccount: (id: string, label: string) => Promise<AntigravityDashboardSnapshot>;
|
|
81
82
|
private readonly done: () => void;
|
|
82
83
|
|
|
83
84
|
constructor(
|
|
@@ -87,6 +88,7 @@ export class AntigravityDashboard implements Component {
|
|
|
87
88
|
snapshot: AntigravityDashboardSnapshot,
|
|
88
89
|
refreshData: (force: boolean) => Promise<AntigravityDashboardSnapshot>,
|
|
89
90
|
toggleAccount: (id: string, enabled: boolean) => Promise<AntigravityDashboardSnapshot>,
|
|
91
|
+
removeAccount: (id: string, label: string) => Promise<AntigravityDashboardSnapshot>,
|
|
90
92
|
done: () => void,
|
|
91
93
|
) {
|
|
92
94
|
this.tui = tui;
|
|
@@ -95,6 +97,7 @@ export class AntigravityDashboard implements Component {
|
|
|
95
97
|
this.snapshot = snapshot;
|
|
96
98
|
this.refreshData = refreshData;
|
|
97
99
|
this.toggleAccount = toggleAccount;
|
|
100
|
+
this.removeAccount = removeAccount;
|
|
98
101
|
this.done = done;
|
|
99
102
|
}
|
|
100
103
|
|
|
@@ -139,10 +142,13 @@ export class AntigravityDashboard implements Component {
|
|
|
139
142
|
if (accounts.length) this.selected = (this.selected + 1) % accounts.length;
|
|
140
143
|
} else if (data === "r") {
|
|
141
144
|
this.startRefresh(true);
|
|
142
|
-
} else if (data === "d" && accounts[this.selected] && !this.refreshing) {
|
|
145
|
+
} else if ((data === "d" || data === "x") && accounts[this.selected] && !this.refreshing) {
|
|
143
146
|
const account = accounts[this.selected]!;
|
|
144
147
|
this.refreshing = true;
|
|
145
|
-
|
|
148
|
+
const action = data === "x"
|
|
149
|
+
? this.removeAccount(account.id, account.email || `account-${account.id.slice(0, 6)}`)
|
|
150
|
+
: this.toggleAccount(account.id, account.disabled === true);
|
|
151
|
+
void action
|
|
146
152
|
.then((snapshot) => this.replaceSnapshot(snapshot))
|
|
147
153
|
.catch((error) => {
|
|
148
154
|
if (!this.closed) this.snapshot = { ...this.snapshot, warning: error instanceof Error ? error.message : String(error) };
|
|
@@ -188,7 +194,7 @@ export class AntigravityDashboard implements Component {
|
|
|
188
194
|
for (let row = 0; row < bodyHeight; row++) lines.push(this.theme.fg("border", "│") + padLine(list[row] ?? "", inner) + this.theme.fg("border", "│"));
|
|
189
195
|
}
|
|
190
196
|
lines.push(frameBottom(this.theme, width));
|
|
191
|
-
lines.push(truncateToWidth(`${this.theme.fg("accent", " ↑↓ / j k")} ${this.theme.fg("dim", "select")} ${this.theme.fg("accent", "r")} ${this.theme.fg("dim", "refresh")} ${this.theme.fg("accent", "d")} ${this.theme.fg("dim", "enable/disable")} ${this.theme.fg("accent", "esc")} ${this.theme.fg("dim", "close")}`, width, ""));
|
|
197
|
+
lines.push(truncateToWidth(`${this.theme.fg("accent", " ↑↓ / j k")} ${this.theme.fg("dim", "select")} ${this.theme.fg("accent", "r")} ${this.theme.fg("dim", "refresh")} ${this.theme.fg("accent", "d")} ${this.theme.fg("dim", "enable/disable")} ${this.theme.fg("accent", "x")} ${this.theme.fg("dim", "remove")} ${this.theme.fg("accent", "esc")} ${this.theme.fg("dim", "close")}`, width, ""));
|
|
192
198
|
return lines.map((line) => truncateToWidth(line, width, ""));
|
|
193
199
|
}
|
|
194
200
|
|
|
@@ -250,10 +256,11 @@ export async function openAntigravityDashboard(
|
|
|
250
256
|
initial: AntigravityDashboardSnapshot,
|
|
251
257
|
refresh: (force: boolean) => Promise<AntigravityDashboardSnapshot>,
|
|
252
258
|
toggle: (id: string, enabled: boolean) => Promise<AntigravityDashboardSnapshot>,
|
|
259
|
+
remove: (id: string, label: string) => Promise<AntigravityDashboardSnapshot>,
|
|
253
260
|
) {
|
|
254
261
|
await ctx.ui.custom<void>(
|
|
255
262
|
(tui, theme, keys, done) => {
|
|
256
|
-
const dashboard = new AntigravityDashboard(tui, theme, keys, initial, refresh, toggle, () => done(undefined));
|
|
263
|
+
const dashboard = new AntigravityDashboard(tui, theme, keys, initial, refresh, toggle, remove, () => done(undefined));
|
|
257
264
|
queueMicrotask(() => dashboard.startRefresh(false));
|
|
258
265
|
return dashboard;
|
|
259
266
|
},
|
|
@@ -24,8 +24,12 @@ import {
|
|
|
24
24
|
import { streamAntigravity } from "./cloud-code-assist.ts";
|
|
25
25
|
import {
|
|
26
26
|
antigravityAccountStatuses,
|
|
27
|
+
inspectAntigravityAccountStore,
|
|
28
|
+
reconcileAntigravityStoredCredential,
|
|
29
|
+
removeAntigravityAccount,
|
|
27
30
|
setAntigravityAccountEnabled,
|
|
28
31
|
upsertAntigravityAccount,
|
|
32
|
+
type AntigravityStoredCredential,
|
|
29
33
|
} from "./accounts.ts";
|
|
30
34
|
import { refreshAntigravityQuotas } from "./quotas.ts";
|
|
31
35
|
import {
|
|
@@ -43,19 +47,43 @@ export default function antigravityProviderExtension(pi: ExtensionAPI) {
|
|
|
43
47
|
};
|
|
44
48
|
};
|
|
45
49
|
|
|
46
|
-
const
|
|
50
|
+
const readOAuthCredential = (): AntigravityStoredCredential | undefined => {
|
|
51
|
+
const credential = readStoredCredential(PROVIDER_ID) as any;
|
|
52
|
+
if (credential?.type !== "oauth" || typeof credential.refresh !== "string" || typeof credential.access !== "string") return undefined;
|
|
53
|
+
return {
|
|
54
|
+
refresh: credential.refresh,
|
|
55
|
+
access: credential.access,
|
|
56
|
+
expires: typeof credential.expires === "number" ? credential.expires : 0,
|
|
57
|
+
projectId: typeof credential.projectId === "string" ? credential.projectId : undefined,
|
|
58
|
+
email: typeof credential.email === "string" ? credential.email : undefined,
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const reconcileStoredCredential = async (ctx?: ExtensionContext) => {
|
|
47
63
|
try {
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
64
|
+
const credentials = readOAuthCredential();
|
|
65
|
+
const reconciliation = reconcileAntigravityStoredCredential(inspectAntigravityAccountStore(), credentials);
|
|
66
|
+
if (reconciliation.action === "migrate") {
|
|
67
|
+
upsertAntigravityAccount(reconciliation.credentials);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const authStorage = (ctx?.modelRegistry as any)?.authStorage;
|
|
71
|
+
if (!authStorage) return;
|
|
72
|
+
if (reconciliation.action === "replace") {
|
|
73
|
+
const account = reconciliation.account;
|
|
74
|
+
await authStorage.modify(PROVIDER_ID, async () => ({
|
|
75
|
+
type: "oauth",
|
|
76
|
+
refresh: account.refresh,
|
|
77
|
+
access: account.access,
|
|
78
|
+
expires: account.expires,
|
|
79
|
+
projectId: account.projectId,
|
|
80
|
+
email: account.email,
|
|
81
|
+
}));
|
|
82
|
+
} else if (reconciliation.action === "delete") {
|
|
83
|
+
await authStorage.delete(PROVIDER_ID);
|
|
84
|
+
}
|
|
57
85
|
} catch {
|
|
58
|
-
//
|
|
86
|
+
// Malformed or unavailable credential state must not block extension startup.
|
|
59
87
|
}
|
|
60
88
|
};
|
|
61
89
|
|
|
@@ -67,9 +95,9 @@ export default function antigravityProviderExtension(pi: ExtensionAPI) {
|
|
|
67
95
|
oauth: {
|
|
68
96
|
name: PROVIDER_NAME,
|
|
69
97
|
login: ((callbacks: Parameters<typeof loginAntigravity>[0]) => {
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
|
|
98
|
+
// A missing account store is a legacy installation, so archive Pi's single
|
|
99
|
+
// credential before login. A valid store remains authoritative after edits.
|
|
100
|
+
void reconcileStoredCredential();
|
|
73
101
|
return loginAntigravity(callbacks);
|
|
74
102
|
}) as any,
|
|
75
103
|
refreshToken: refreshAntigravityToken as any,
|
|
@@ -78,9 +106,9 @@ export default function antigravityProviderExtension(pi: ExtensionAPI) {
|
|
|
78
106
|
streamSimple: streamAntigravity,
|
|
79
107
|
} as any);
|
|
80
108
|
|
|
81
|
-
pi.on("session_start", (_event, ctx) => {
|
|
109
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
82
110
|
(ctx.modelRegistry as any).authStorage?.reload?.();
|
|
83
|
-
|
|
111
|
+
await reconcileStoredCredential(ctx);
|
|
84
112
|
void refreshAntigravityQuotas({ signal: ctx.signal }).catch(() => {
|
|
85
113
|
// Cached quota state remains available; /antigravity reports refresh failures.
|
|
86
114
|
});
|
|
@@ -94,7 +122,7 @@ export default function antigravityProviderExtension(pi: ExtensionAPI) {
|
|
|
94
122
|
pi.registerCommand("antigravity", {
|
|
95
123
|
description: "Open Antigravity accounts, rotation, and quota dashboard",
|
|
96
124
|
handler: async (_args, ctx) => {
|
|
97
|
-
|
|
125
|
+
await reconcileStoredCredential(ctx);
|
|
98
126
|
await openAntigravityDashboard(
|
|
99
127
|
ctx,
|
|
100
128
|
dashboardSnapshot(ctx),
|
|
@@ -107,6 +135,13 @@ export default function antigravityProviderExtension(pi: ExtensionAPI) {
|
|
|
107
135
|
if (enabled) await refreshAntigravityQuotas({ force: true, signal: ctx.signal });
|
|
108
136
|
return dashboardSnapshot(ctx);
|
|
109
137
|
},
|
|
138
|
+
async (id, label) => {
|
|
139
|
+
const confirmed = await ctx.ui.confirm("Remove Antigravity account?", `Permanently remove ${label} from this Pi installation?`);
|
|
140
|
+
if (!confirmed) return dashboardSnapshot(ctx);
|
|
141
|
+
removeAntigravityAccount(id);
|
|
142
|
+
await reconcileStoredCredential(ctx);
|
|
143
|
+
return dashboardSnapshot(ctx);
|
|
144
|
+
},
|
|
110
145
|
);
|
|
111
146
|
},
|
|
112
147
|
});
|
|
@@ -42,7 +42,7 @@ Capability modes:
|
|
|
42
42
|
- `execute` — the read-only allowlist plus shell and background-terminal execution, without direct file edit tools
|
|
43
43
|
- `all` — full child tool access
|
|
44
44
|
|
|
45
|
-
Restrictive modes fail closed: newly registered extension tools remain unavailable until they are explicitly classified. This prevents another extension from silently bypassing the selected capability.
|
|
45
|
+
Restrictive modes fail closed: newly registered extension tools remain unavailable until they are explicitly classified. This prevents another extension from silently bypassing the selected capability. The session-only `task_list` planning tool is explicitly classified as safe in every capability mode, so each child can maintain its own task list without receiving file-write or command-execution authority.
|
|
46
46
|
|
|
47
47
|
Optional user profiles and personas can be defined in `~/.pi/agent/subagents.json`. Trusted projects may override them in `.pi/subagents.json`:
|
|
48
48
|
|
|
@@ -64,7 +64,7 @@ Optional user profiles and personas can be defined in `~/.pi/agent/subagents.jso
|
|
|
64
64
|
}
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
-
Project configuration is ignored when the project is not trusted. Concurrency is bounded to 1–50; this
|
|
67
|
+
Project configuration is ignored when the project is not trusted. Concurrency is bounded to 1–50; this suite defaults to 50. `/subagents profiles` provides discovery, while `/subagents config` opens a validated editor for global or trusted-project configuration.
|
|
68
68
|
|
|
69
69
|
## Context and continuation
|
|
70
70
|
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Pi Task List
|
|
2
|
+
|
|
3
|
+
A branch-safe task list for ordinary multi-step work. The active model creates and maintains the list while it works; there is no background planner or separate task-list agent.
|
|
4
|
+
|
|
5
|
+
This extension is independent of the Goal extension. A normal task can use Task List without becoming a persistent goal, and an explicitly created goal can use its own evidence ledger and Task List at the same time.
|
|
6
|
+
|
|
7
|
+
## Model tool
|
|
8
|
+
|
|
9
|
+
`task_list` reads or replaces the current session list:
|
|
10
|
+
|
|
11
|
+
- omit `tasks` to read the current list;
|
|
12
|
+
- supply `tasks` to replace the entire ordered list;
|
|
13
|
+
- send stable IDs so progress survives revisions;
|
|
14
|
+
- use `explanation` when scope, order, or approach changes.
|
|
15
|
+
|
|
16
|
+
Each item has:
|
|
17
|
+
|
|
18
|
+
- `id` — stable letters/numbers/dots/underscores/hyphens identifier;
|
|
19
|
+
- `content` — a concrete outcome, including exact user-provided commands or literals when relevant;
|
|
20
|
+
- `status` — `pending`, `in_progress`, `completed`, `blocked`, or `cancelled`;
|
|
21
|
+
- `priority` — `high`, `medium`, or `low` (`medium` by default);
|
|
22
|
+
- `note` — optional evidence, blocker, cancellation reason, or execution detail.
|
|
23
|
+
|
|
24
|
+
A list may contain up to 64 items. A model write with pending work must keep at least one item `in_progress`. Sequential work should have one active item; several are allowed only when work is genuinely running in parallel.
|
|
25
|
+
|
|
26
|
+
## Update discipline
|
|
27
|
+
|
|
28
|
+
The tool definition and Pi prompt guidance explicitly require the model to:
|
|
29
|
+
|
|
30
|
+
1. create a list for requests with at least three distinct actions, multiple requested tasks, or meaningful phases;
|
|
31
|
+
2. skip the list for direct answers and one- or two-action work;
|
|
32
|
+
3. send the initial list in the same assistant message as the first real action;
|
|
33
|
+
4. update the list as each step changes instead of batching bookkeeping at the end;
|
|
34
|
+
5. mark work complete only after its outcome is verified;
|
|
35
|
+
6. preserve every user-requested item and exact command, flag, path, and success condition;
|
|
36
|
+
7. reconcile the full list before the final response.
|
|
37
|
+
|
|
38
|
+
The runtime reinforces these instructions without assigning the list to another worker:
|
|
39
|
+
|
|
40
|
+
- after two substantive tool calls with no list, the next model context receives a conditional reminder;
|
|
41
|
+
- routine file reads, edits, commands, and tool calls do not trigger bookkeeping updates while the current task remains active;
|
|
42
|
+
- the model updates only for task-level transitions: verified completion and handoff to the next task, genuine blockers or cancellations, user-requested scope changes, and final reconciliation;
|
|
43
|
+
- status-only inspection tools do not count as substantive progress.
|
|
44
|
+
|
|
45
|
+
The list remains a coordination aid, not evidence that implementation or verification succeeded.
|
|
46
|
+
|
|
47
|
+
## Continuity
|
|
48
|
+
|
|
49
|
+
Every update stores a complete immutable snapshot in Pi custom session entries. State is reconstructed from the active branch on startup, resume, reload, and tree navigation, so branching restores the list that belonged to that point in history.
|
|
50
|
+
|
|
51
|
+
Active items are injected into model context only when the current snapshot is no longer represented there, including after compaction. Completed and cancelled work is summarized by count in that continuity message so the model does not redo it.
|
|
52
|
+
|
|
53
|
+
No external task database or writable package file is used.
|
|
54
|
+
|
|
55
|
+
## Subagents
|
|
56
|
+
|
|
57
|
+
Pi subagents load this extension with their normal child resources. `task_list` is explicitly allowed under every child capability policy because it changes only the child session's planning state. Each child owns an independent list in its own persistent Pi session; a child does not mutate the parent model's list.
|
|
58
|
+
|
|
59
|
+
## User interface
|
|
60
|
+
|
|
61
|
+
Run:
|
|
62
|
+
|
|
63
|
+
```text
|
|
64
|
+
/tasks
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
While work is active, a compact widget above the editor shows progress and the current items. A finished list lingers for four seconds so the final checkmark is visible, then clears from the live chrome while remaining available in session history.
|
|
68
|
+
|
|
69
|
+
`/tasks` opens the full-width interactive dashboard:
|
|
70
|
+
|
|
71
|
+
- `↑`/`↓` or `j`/`k` — select an item;
|
|
72
|
+
- `space` — advance pending → in progress → completed;
|
|
73
|
+
- `b` — block/unblock;
|
|
74
|
+
- `c` — cancel/restore;
|
|
75
|
+
- `p` — cycle priority;
|
|
76
|
+
- `a` — add;
|
|
77
|
+
- `e` — edit;
|
|
78
|
+
- `d` — delete with confirmation;
|
|
79
|
+
- `h` — hide/show completed and cancelled items;
|
|
80
|
+
- `X` — clear the list with confirmation;
|
|
81
|
+
- `Esc` or `q` — close.
|
|
82
|
+
|
|
83
|
+
User edits automatically promote the next pending item when no task remains in progress. `/tasks clear` provides the same guarded clear action without opening the dashboard. In print, JSON, or RPC-oriented use, the tool remains fully functional and `/tasks status` falls back to a text summary where a custom terminal dashboard is unavailable.
|
|
84
|
+
|
|
85
|
+
## Design inputs
|
|
86
|
+
|
|
87
|
+
The implementation combines the strongest verified patterns from the compared harnesses:
|
|
88
|
+
|
|
89
|
+
- Pi's branch-aware tool-result/session-entry model and custom TUI surfaces;
|
|
90
|
+
- Codex's concise ordered plan, optional update explanation, and timely status transitions;
|
|
91
|
+
- OpenCode's whole-list replacement and persistent session projection;
|
|
92
|
+
- Grok Build's compact live panel, cancelled state, compaction continuity, and stale-list reminders;
|
|
93
|
+
- DeepSeek Harness's strict input validation, parallel-active policy, and dedicated composer panel;
|
|
94
|
+
- Hermes Agent's stable IDs, read-or-write tool, merge-informed state model, four-state UI, and post-compaction active-list injection;
|
|
95
|
+
- Factory Droid's same-message task/action rule, three-action threshold, real-time completion discipline, stale-plan warning, and compact TodoWrite presentation.
|
|
96
|
+
|
|
97
|
+
OpenClaw's inspected task/update surfaces do not provide a comparable model-maintained coding-session todo tool, so no incompatible lifecycle was copied from them.
|
|
98
|
+
|
|
99
|
+
## Validation
|
|
100
|
+
|
|
101
|
+
From the repository root:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
bun test extensions/task-list extensions/subagents/context-config.test.ts
|
|
105
|
+
bun x tsc --noEmit
|
|
106
|
+
npm run validate
|
|
107
|
+
```
|