pi-orchestrator-config 4.2.1 → 4.3.2

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.
Files changed (92) hide show
  1. package/README.md +52 -61
  2. package/agents/git-expert.md +5 -1
  3. package/agents/github-expert.md +4 -1
  4. package/extensions/acpx-provider/runtime-models.ts +18 -6
  5. package/extensions/cli-provider/agents/claude.ts +3 -2
  6. package/extensions/cli-provider/agents/cursor.ts +3 -5
  7. package/extensions/cli-provider/agents/gemini.ts +3 -2
  8. package/extensions/cli-provider/discover.ts +7 -7
  9. package/extensions/cli-provider/index.ts +5 -0
  10. package/extensions/cli-provider/runtime-models.ts +18 -6
  11. package/extensions/cli-provider/session-reaper.ts +5 -7
  12. package/extensions/cli-provider/sessions.ts +4 -0
  13. package/extensions/cli-provider/shared/discover-cache.ts +17 -176
  14. package/extensions/coms/coms-p2p.ts +1719 -423
  15. package/extensions/coms/coms-shared.ts +261 -39
  16. package/extensions/coms/coms-wrapper.ts +100 -62
  17. package/extensions/coms/index.ts +8 -4
  18. package/extensions/coms/mixed-turn.ts +26 -0
  19. package/extensions/coms/probe-socket.ts +42 -0
  20. package/extensions/coms/themeMap.ts +0 -1
  21. package/extensions/image-gen/image-gen.ts +19 -1
  22. package/extensions/orchestrator/async-agents.ts +509 -85
  23. package/extensions/orchestrator/cron.ts +14 -8
  24. package/extensions/orchestrator/dreaming.ts +31 -24
  25. package/extensions/orchestrator/enforcement-helpers.ts +459 -17
  26. package/extensions/orchestrator/enforcement-rules.ts +5 -4
  27. package/extensions/orchestrator/enforcement.ts +79 -25
  28. package/extensions/orchestrator/extended-autocomplete.ts +7 -0
  29. package/extensions/orchestrator/git-helpers.ts +2 -2
  30. package/extensions/orchestrator/index.ts +23 -10
  31. package/extensions/orchestrator/markdown-transformer.ts +313 -0
  32. package/extensions/orchestrator/memory-embeddings.ts +36 -34
  33. package/extensions/orchestrator/memory-provenance.ts +29 -25
  34. package/extensions/orchestrator/memory-scoring.ts +23 -18
  35. package/extensions/orchestrator/memory-tools.ts +10 -3
  36. package/extensions/orchestrator/overlay-dashboard.ts +17 -5
  37. package/extensions/orchestrator/pi-config-review-state.ts +44 -33
  38. package/extensions/orchestrator/project-settings.ts +220 -21
  39. package/extensions/orchestrator/review-porcelain.ts +4 -0
  40. package/extensions/orchestrator/review-ui.ts +11 -5
  41. package/extensions/orchestrator/rule-placeholders.ts +384 -1
  42. package/extensions/orchestrator/rules.ts +80 -132
  43. package/extensions/orchestrator/settings-tui-helpers.ts +380 -0
  44. package/extensions/orchestrator/settings-tui-submenus.ts +647 -0
  45. package/extensions/orchestrator/settings-tui.ts +636 -0
  46. package/extensions/orchestrator/state-jsonl.ts +431 -0
  47. package/extensions/orchestrator/subagent-tool.ts +105 -50
  48. package/extensions/orchestrator/sync-limit.ts +23 -0
  49. package/extensions/orchestrator/task-lifecycle.ts +77 -67
  50. package/extensions/orchestrator/utils.ts +8 -5
  51. package/extensions/pidash/pidash.ts +193 -156
  52. package/extensions/pidiff/pidiff.ts +64 -38
  53. package/extensions/pidiff/spawn-lock.ts +62 -0
  54. package/extensions/pitasks/index.ts +430 -0
  55. package/extensions/pitasks/reminders.ts +37 -0
  56. package/extensions/pitasks/task-store.ts +467 -0
  57. package/extensions/pitasks/task-tools.ts +375 -0
  58. package/extensions/pitasks/task-widget.ts +181 -0
  59. package/extensions/pitasks/types.ts +38 -0
  60. package/extensions/providers/acpx-driver.ts +12 -2
  61. package/extensions/providers/built-in-drivers.ts +6 -6
  62. package/extensions/providers/claude-driver.ts +1 -1
  63. package/extensions/providers/cursor-cli-driver.ts +1 -1
  64. package/extensions/providers/gemini-driver.ts +1 -1
  65. package/extensions/providers/index.ts +144 -26
  66. package/extensions/providers/initialized-guard.ts +27 -0
  67. package/extensions/providers/restore-default-model.ts +596 -0
  68. package/extensions/providers/session-shutdown.ts +39 -0
  69. package/extensions/shared/coms-active.ts +23 -0
  70. package/extensions/shared/create-runtime-provider.ts +2 -0
  71. package/extensions/shared/daemon-manager.ts +0 -8
  72. package/extensions/shared/file-logger.ts +132 -25
  73. package/extensions/shared/logger.ts +50 -0
  74. package/extensions/shared/models-dev.ts +421 -0
  75. package/extensions/shared/oneshot.ts +116 -0
  76. package/extensions/shared/resolve-binary.ts +191 -0
  77. package/extensions/shared/types.ts +3 -0
  78. package/package.json +7 -2
  79. package/prompts/coderabbit-rate-limit.md +2 -0
  80. package/rules/00-orchestrator-core.md +3 -0
  81. package/rules/10-agent-routing.md +1 -1
  82. package/rules/20-code-review-loop.md +38 -16
  83. package/rules/30-prompt-templates.md +4 -0
  84. package/rules/35-memory.md +3 -3
  85. package/rules/40-critical-rules.md +3 -0
  86. package/rules/55-coms-protocol.md +84 -18
  87. package/rules/60-task-tracking.md +2 -0
  88. package/settings-keys.json +481 -20
  89. package/templates/coms-feature-manager-prompt.md +4 -6
  90. package/extensions/coms/coms-net-server.ts +0 -1642
  91. package/extensions/coms/coms-net-wrapper.ts +0 -521
  92. package/extensions/coms/coms-net.ts +0 -1589
