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 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
@@ -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.
@@ -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. Existing Pi OAuth credentials are migrated into that pool without exposing token values. 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 and can reversibly enable or disable accounts.
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 loadAntigravityAccounts(): AntigravityAccount[] {
124
+ export function inspectAntigravityAccountStore(): AntigravityAccountStore {
109
125
  try {
110
126
  const parsed = JSON.parse(fs.readFileSync(ANTIGRAVITY_ACCOUNTS_PATH, "utf8")) as Partial<AntigravityAccountFile>;
111
- return Array.isArray(parsed.accounts)
112
- ? parsed.accounts.map(normalizeAccount).filter((account): account is AntigravityAccount => Boolean(account))
113
- : [];
114
- } catch {
115
- return [];
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
- void this.toggleAccount(account.id, account.disabled === true)
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 migrateStoredCredential = () => {
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 credential = readStoredCredential(PROVIDER_ID) as any;
49
- if (credential?.type !== "oauth" || typeof credential.refresh !== "string" || typeof credential.access !== "string") return;
50
- upsertAntigravityAccount({
51
- refresh: credential.refresh,
52
- access: credential.access,
53
- expires: typeof credential.expires === "number" ? credential.expires : 0,
54
- projectId: typeof credential.projectId === "string" ? credential.projectId : undefined,
55
- email: typeof credential.email === "string" ? credential.email : undefined,
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
- // A malformed or unavailable Pi credential must not block extension startup.
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
- // Pi replaces its single stored provider credential after login. Archive
71
- // the current account first so signing into another account cannot lose it.
72
- migrateStoredCredential();
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
- migrateStoredCredential();
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
- migrateStoredCredential();
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 private package defaults to 50. `/subagents profiles` provides discovery, while `/subagents config` opens a validated editor for global or trusted-project configuration.
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
 
@@ -65,6 +65,7 @@ const READ_CAPABILITY_TOOLS = new Set([
65
65
  "pi_memory_read",
66
66
  "pi_memory_status",
67
67
  "get_goal",
68
+ "task_list",
68
69
  "read_terminal",
69
70
  "list_terminals",
70
71
  "message_parent",
@@ -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
+ ```