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.
- package/README.md +52 -61
- package/agents/git-expert.md +5 -1
- package/agents/github-expert.md +4 -1
- package/extensions/acpx-provider/runtime-models.ts +18 -6
- package/extensions/cli-provider/agents/claude.ts +3 -2
- package/extensions/cli-provider/agents/cursor.ts +3 -5
- package/extensions/cli-provider/agents/gemini.ts +3 -2
- package/extensions/cli-provider/discover.ts +7 -7
- package/extensions/cli-provider/index.ts +5 -0
- package/extensions/cli-provider/runtime-models.ts +18 -6
- package/extensions/cli-provider/session-reaper.ts +5 -7
- package/extensions/cli-provider/sessions.ts +4 -0
- package/extensions/cli-provider/shared/discover-cache.ts +17 -176
- package/extensions/coms/coms-p2p.ts +1719 -423
- package/extensions/coms/coms-shared.ts +261 -39
- package/extensions/coms/coms-wrapper.ts +100 -62
- package/extensions/coms/index.ts +8 -4
- package/extensions/coms/mixed-turn.ts +26 -0
- package/extensions/coms/probe-socket.ts +42 -0
- package/extensions/coms/themeMap.ts +0 -1
- package/extensions/image-gen/image-gen.ts +19 -1
- package/extensions/orchestrator/async-agents.ts +509 -85
- package/extensions/orchestrator/cron.ts +14 -8
- package/extensions/orchestrator/dreaming.ts +31 -24
- package/extensions/orchestrator/enforcement-helpers.ts +459 -17
- package/extensions/orchestrator/enforcement-rules.ts +5 -4
- package/extensions/orchestrator/enforcement.ts +79 -25
- package/extensions/orchestrator/extended-autocomplete.ts +7 -0
- package/extensions/orchestrator/git-helpers.ts +2 -2
- package/extensions/orchestrator/index.ts +23 -10
- package/extensions/orchestrator/markdown-transformer.ts +313 -0
- package/extensions/orchestrator/memory-embeddings.ts +36 -34
- package/extensions/orchestrator/memory-provenance.ts +29 -25
- package/extensions/orchestrator/memory-scoring.ts +23 -18
- package/extensions/orchestrator/memory-tools.ts +10 -3
- package/extensions/orchestrator/overlay-dashboard.ts +17 -5
- package/extensions/orchestrator/pi-config-review-state.ts +44 -33
- package/extensions/orchestrator/project-settings.ts +220 -21
- package/extensions/orchestrator/review-porcelain.ts +4 -0
- package/extensions/orchestrator/review-ui.ts +11 -5
- package/extensions/orchestrator/rule-placeholders.ts +384 -1
- package/extensions/orchestrator/rules.ts +80 -132
- package/extensions/orchestrator/settings-tui-helpers.ts +380 -0
- package/extensions/orchestrator/settings-tui-submenus.ts +647 -0
- package/extensions/orchestrator/settings-tui.ts +636 -0
- package/extensions/orchestrator/state-jsonl.ts +431 -0
- package/extensions/orchestrator/subagent-tool.ts +105 -50
- package/extensions/orchestrator/sync-limit.ts +23 -0
- package/extensions/orchestrator/task-lifecycle.ts +77 -67
- package/extensions/orchestrator/utils.ts +8 -5
- package/extensions/pidash/pidash.ts +193 -156
- package/extensions/pidiff/pidiff.ts +64 -38
- package/extensions/pidiff/spawn-lock.ts +62 -0
- package/extensions/pitasks/index.ts +430 -0
- package/extensions/pitasks/reminders.ts +37 -0
- package/extensions/pitasks/task-store.ts +467 -0
- package/extensions/pitasks/task-tools.ts +375 -0
- package/extensions/pitasks/task-widget.ts +181 -0
- package/extensions/pitasks/types.ts +38 -0
- package/extensions/providers/acpx-driver.ts +12 -2
- package/extensions/providers/built-in-drivers.ts +6 -6
- package/extensions/providers/claude-driver.ts +1 -1
- package/extensions/providers/cursor-cli-driver.ts +1 -1
- package/extensions/providers/gemini-driver.ts +1 -1
- package/extensions/providers/index.ts +144 -26
- package/extensions/providers/initialized-guard.ts +27 -0
- package/extensions/providers/restore-default-model.ts +596 -0
- package/extensions/providers/session-shutdown.ts +39 -0
- package/extensions/shared/coms-active.ts +23 -0
- package/extensions/shared/create-runtime-provider.ts +2 -0
- package/extensions/shared/daemon-manager.ts +0 -8
- package/extensions/shared/file-logger.ts +132 -25
- package/extensions/shared/logger.ts +50 -0
- package/extensions/shared/models-dev.ts +421 -0
- package/extensions/shared/oneshot.ts +116 -0
- package/extensions/shared/resolve-binary.ts +191 -0
- package/extensions/shared/types.ts +3 -0
- package/package.json +7 -2
- package/prompts/coderabbit-rate-limit.md +2 -0
- package/rules/00-orchestrator-core.md +3 -0
- package/rules/10-agent-routing.md +1 -1
- package/rules/20-code-review-loop.md +38 -16
- package/rules/30-prompt-templates.md +4 -0
- package/rules/35-memory.md +3 -3
- package/rules/40-critical-rules.md +3 -0
- package/rules/55-coms-protocol.md +84 -18
- package/rules/60-task-tracking.md +2 -0
- package/settings-keys.json +481 -20
- package/templates/coms-feature-manager-prompt.md +4 -6
- package/extensions/coms/coms-net-server.ts +0 -1642
- package/extensions/coms/coms-net-wrapper.ts +0 -521
- 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
|
-
//
|
|
33
|
-
let
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
73
|
-
const
|
|
74
|
-
|
|
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", () =>
|
|
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
|
|
567
|
-
function validateTaskId(taskId: string,
|
|
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
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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 {
|
|
8
|
+
import { createLogger } from "../shared/logger.js";
|
|
10
9
|
|
|
11
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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";
|