@@ -16,6 +16,7 @@ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
16
16
  import type { Message } from "@earendil-works/pi-ai";
17
17
  import { StringEnum } from "@earendil-works/pi-ai";
18
18
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
19
+ import { createLogger } from "../shared/logger.js";
19
20
  import {
20
21
  getMarkdownTheme,
21
22
  withFileMutationQueue,
@@ -29,26 +30,20 @@ import {
29
30
  import { Type } from "typebox";
30
31
  import { type AgentConfig, type AgentScope, discoverAgents } from "./agents.js";
31
32
 
32
- // Import TaskStore for taskId validation (bypasses raw file reads)
33
- let TaskStoreClass: any = null;
34
- (async () => {
35
- const candidates = [
36
- "@tintinweb/pi-tasks/dist/task-store.js",
37
- pathToFileURL(path.join(os.homedir(), ".pi/agent/npm/node_modules/@tintinweb/pi-tasks/dist/task-store.js")).href,
38
- ];
39
- for (const c of candidates) {
40
- try { const mod = await import(c); if (mod.TaskStore) { TaskStoreClass = mod.TaskStore; break; } } catch { continue; }
41
- }
42
- if (!TaskStoreClass) {
43
- throw new Error("[subagent] FATAL: TaskStore not found — @tintinweb/pi-tasks is required but failed to load");
44
- }
45
- })();
33
+ // Task store access via shared instance from extensions/pitasks
34
+ let _taskStore: any = null;
35
+ async function getTaskStore(): Promise<any> {
36
+ if (_taskStore) return _taskStore;
37
+ try { const mod = await import("../pitasks/index.js"); _taskStore = mod.taskStore; } catch { /* ignore */ }
38
+ return _taskStore;
39
+ }
46
40
  import {
47
41
  decideAsyncLlmDispatch,
48
42
  supportsAsyncLlm,
49
43
  } from "./async-capability.js";
50
44
  import { autoMarkInProgress, autoCompleteTask } from "./async-agents.js";
51
45
  import { getSetting } from "./project-settings.js";
46
+ import { checkSyncLimit } from "./sync-limit.js";
52
47
  import { substituteSettingsPlaceholders } from "./rule-placeholders.js";
53
48
  import { resolveAgentModelProvider } from "./resolve-agent-model.js";
54
49
  import { parseModelOverride, mergeModelOverride } from "./parse-model-override.js";
@@ -61,7 +56,7 @@ const SETTINGS_KEYS_FOR_SUBAGENT = JSON.parse(
61
56
  ),
62
57
  );
63
58
 
64
- export { resolveAgentModelProvider, parseModelOverride };
59
+ export { resolveAgentModelProvider, parseModelOverride, checkSyncLimit };
65
60
 
66
61
  // ── Constants ────────────────────────────────────────────────────────────
67
62
 
@@ -69,11 +64,19 @@ const MAX_PARALLEL_TASKS = 8;
69
64
  const MAX_CONCURRENCY = 4;
70
65
  const COLLAPSED_ITEM_COUNT = 10;
71
66
  const MISSING_CWD_ERROR = "Missing required parameter: cwd. Always specify the working directory for subagent tasks.";
72
- const MAX_SYNC_SECONDS = 30;
73
- const SYNC_TIME_EXCEEDED_ERROR = (seconds: number) =>
74
- `Estimated time ${seconds}s meets or exceeds ${MAX_SYNC_SECONDS}s sync limit. Use async: true instead.`;
67
+ // Resolved per-call from settings
68
+ const log = createLogger("subagent");
69
+ const SYNC_TIME_EXCEEDED_ERROR = (seconds: number, limit: number, cwd: string) => {
70
+ log.info("sync_time_exceeded", "seconds", seconds, "limit", limit, "cwd", cwd);
71
+ return `Estimated time ${seconds}s meets or exceeds ${limit}s sync limit. Use async: true instead.`;
72
+ };
75
73
  const MISSING_ESTIMATE_ERROR = "Missing required parameter: estimatedSeconds. Provide an estimated duration in seconds for sync agent tasks.";
76
74
 
75
+ let syncPenalty = 0;
76
+ let lastSyncAgent = "";
77
+ let lastSyncEstimate = 0;
78
+ let lastSyncActual = 0;
79
+
77
80
  /** Agents that MUST be dispatched with async: true. Sync calls are rejected.
78
81
  * Keep in sync with rules/20-code-review-loop.md when changing this list. */
79
82
  const ASYNC_ONLY_AGENTS = new Set([
@@ -474,13 +477,14 @@ export async function runSingleAgent(
474
477
  args.push(`Task: ${task}`);
475
478
  let aborted = false;
476
479
 
480
+ log.info("sync_spawn_start", agentName, "cwd", cwd);
477
481
  const exitCode = await new Promise<number>((resolve) => {
478
482
  const inv = getPiInvocation(args);
479
483
  const proc = spawn(inv.command, inv.args, {
480
484
  cwd: cwd,
481
485
  shell: false,
482
486
  stdio: ["ignore", "pipe", "pipe"],
483
- env: { ...process.env, PI_SUBAGENT_CHILD: "1", PI_AGENT_NAME: agentName },
487
+ env: { ...process.env, PI_SUBAGENT_CHILD: "1", PI_AGENT_NAME: agentName, PI_PRIMARY_MODEL: process.env.PI_PRIMARY_MODEL || process.env.PI_MODEL || "", __PI_CONFIG_SESSION_ID: (globalThis as any).__piConfigSessionId || "", __PI_PARENT_SESSION_ID: (globalThis as any).__piConfigSessionId || "" },
484
488
  });
485
489
  let buf = "";
486
490
 
@@ -528,10 +532,14 @@ export async function runSingleAgent(
528
532
  cur.stderr += d.toString();
529
533
  });
530
534
  proc.on("close", (c) => {
535
+ log.info("sync_proc_close", agentName, "code", c);
531
536
  if (buf.trim()) processLine(buf);
532
537
  resolve(c ?? 0);
533
538
  });
534
- proc.on("error", () => resolve(1));
539
+ proc.on("error", (err) => {
540
+ log.error("sync_proc_error", agentName, err?.message);
541
+ resolve(1);
542
+ });
535
543
 
536
544
  if (signal) {
537
545
  const kill = () => {
@@ -546,6 +554,7 @@ export async function runSingleAgent(
546
554
  }
547
555
  });
548
556
 
557
+ log.info("sync_proc_resolved", agentName, "exitCode", exitCode, "elapsed", Date.now() - startTime);
549
558
  cur.exitCode = exitCode;
550
559
  cur.durationMs = Date.now() - startTime;
551
560
 
@@ -563,32 +572,21 @@ export async function runSingleAgent(
563
572
  }
564
573
  }
565
574
 
566
- /** Validate that a taskId references an existing task in the pi-tasks store. */
567
- function validateTaskId(taskId: string, cwd: string, sessionId?: string): string | null {
575
+ /** Validate that a taskId references an existing task in the pitasks store. */
576
+ function validateTaskId(taskId: string, _cwd: string, _sessionId?: string): string | null {
568
577
  if (taskId === "-1") return null;
569
-
570
- const tasksDir = path.join(cwd, ".pi", "tasks");
571
- const paths: string[] = [];
572
- if (sessionId) paths.push(path.join(tasksDir, `tasks-${sessionId}.json`));
573
- paths.push(path.join(tasksDir, "tasks.json"));
574
-
575
- for (const p of paths) {
576
- try {
577
- if (TaskStoreClass) {
578
- const store = new TaskStoreClass(p);
579
- if (store.get(taskId)) return null;
580
- } else {
581
- // Fallback: raw file read when TaskStore hasn't loaded yet (async init race)
582
- const data = JSON.parse(fs.readFileSync(p, "utf-8"));
583
- if ((data.tasks || []).some((t: any) => t.id === taskId)) return null;
584
- }
585
- } catch (e: any) {
586
- // Only continue silently for missing files; report other errors
587
- if (e?.code === "ENOENT" || e?.message?.includes("ENOENT")) continue;
588
- return `Task store error for ${path.basename(p)}: ${e?.message || e}. Fix the task store or pass taskId: "-1".`;
578
+ try {
579
+ // Ensure store is loaded (async init may not have completed)
580
+ if (!_taskStore) {
581
+ getTaskStore().catch(() => {});
589
582
  }
590
- }
591
-
583
+ const store = _taskStore;
584
+ if (!store) {
585
+ // Store not loaded yet (async init) — fail open, skip validation
586
+ return null;
587
+ }
588
+ if (store.get(taskId)) return null;
589
+ } catch { return null; }
592
590
  return `Task #${taskId} not found. Verify the task ID exists (use TaskList to check), or pass taskId: "-1" if not linked to a task.`;
593
591
  }
594
592
 
@@ -766,6 +764,10 @@ export function registerSubagentTool(
766
764
  };
767
765
  }
768
766
  }
767
+ if (syncPenalty > 0) {
768
+ log.info("sync_penalty_reset", "async used");
769
+ syncPenalty = 0;
770
+ }
769
771
  const result = spawnAsyncAgent(params.agent, params.task, params.cwd, agents, {
770
772
  fireAndForget: params.fireAndForget,
771
773
  name: params.name,
@@ -840,9 +842,25 @@ export function registerSubagentTool(
840
842
  }
841
843
  // chain runs sequentially — sum all steps
842
844
  const totalChainSeconds = params.chain.reduce((sum: number, s: any) => sum + s.estimatedSeconds, 0);
843
- if (totalChainSeconds >= MAX_SYNC_SECONDS && asyncOk) {
845
+ if (!params.chain || params.chain.length === 0) {
846
+ return { content: [{ type: "text" as const, text: "Error: chain array is empty — at least one step is required." }] };
847
+ }
848
+ const chainCwd = params.chain[0].cwd;
849
+ const { limit: chainLimit } = checkSyncLimit(0, chainCwd);
850
+ if (syncPenalty > 0) {
851
+ const adjustedTotal = totalChainSeconds + syncPenalty;
852
+ if (adjustedTotal >= chainLimit) {
853
+ log.info("sync_penalty_rejected_chain", "penalty", syncPenalty, "adjusted", adjustedTotal);
854
+ return {
855
+ content: [{ type: "text" as const, text: `Rejected: your last sync agent '${lastSyncAgent}' estimated ${lastSyncEstimate}s but took ${lastSyncActual}s. Penalty: ${syncPenalty}s. Chain total ${totalChainSeconds}s + penalty ${syncPenalty}s = ${adjustedTotal}s — exceeds ${chainLimit}s limit. Use async: true.` }],
856
+ details: mkd("chain")([]),
857
+ isError: true,
858
+ };
859
+ }
860
+ }
861
+ if (totalChainSeconds >= chainLimit && asyncOk) {
844
862
  return {
845
- content: [{ type: "text" as const, text: SYNC_TIME_EXCEEDED_ERROR(totalChainSeconds) }],
863
+ content: [{ type: "text" as const, text: SYNC_TIME_EXCEEDED_ERROR(totalChainSeconds, chainLimit, chainCwd) }],
846
864
  details: mkd("chain")([]),
847
865
  isError: true,
848
866
  };
@@ -965,9 +983,22 @@ export function registerSubagentTool(
965
983
  }
966
984
  // parallel runs concurrently — use longest task
967
985
  const maxParallelSeconds = Math.max(...params.tasks.map((t: any) => t.estimatedSeconds!));
968
- if (maxParallelSeconds >= MAX_SYNC_SECONDS && asyncOk) {
986
+ const parallelCwd = params.tasks[0]?.cwd ?? process.cwd();
987
+ const { limit: parallelLimit } = checkSyncLimit(0, parallelCwd);
988
+ if (syncPenalty > 0) {
989
+ const adjustedMax = maxParallelSeconds + syncPenalty;
990
+ if (adjustedMax >= parallelLimit) {
991
+ log.info("sync_penalty_rejected_parallel", "penalty", syncPenalty, "adjusted", adjustedMax);
992
+ return {
993
+ content: [{ type: "text" as const, text: `Rejected: your last sync agent '${lastSyncAgent}' estimated ${lastSyncEstimate}s but took ${lastSyncActual}s. Penalty: ${syncPenalty}s. Max parallel estimate ${maxParallelSeconds}s + penalty ${syncPenalty}s = ${adjustedMax}s — exceeds ${parallelLimit}s limit. Use async: true.` }],
994
+ details: mkd("parallel")([]),
995
+ isError: true,
996
+ };
997
+ }
998
+ }
999
+ if (maxParallelSeconds >= parallelLimit && asyncOk) {
969
1000
  return {
970
- content: [{ type: "text" as const, text: SYNC_TIME_EXCEEDED_ERROR(maxParallelSeconds) }],
1001
+ content: [{ type: "text" as const, text: SYNC_TIME_EXCEEDED_ERROR(maxParallelSeconds, parallelLimit, parallelCwd) }],
971
1002
  details: mkd("parallel")([]),
972
1003
  isError: true,
973
1004
  };
@@ -1127,9 +1158,21 @@ export function registerSubagentTool(
1127
1158
  isError: true,
1128
1159
  };
1129
1160
  }
1130
- if (params.estimatedSeconds >= MAX_SYNC_SECONDS && asyncOk) {
1161
+ const { limit: singleLimit } = checkSyncLimit(0, params.cwd);
1162
+ if (syncPenalty > 0) {
1163
+ const adjustedEstimate = (params.estimatedSeconds ?? 0) + syncPenalty;
1164
+ if (adjustedEstimate >= singleLimit) {
1165
+ log.info("sync_penalty_rejected", params.agent, "estimate", params.estimatedSeconds, "penalty", syncPenalty, "adjusted", adjustedEstimate);
1166
+ return {
1167
+ content: [{ type: "text" as const, text: `Rejected: your last sync agent '${lastSyncAgent}' estimated ${lastSyncEstimate}s but took ${lastSyncActual}s. Penalty: ${syncPenalty}s. Your estimate ${params.estimatedSeconds}s + penalty ${syncPenalty}s = ${adjustedEstimate}s — exceeds ${singleLimit}s limit. Use async: true.` }],
1168
+ details: mkd("single")([]),
1169
+ isError: true,
1170
+ };
1171
+ }
1172
+ }
1173
+ if (params.estimatedSeconds >= singleLimit && asyncOk) {
1131
1174
  return {
1132
- content: [{ type: "text" as const, text: SYNC_TIME_EXCEEDED_ERROR(params.estimatedSeconds) }],
1175
+ content: [{ type: "text" as const, text: SYNC_TIME_EXCEEDED_ERROR(params.estimatedSeconds, singleLimit, params.cwd) }],
1133
1176
  details: mkd("single")([]),
1134
1177
  isError: true,
1135
1178
  };
@@ -1142,6 +1185,8 @@ export function registerSubagentTool(
1142
1185
  if (syncTaskId && syncTaskId !== "-1") {
1143
1186
  await autoMarkInProgress(syncTaskId, params.cwd, sessionId).catch(() => {});
1144
1187
  }
1188
+ log.info("sync_execute_start", params.agent, "taskId", syncTaskId ?? "none");
1189
+ const syncStartTime = Date.now();
1145
1190
  const r = await runSingleAgent(
1146
1191
  agents,
1147
1192
  params.agent,
@@ -1156,6 +1201,15 @@ export function registerSubagentTool(
1156
1201
  params.persistSession,
1157
1202
  explicit,
1158
1203
  );
1204
+ const actualSeconds = Math.round((Date.now() - syncStartTime) / 1000);
1205
+ if (actualSeconds > checkSyncLimit(0, params.cwd).limit && actualSeconds - (params.estimatedSeconds ?? 0) > 30) {
1206
+ syncPenalty = actualSeconds - (params.estimatedSeconds ?? 0);
1207
+ lastSyncAgent = params.agent;
1208
+ lastSyncEstimate = params.estimatedSeconds ?? 0;
1209
+ lastSyncActual = actualSeconds;
1210
+ log.info("sync_penalty_set", params.agent, "estimate", params.estimatedSeconds, "actual", actualSeconds, "penalty", syncPenalty);
1211
+ }
1212
+ log.info("sync_execute_done", params.agent, "exitCode", r.exitCode, "stopReason", r.stopReason, "elapsed", Date.now() - syncStartTime);
1159
1213
  activeAgents.delete(label);
1160
1214
  updateWorking();
1161
1215
  const err =
@@ -1177,6 +1231,7 @@ export function registerSubagentTool(
1177
1231
  if (syncTaskId && syncTaskId !== "-1") {
1178
1232
  await autoCompleteTask(syncTaskId, params.cwd, sessionId).catch(() => {});
1179
1233
  }
1234
+ log.info("sync_execute_returning", params.agent);
1180
1235
  return {
1181
1236
  content: [
1182
1237
  { type: "text" as const, text: getFinalOutput(r.messages) || "(no output)" },
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Sync agent time-limit helpers.
3
+ * Kept separate from subagent-tool so unit tests can import without heavy deps.
4
+ */
5
+
6
+ import { getSetting } from "./project-settings.js";
7
+ import { createLogger } from "../shared/logger.js";
8
+
9
+ const log = createLogger("subagent");
10
+
11
+ /**
12
+ * Check if estimated seconds exceed the sync limit for a given cwd.
13
+ * Exported for testing.
14
+ */
15
+ export function checkSyncLimit(
16
+ estimatedSeconds: number,
17
+ cwd: string,
18
+ ): { exceeded: boolean; limit: number } {
19
+ const raw = getSetting(cwd, "sync_agent_max_seconds");
20
+ const limit = typeof raw === "number" && raw > 0 ? raw : 60; // fallback to default
21
+ log.debug("checkSyncLimit", "estimated", estimatedSeconds, "limit", limit, "exceeded", estimatedSeconds >= limit);
22
+ return { exceeded: estimatedSeconds >= limit, limit };
23
+ }
@@ -1,88 +1,98 @@
1
1
  /**
2
- * Task lifecycle helpers — auto-complete / mark in_progress via pi-tasks TaskStore.
3
- * Kept separate from async-agents.ts so unit tests can import without loading
4
- * @earendil-works/pi-coding-agent (unavailable in the tsx test harness).
2
+ * Task lifecycle helpers — auto-complete / mark in_progress via owned taskStore.
3
+ * Uses the exported store instance from extensions/pitasks when available.
4
+ * Falls back to _storeFactory for unit testing.
5
5
  */
6
6
 
7
- import * as os from "node:os";
8
7
  import * as path from "node:path";
9
- import { pathToFileURL } from "node:url";
8
+ import { createLogger } from "../shared/logger.js";
10
9
 
11
- let TaskStoreClass: any = null;
12
- const taskStoreReady: Promise<void> = (async () => {
13
- const candidates = [
14
- "@tintinweb/pi-tasks/dist/task-store.js",
15
- pathToFileURL(path.join(os.homedir(), ".pi/agent/npm/node_modules/@tintinweb/pi-tasks/dist/task-store.js")).href,
16
- ];
17
- for (const candidate of candidates) {
18
- try {
19
- const mod = await import(candidate);
20
- if (mod.TaskStore) { TaskStoreClass = mod.TaskStore; break; }
21
- } catch { continue; }
22
- }
23
- if (!TaskStoreClass) {
24
- throw new Error("[task-lifecycle] FATAL: TaskStore not found — @tintinweb/pi-tasks is required but failed to load");
25
- }
26
- })();
10
+ const log = createLogger("task-lifecycle");
27
11
 
28
- /** Auto-complete a task via pi-tasks TaskStore (in-process, no AI involvement). */
12
+ /** Get the shared task store instance (lazy import to avoid circular deps). */
13
+ async function getStore(): Promise<any> {
14
+ // Don't cache — pitasks reassigns taskStore during session upgrade/switch.
15
+ // Always read the latest export.
16
+ try {
17
+ const mod = await import("../pitasks/index.js");
18
+ return mod.taskStore ?? null;
19
+ } catch { return null; }
20
+ }
21
+
22
+ /** Auto-complete a task (in-process, no AI involvement). */
29
23
  export async function autoCompleteTask(taskId: string, cwd: string, sessionId?: string, _storeFactory?: (path: string) => any): Promise<boolean> {
30
24
  if (!taskId || taskId === "-1") return false;
31
- await taskStoreReady;
32
-
33
- const tasksDir = path.join(cwd, ".pi", "tasks");
34
- const candidates: string[] = [];
35
- if (sessionId) candidates.push(path.join(tasksDir, `tasks-${sessionId}.json`));
36
- candidates.push(path.join(tasksDir, "tasks.json"));
37
25
 
38
- for (const storePath of candidates) {
39
- try {
40
- const factory = _storeFactory ?? ((p: string) => new TaskStoreClass(p));
41
- const store = factory(storePath);
42
- const task = store.get(taskId);
43
- if (task) {
44
- // Task found in this store — do not fall through to later candidates
45
- if (task.status !== "completed") {
46
- store.update(taskId, { status: "completed" });
47
- return true;
26
+ // TEST-ONLY: _storeFactory constructs file paths for unit test isolation.
27
+ // Production code NEVER passes this — it falls through to getStore() below.
28
+ if (_storeFactory) {
29
+ const tasksDir = path.join(cwd, ".pi", "tasks");
30
+ const candidates: string[] = [];
31
+ if (sessionId) candidates.push(path.join(tasksDir, `tasks-${sessionId}.json`));
32
+ candidates.push(path.join(tasksDir, "tasks.json"));
33
+ for (const storePath of candidates) {
34
+ let store: any;
35
+ try {
36
+ store = _storeFactory(storePath);
37
+ const task = store.get(taskId);
38
+ if (task) {
39
+ if (task.status !== "completed") { store.update(taskId, { status: "completed" }); return true; }
40
+ return false;
48
41
  }
49
- return false;
50
- }
51
- } catch (e: any) {
52
- console.debug(`[task-lifecycle] autoCompleteTask failed for task ${taskId}: ${e?.message?.slice(0, 100)}`);
53
- continue;
42
+ } catch { continue; }
43
+ finally { if (store && typeof store.close === "function") store.close(); }
54
44
  }
45
+ return false;
46
+ }
47
+
48
+ const store = await getStore();
49
+ if (!store) return false;
50
+ try {
51
+ const task = store.get(taskId);
52
+ if (!task) return false;
53
+ if (task.status !== "completed") { store.update(taskId, { status: "completed" }); return true; }
54
+ return false;
55
+ } catch (e: any) {
56
+ log.debug("autoCompleteTask failed for task", taskId, e?.message?.slice(0, 100));
57
+ return false;
55
58
  }
56
- return false;
57
59
  }
58
60
 
59
- /** Auto-mark a task in_progress via pi-tasks TaskStore (in-process, no AI involvement). */
61
+ /** Auto-mark a task in_progress (in-process, no AI involvement). */
60
62
  export async function autoMarkInProgress(taskId: string, cwd: string, sessionId?: string, _storeFactory?: (path: string) => any): Promise<boolean> {
61
63
  if (!taskId || taskId === "-1") return false;
62
- await taskStoreReady;
63
-
64
- const tasksDir = path.join(cwd, ".pi", "tasks");
65
- const candidates: string[] = [];
66
- if (sessionId) candidates.push(path.join(tasksDir, `tasks-${sessionId}.json`));
67
- candidates.push(path.join(tasksDir, "tasks.json"));
68
64
 
69
- for (const storePath of candidates) {
70
- try {
71
- const factory = _storeFactory ?? ((p: string) => new TaskStoreClass(p));
72
- const store = factory(storePath);
73
- const task = store.get(taskId);
74
- if (task) {
75
- // Task found in this store — do not fall through to later candidates
76
- if (task.status === "pending") {
77
- store.update(taskId, { status: "in_progress" });
78
- return true;
65
+ // TEST-ONLY: _storeFactory constructs file paths for unit test isolation.
66
+ // Production code NEVER passes this — it falls through to getStore() below.
67
+ if (_storeFactory) {
68
+ const tasksDir = path.join(cwd, ".pi", "tasks");
69
+ const candidates: string[] = [];
70
+ if (sessionId) candidates.push(path.join(tasksDir, `tasks-${sessionId}.json`));
71
+ candidates.push(path.join(tasksDir, "tasks.json"));
72
+ for (const storePath of candidates) {
73
+ let store: any;
74
+ try {
75
+ store = _storeFactory(storePath);
76
+ const task = store.get(taskId);
77
+ if (task) {
78
+ if (task.status === "pending") { store.update(taskId, { status: "in_progress" }); return true; }
79
+ return false;
79
80
  }
80
- return false;
81
- }
82
- } catch (e: any) {
83
- console.debug(`[task-lifecycle] autoMarkInProgress failed for task ${taskId}: ${e?.message?.slice(0, 100)}`);
84
- continue;
81
+ } catch { continue; }
82
+ finally { if (store && typeof store.close === "function") store.close(); }
85
83
  }
84
+ return false;
85
+ }
86
+
87
+ const store = await getStore();
88
+ if (!store) return false;
89
+ try {
90
+ const task = store.get(taskId);
91
+ if (!task) return false;
92
+ if (task.status === "pending") { store.update(taskId, { status: "in_progress" }); return true; }
93
+ return false;
94
+ } catch (e: any) {
95
+ log.debug("autoMarkInProgress failed for task", taskId, e?.message?.slice(0, 100));
96
+ return false;
86
97
  }
87
- return false;
88
98
  }
@@ -4,11 +4,8 @@
4
4
 
5
5
  import { execFile, execFileSync } from "node:child_process";
6
6
  import * as fs from "node:fs";
7
- import { createRequire } from "node:module";
8
7
  import * as path from "node:path";
9
8
 
10
- const require = createRequire(import.meta.url);
11
-
12
9
  /** Whether notify-send is available (false = ENOENT, never retry) */
13
10
  let notifyAvailable: boolean | undefined;
14
11
 
@@ -91,7 +88,7 @@ export function resolveRepoRoot(cwd: string): string {
91
88
 
92
89
  /** Resolve to the current worktree root (or repo root if not in a worktree).
93
90
  * Uses --show-toplevel → returns THIS worktree's root, not the shared repo root.
94
- * Use for per-worktree resources: pi-config-review-state.json.
91
+ * Use for per-worktree resources: pi-config-review-state.jsonl.
95
92
  * For non-worktree repos, returns the same as resolveRepoRoot.
96
93
  * @param cwd — Directory to resolve from (any path inside the worktree)
97
94
  * @returns The worktree's top-level directory, or `cwd` as fallback */
@@ -151,7 +148,7 @@ export function tryGetSystemPromptOptions(ctx: any): { contextFiles?: any[]; ski
151
148
  }
152
149
 
153
150
  /** Minimum pi version required by this pi-config version. */
154
- export const MIN_PI_VERSION = "0.81.0";
151
+ export const MIN_PI_VERSION = "0.84.0";
155
152
 
156
153
  /** Get the installed pi version from its package.json. */
157
154
  export function getPiVersion(): string | null {
@@ -225,3 +222,9 @@ export function isPiMetaInvocation(argv: string[] = process.argv): boolean {
225
222
  }
226
223
  return sawMeta;
227
224
  }
225
+
226
+ export {
227
+ isPiOneshotInvocation,
228
+ shouldSkipOneshotRegister,
229
+ shouldSkipOneshotShutdownDream,
230
+ } from "../shared/oneshot.js";