gsd-pi 2.10.2 → 2.10.5
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 +2 -0
- package/dist/cli.js +7 -0
- package/dist/loader.js +1 -0
- package/dist/onboarding.js +104 -59
- package/dist/update-cmd.d.ts +1 -0
- package/dist/update-cmd.js +40 -0
- package/node_modules/@gsd/native/dist/hasher/index.d.ts +32 -0
- package/node_modules/@gsd/native/dist/hasher/index.js +37 -0
- package/node_modules/@gsd/native/dist/native.d.ts +4 -1
- package/node_modules/@gsd/native/dist/native.js +39 -9
- package/node_modules/@gsd/native/dist/xxhash/index.d.ts +14 -0
- package/node_modules/@gsd/native/dist/xxhash/index.js +17 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/agent-session.d.ts +6 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/agent-session.js +58 -9
- package/node_modules/@gsd/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/auth-storage.d.ts +72 -12
- package/node_modules/@gsd/pi-coding-agent/dist/core/auth-storage.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/auth-storage.js +254 -43
- package/node_modules/@gsd/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/auth-storage.test.d.ts +2 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/auth-storage.test.d.ts.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/auth-storage.test.js +159 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/auth-storage.test.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/model-registry.d.ts +4 -2
- package/node_modules/@gsd/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/model-registry.js +6 -4
- package/node_modules/@gsd/pi-coding-agent/dist/core/model-registry.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/settings-manager.d.ts +15 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/settings-manager.js +12 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash-interceptor.d.ts +40 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash-interceptor.d.ts.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash-interceptor.js +92 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash-interceptor.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash-interceptor.test.d.ts +2 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash-interceptor.test.d.ts.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash-interceptor.test.js +156 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash-interceptor.test.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.d.ts +8 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.js +18 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.d.ts +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.js +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/index.d.ts +2 -2
- package/node_modules/@gsd/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/index.js +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/index.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/src/core/agent-session.ts +65 -9
- package/node_modules/@gsd/pi-coding-agent/src/core/auth-storage.test.ts +194 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/auth-storage.ts +283 -53
- package/node_modules/@gsd/pi-coding-agent/src/core/model-registry.ts +6 -4
- package/node_modules/@gsd/pi-coding-agent/src/core/settings-manager.ts +29 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/tools/bash-interceptor.test.ts +198 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/tools/bash-interceptor.ts +115 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/tools/bash.ts +29 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/tools/index.ts +8 -0
- package/node_modules/@gsd/pi-coding-agent/src/index.ts +6 -0
- package/node_modules/@gsd/pi-coding-agent/src/modes/interactive/interactive-mode.ts +1 -1
- package/package.json +8 -2
- package/packages/native/dist/hasher/index.d.ts +32 -0
- package/packages/native/dist/hasher/index.js +37 -0
- package/packages/native/dist/native.d.ts +4 -1
- package/packages/native/dist/native.js +39 -9
- package/packages/native/dist/xxhash/index.d.ts +14 -0
- package/packages/native/dist/xxhash/index.js +17 -0
- package/packages/native/src/native.ts +39 -9
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts +6 -0
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +58 -9
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.d.ts +72 -12
- package/packages/pi-coding-agent/dist/core/auth-storage.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.js +254 -43
- package/packages/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/auth-storage.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/auth-storage.test.js +159 -0
- package/packages/pi-coding-agent/dist/core/auth-storage.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts +4 -2
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.js +6 -4
- package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +15 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +12 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/bash-interceptor.d.ts +40 -0
- package/packages/pi-coding-agent/dist/core/tools/bash-interceptor.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/tools/bash-interceptor.js +92 -0
- package/packages/pi-coding-agent/dist/core/tools/bash-interceptor.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/tools/bash-interceptor.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/tools/bash-interceptor.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/tools/bash-interceptor.test.js +156 -0
- package/packages/pi-coding-agent/dist/core/tools/bash-interceptor.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/tools/bash.d.ts +8 -0
- package/packages/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/bash.js +18 -0
- package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/index.d.ts +1 -0
- package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/index.js +1 -0
- package/packages/pi-coding-agent/dist/core/tools/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts +2 -2
- package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/index.js +1 -1
- package/packages/pi-coding-agent/dist/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/core/agent-session.ts +65 -9
- package/packages/pi-coding-agent/src/core/auth-storage.test.ts +194 -0
- package/packages/pi-coding-agent/src/core/auth-storage.ts +283 -53
- package/packages/pi-coding-agent/src/core/model-registry.ts +6 -4
- package/packages/pi-coding-agent/src/core/settings-manager.ts +29 -0
- package/packages/pi-coding-agent/src/core/tools/bash-interceptor.test.ts +198 -0
- package/packages/pi-coding-agent/src/core/tools/bash-interceptor.ts +115 -0
- package/packages/pi-coding-agent/src/core/tools/bash.ts +29 -0
- package/packages/pi-coding-agent/src/core/tools/index.ts +8 -0
- package/packages/pi-coding-agent/src/index.ts +6 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +1 -1
- package/src/resources/extensions/async-jobs/async-bash-tool.ts +211 -0
- package/src/resources/extensions/async-jobs/await-tool.ts +101 -0
- package/src/resources/extensions/async-jobs/cancel-job-tool.ts +34 -0
- package/src/resources/extensions/async-jobs/index.ts +133 -0
- package/src/resources/extensions/async-jobs/job-manager.ts +250 -0
- package/src/resources/extensions/gsd/git-service.ts +13 -3
- package/src/resources/extensions/gsd/prompts/system.md +5 -2
- package/src/resources/extensions/gsd/tests/git-service.test.ts +36 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* await_job tool — wait for one or more background jobs to complete.
|
|
3
|
+
*
|
|
4
|
+
* If specific job IDs are provided, waits for those jobs.
|
|
5
|
+
* If omitted, waits for any running job to complete.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ToolDefinition } from "@gsd/pi-coding-agent";
|
|
9
|
+
import { Type } from "@sinclair/typebox";
|
|
10
|
+
import type { AsyncJobManager, Job } from "./job-manager.js";
|
|
11
|
+
|
|
12
|
+
const schema = Type.Object({
|
|
13
|
+
jobs: Type.Optional(
|
|
14
|
+
Type.Array(Type.String(), {
|
|
15
|
+
description: "Job IDs to wait for. Omit to wait for any running job.",
|
|
16
|
+
}),
|
|
17
|
+
),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export function createAwaitTool(getManager: () => AsyncJobManager): ToolDefinition<typeof schema> {
|
|
21
|
+
return {
|
|
22
|
+
name: "await_job",
|
|
23
|
+
label: "Await Background Job",
|
|
24
|
+
description:
|
|
25
|
+
"Wait for background jobs to complete. Provide specific job IDs or omit to wait for the next job that finishes. Returns results of completed jobs.",
|
|
26
|
+
parameters: schema,
|
|
27
|
+
async execute(_toolCallId, params) {
|
|
28
|
+
const manager = getManager();
|
|
29
|
+
const { jobs: jobIds } = params;
|
|
30
|
+
|
|
31
|
+
let watched: Job[];
|
|
32
|
+
if (jobIds && jobIds.length > 0) {
|
|
33
|
+
watched = [];
|
|
34
|
+
const notFound: string[] = [];
|
|
35
|
+
for (const id of jobIds) {
|
|
36
|
+
const job = manager.getJob(id);
|
|
37
|
+
if (job) {
|
|
38
|
+
watched.push(job);
|
|
39
|
+
} else {
|
|
40
|
+
notFound.push(id);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (notFound.length > 0 && watched.length === 0) {
|
|
44
|
+
return {
|
|
45
|
+
content: [{ type: "text", text: `No jobs found: ${notFound.join(", ")}` }],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
watched = manager.getRunningJobs();
|
|
50
|
+
if (watched.length === 0) {
|
|
51
|
+
return {
|
|
52
|
+
content: [{ type: "text", text: "No running background jobs." }],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// If all watched jobs are already done, return immediately
|
|
58
|
+
const running = watched.filter((j) => j.status === "running");
|
|
59
|
+
if (running.length === 0) {
|
|
60
|
+
const result = formatResults(watched);
|
|
61
|
+
manager.acknowledgeDeliveries(watched.map((j) => j.id));
|
|
62
|
+
return { content: [{ type: "text", text: result }] };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Wait for at least one to complete
|
|
66
|
+
await Promise.race(running.map((j) => j.promise));
|
|
67
|
+
|
|
68
|
+
// Collect all completed results (more may have finished while waiting)
|
|
69
|
+
const completed = watched.filter((j) => j.status !== "running");
|
|
70
|
+
manager.acknowledgeDeliveries(completed.map((j) => j.id));
|
|
71
|
+
|
|
72
|
+
const stillRunning = watched.filter((j) => j.status === "running");
|
|
73
|
+
let result = formatResults(completed);
|
|
74
|
+
if (stillRunning.length > 0) {
|
|
75
|
+
result += `\n\n**Still running:** ${stillRunning.map((j) => `${j.id} (${j.label})`).join(", ")}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return { content: [{ type: "text", text: result }] };
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function formatResults(jobs: Job[]): string {
|
|
84
|
+
if (jobs.length === 0) return "No completed jobs.";
|
|
85
|
+
|
|
86
|
+
const parts: string[] = [];
|
|
87
|
+
for (const job of jobs) {
|
|
88
|
+
const elapsed = ((Date.now() - job.startTime) / 1000).toFixed(1);
|
|
89
|
+
const header = `### ${job.id} — ${job.label} (${job.status}, ${elapsed}s)`;
|
|
90
|
+
|
|
91
|
+
if (job.status === "completed") {
|
|
92
|
+
parts.push(`${header}\n\n${job.resultText ?? "(no output)"}`);
|
|
93
|
+
} else if (job.status === "failed") {
|
|
94
|
+
parts.push(`${header}\n\nError: ${job.errorText ?? "unknown error"}`);
|
|
95
|
+
} else if (job.status === "cancelled") {
|
|
96
|
+
parts.push(`${header}\n\nCancelled.`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return parts.join("\n\n---\n\n");
|
|
101
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cancel_job tool — cancel a running background job.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ToolDefinition } from "@gsd/pi-coding-agent";
|
|
6
|
+
import { Type } from "@sinclair/typebox";
|
|
7
|
+
import type { AsyncJobManager } from "./job-manager.js";
|
|
8
|
+
|
|
9
|
+
const schema = Type.Object({
|
|
10
|
+
job_id: Type.String({ description: "The background job ID to cancel (e.g. bg_a1b2c3d4)" }),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export function createCancelJobTool(getManager: () => AsyncJobManager): ToolDefinition<typeof schema> {
|
|
14
|
+
return {
|
|
15
|
+
name: "cancel_job",
|
|
16
|
+
label: "Cancel Background Job",
|
|
17
|
+
description: "Cancel a running background job by its ID.",
|
|
18
|
+
parameters: schema,
|
|
19
|
+
async execute(_toolCallId, params) {
|
|
20
|
+
const manager = getManager();
|
|
21
|
+
const result = manager.cancel(params.job_id);
|
|
22
|
+
|
|
23
|
+
const messages: Record<string, string> = {
|
|
24
|
+
cancelled: `Job ${params.job_id} has been cancelled.`,
|
|
25
|
+
not_found: `Job ${params.job_id} not found.`,
|
|
26
|
+
already_completed: `Job ${params.job_id} has already completed (or failed/cancelled).`,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
content: [{ type: "text", text: messages[result] ?? `Unknown result: ${result}` }],
|
|
31
|
+
};
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Async Jobs Extension
|
|
3
|
+
*
|
|
4
|
+
* Allows bash commands to run in the background. The agent gets a job ID
|
|
5
|
+
* immediately and can continue working. Results are delivered via follow-up
|
|
6
|
+
* messages when jobs complete.
|
|
7
|
+
*
|
|
8
|
+
* Tools:
|
|
9
|
+
* async_bash — run a command in the background, get a job ID
|
|
10
|
+
* await_job — wait for background jobs to complete, get results
|
|
11
|
+
* cancel_job — cancel a running background job
|
|
12
|
+
*
|
|
13
|
+
* Commands:
|
|
14
|
+
* /jobs — show running and recent background jobs
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent";
|
|
18
|
+
import { AsyncJobManager, type Job } from "./job-manager.js";
|
|
19
|
+
import { createAsyncBashTool } from "./async-bash-tool.js";
|
|
20
|
+
import { createAwaitTool } from "./await-tool.js";
|
|
21
|
+
import { createCancelJobTool } from "./cancel-job-tool.js";
|
|
22
|
+
|
|
23
|
+
export default function AsyncJobs(pi: ExtensionAPI) {
|
|
24
|
+
let manager: AsyncJobManager | null = null;
|
|
25
|
+
let latestCwd: string = process.cwd();
|
|
26
|
+
|
|
27
|
+
function getManager(): AsyncJobManager {
|
|
28
|
+
if (!manager) {
|
|
29
|
+
throw new Error("AsyncJobManager not initialized. Wait for session_start.");
|
|
30
|
+
}
|
|
31
|
+
return manager;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getCwd(): string {
|
|
35
|
+
return latestCwd;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ── Session lifecycle ──────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
41
|
+
latestCwd = ctx.cwd;
|
|
42
|
+
|
|
43
|
+
manager = new AsyncJobManager({
|
|
44
|
+
onJobComplete: (job) => {
|
|
45
|
+
const statusEmoji = job.status === "completed" ? "done" : "error";
|
|
46
|
+
const elapsed = ((Date.now() - job.startTime) / 1000).toFixed(1);
|
|
47
|
+
const output = job.status === "completed"
|
|
48
|
+
? job.resultText ?? "(no output)"
|
|
49
|
+
: `Error: ${job.errorText ?? "unknown error"}`;
|
|
50
|
+
|
|
51
|
+
// Truncate output for the follow-up message
|
|
52
|
+
const maxLen = 2000;
|
|
53
|
+
const truncatedOutput = output.length > maxLen
|
|
54
|
+
? output.slice(0, maxLen) + "\n\n[... truncated, use await_job for full output]"
|
|
55
|
+
: output;
|
|
56
|
+
|
|
57
|
+
pi.sendMessage(
|
|
58
|
+
{
|
|
59
|
+
customType: "async_job_result",
|
|
60
|
+
content: [
|
|
61
|
+
`**Background job ${statusEmoji}: ${job.id}** (${job.label}, ${elapsed}s)`,
|
|
62
|
+
"",
|
|
63
|
+
truncatedOutput,
|
|
64
|
+
].join("\n"),
|
|
65
|
+
display: `Background job ${job.id} ${job.status}`,
|
|
66
|
+
},
|
|
67
|
+
{ deliverAs: "followUp", triggerTurn: true },
|
|
68
|
+
);
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
pi.on("session_shutdown", async () => {
|
|
74
|
+
if (manager) {
|
|
75
|
+
manager.shutdown();
|
|
76
|
+
manager = null;
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// ── Tools ──────────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
pi.registerTool(createAsyncBashTool(getManager, getCwd));
|
|
83
|
+
pi.registerTool(createAwaitTool(getManager));
|
|
84
|
+
pi.registerTool(createCancelJobTool(getManager));
|
|
85
|
+
|
|
86
|
+
// ── /jobs command ──────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
pi.registerCommand("jobs", {
|
|
89
|
+
description: "Show running and recent background jobs",
|
|
90
|
+
handler: async (_args: string, _ctx: ExtensionCommandContext) => {
|
|
91
|
+
if (!manager) {
|
|
92
|
+
pi.sendMessage({
|
|
93
|
+
customType: "async_jobs_list",
|
|
94
|
+
content: "No async job manager active.",
|
|
95
|
+
display: "No jobs",
|
|
96
|
+
});
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const running = manager.getRunningJobs();
|
|
101
|
+
const recent = manager.getRecentJobs(10);
|
|
102
|
+
const completed = recent.filter((j) => j.status !== "running");
|
|
103
|
+
|
|
104
|
+
const lines: string[] = ["## Background Jobs"];
|
|
105
|
+
|
|
106
|
+
if (running.length === 0 && completed.length === 0) {
|
|
107
|
+
lines.push("", "No background jobs.");
|
|
108
|
+
} else {
|
|
109
|
+
if (running.length > 0) {
|
|
110
|
+
lines.push("", "### Running");
|
|
111
|
+
for (const job of running) {
|
|
112
|
+
const elapsed = ((Date.now() - job.startTime) / 1000).toFixed(0);
|
|
113
|
+
lines.push(`- **${job.id}** — ${job.label} (${elapsed}s)`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (completed.length > 0) {
|
|
118
|
+
lines.push("", "### Recent");
|
|
119
|
+
for (const job of completed) {
|
|
120
|
+
const elapsed = ((Date.now() - job.startTime) / 1000).toFixed(1);
|
|
121
|
+
lines.push(`- **${job.id}** — ${job.label} (${job.status}, ${elapsed}s)`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
pi.sendMessage({
|
|
127
|
+
customType: "async_jobs_list",
|
|
128
|
+
content: lines.join("\n"),
|
|
129
|
+
display: `${running.length} running, ${completed.length} recent`,
|
|
130
|
+
});
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AsyncJobManager — manages background tool call jobs.
|
|
3
|
+
*
|
|
4
|
+
* Each job runs asynchronously and delivers its result via a callback
|
|
5
|
+
* when complete. Jobs are evicted after a configurable TTL.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { randomUUID } from "node:crypto";
|
|
9
|
+
|
|
10
|
+
// ── Types ──────────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
export type JobStatus = "running" | "completed" | "failed" | "cancelled";
|
|
13
|
+
export type JobType = "bash";
|
|
14
|
+
|
|
15
|
+
export interface Job {
|
|
16
|
+
id: string;
|
|
17
|
+
type: JobType;
|
|
18
|
+
status: JobStatus;
|
|
19
|
+
startTime: number;
|
|
20
|
+
label: string;
|
|
21
|
+
abortController: AbortController;
|
|
22
|
+
promise: Promise<void>;
|
|
23
|
+
resultText?: string;
|
|
24
|
+
errorText?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface JobManagerOptions {
|
|
28
|
+
maxRunning?: number; // default 15
|
|
29
|
+
maxTotal?: number; // default 100
|
|
30
|
+
evictionMs?: number; // default 5 minutes
|
|
31
|
+
onJobComplete?: (job: Job) => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ── Delivery Retry ─────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
const DELIVERY_BASE_MS = 500;
|
|
37
|
+
const DELIVERY_MAX_MS = 30_000;
|
|
38
|
+
const DELIVERY_JITTER_MS = 200;
|
|
39
|
+
|
|
40
|
+
// ── Manager ────────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
export class AsyncJobManager {
|
|
43
|
+
private jobs = new Map<string, Job>();
|
|
44
|
+
private deliveryTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
45
|
+
private acknowledgedJobs = new Set<string>();
|
|
46
|
+
private evictionTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
47
|
+
|
|
48
|
+
private maxRunning: number;
|
|
49
|
+
private maxTotal: number;
|
|
50
|
+
private evictionMs: number;
|
|
51
|
+
private onJobComplete?: (job: Job) => void;
|
|
52
|
+
|
|
53
|
+
constructor(options: JobManagerOptions = {}) {
|
|
54
|
+
this.maxRunning = options.maxRunning ?? 15;
|
|
55
|
+
this.maxTotal = options.maxTotal ?? 100;
|
|
56
|
+
this.evictionMs = options.evictionMs ?? 5 * 60 * 1000;
|
|
57
|
+
this.onJobComplete = options.onJobComplete;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Register a new background job.
|
|
62
|
+
* @returns job ID (prefixed with `bg_`)
|
|
63
|
+
*/
|
|
64
|
+
register(
|
|
65
|
+
type: JobType,
|
|
66
|
+
label: string,
|
|
67
|
+
runFn: (signal: AbortSignal) => Promise<string>,
|
|
68
|
+
): string {
|
|
69
|
+
// Enforce limits
|
|
70
|
+
const running = this.getRunningJobs();
|
|
71
|
+
if (running.length >= this.maxRunning) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`Maximum concurrent background jobs reached (${this.maxRunning}). ` +
|
|
74
|
+
`Use await_job or cancel_job to free a slot.`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
if (this.jobs.size >= this.maxTotal) {
|
|
78
|
+
// Evict oldest completed job
|
|
79
|
+
this.evictOldest();
|
|
80
|
+
if (this.jobs.size >= this.maxTotal) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`Maximum total background jobs reached (${this.maxTotal}). ` +
|
|
83
|
+
`Use cancel_job to remove jobs.`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const id = `bg_${randomUUID().slice(0, 8)}`;
|
|
89
|
+
const abortController = new AbortController();
|
|
90
|
+
|
|
91
|
+
// Declare job first so the promise callbacks can close over it safely.
|
|
92
|
+
const job: Job = {
|
|
93
|
+
id,
|
|
94
|
+
type,
|
|
95
|
+
status: "running",
|
|
96
|
+
startTime: Date.now(),
|
|
97
|
+
label,
|
|
98
|
+
abortController,
|
|
99
|
+
// promise assigned below
|
|
100
|
+
promise: undefined as unknown as Promise<void>,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
job.promise = runFn(abortController.signal)
|
|
104
|
+
.then((resultText) => {
|
|
105
|
+
job.status = "completed";
|
|
106
|
+
job.resultText = resultText;
|
|
107
|
+
this.scheduleEviction(id);
|
|
108
|
+
this.deliverResult(job);
|
|
109
|
+
})
|
|
110
|
+
.catch((err) => {
|
|
111
|
+
if (job.status === "cancelled") {
|
|
112
|
+
// Already cancelled — don't overwrite
|
|
113
|
+
this.scheduleEviction(id);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
job.status = "failed";
|
|
117
|
+
job.errorText = err instanceof Error ? err.message : String(err);
|
|
118
|
+
this.scheduleEviction(id);
|
|
119
|
+
this.deliverResult(job);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
this.jobs.set(id, job);
|
|
123
|
+
return id;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Cancel a running job.
|
|
128
|
+
*/
|
|
129
|
+
cancel(id: string): "cancelled" | "not_found" | "already_completed" {
|
|
130
|
+
const job = this.jobs.get(id);
|
|
131
|
+
if (!job) return "not_found";
|
|
132
|
+
if (job.status !== "running") return "already_completed";
|
|
133
|
+
|
|
134
|
+
job.status = "cancelled";
|
|
135
|
+
job.errorText = "Cancelled by user";
|
|
136
|
+
job.abortController.abort();
|
|
137
|
+
this.scheduleEviction(id);
|
|
138
|
+
return "cancelled";
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
getJob(id: string): Job | undefined {
|
|
142
|
+
return this.jobs.get(id);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
getRunningJobs(): Job[] {
|
|
146
|
+
return [...this.jobs.values()].filter((j) => j.status === "running");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
getRecentJobs(limit = 10): Job[] {
|
|
150
|
+
return [...this.jobs.values()]
|
|
151
|
+
.sort((a, b) => b.startTime - a.startTime)
|
|
152
|
+
.slice(0, limit);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
getAllJobs(): Job[] {
|
|
156
|
+
return [...this.jobs.values()];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Mark jobs as acknowledged so delivery retries stop.
|
|
161
|
+
*/
|
|
162
|
+
acknowledgeDeliveries(jobIds: string[]): void {
|
|
163
|
+
for (const id of jobIds) {
|
|
164
|
+
this.acknowledgedJobs.add(id);
|
|
165
|
+
const timer = this.deliveryTimers.get(id);
|
|
166
|
+
if (timer) {
|
|
167
|
+
clearTimeout(timer);
|
|
168
|
+
this.deliveryTimers.delete(id);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Cleanup all timers and resources.
|
|
175
|
+
*/
|
|
176
|
+
shutdown(): void {
|
|
177
|
+
for (const timer of this.deliveryTimers.values()) {
|
|
178
|
+
clearTimeout(timer);
|
|
179
|
+
}
|
|
180
|
+
this.deliveryTimers.clear();
|
|
181
|
+
|
|
182
|
+
for (const timer of this.evictionTimers.values()) {
|
|
183
|
+
clearTimeout(timer);
|
|
184
|
+
}
|
|
185
|
+
this.evictionTimers.clear();
|
|
186
|
+
|
|
187
|
+
// Abort all running jobs
|
|
188
|
+
for (const job of this.jobs.values()) {
|
|
189
|
+
if (job.status === "running") {
|
|
190
|
+
job.status = "cancelled";
|
|
191
|
+
job.abortController.abort();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ── Private ────────────────────────────────────────────────────────────
|
|
197
|
+
|
|
198
|
+
private deliverResult(job: Job, attempt = 0): void {
|
|
199
|
+
if (this.acknowledgedJobs.has(job.id)) return;
|
|
200
|
+
if (!this.onJobComplete) return;
|
|
201
|
+
|
|
202
|
+
this.onJobComplete(job);
|
|
203
|
+
|
|
204
|
+
// Schedule retry with exponential backoff + jitter
|
|
205
|
+
const delay = Math.min(
|
|
206
|
+
DELIVERY_BASE_MS * Math.pow(2, attempt) + Math.random() * DELIVERY_JITTER_MS,
|
|
207
|
+
DELIVERY_MAX_MS,
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
const timer = setTimeout(() => {
|
|
211
|
+
this.deliveryTimers.delete(job.id);
|
|
212
|
+
if (!this.acknowledgedJobs.has(job.id)) {
|
|
213
|
+
this.deliverResult(job, attempt + 1);
|
|
214
|
+
}
|
|
215
|
+
}, delay);
|
|
216
|
+
|
|
217
|
+
this.deliveryTimers.set(job.id, timer);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private scheduleEviction(id: string): void {
|
|
221
|
+
const existing = this.evictionTimers.get(id);
|
|
222
|
+
if (existing) clearTimeout(existing);
|
|
223
|
+
|
|
224
|
+
const timer = setTimeout(() => {
|
|
225
|
+
this.evictionTimers.delete(id);
|
|
226
|
+
this.jobs.delete(id);
|
|
227
|
+
this.acknowledgedJobs.delete(id);
|
|
228
|
+
}, this.evictionMs);
|
|
229
|
+
|
|
230
|
+
this.evictionTimers.set(id, timer);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private evictOldest(): void {
|
|
234
|
+
let oldest: Job | undefined;
|
|
235
|
+
for (const job of this.jobs.values()) {
|
|
236
|
+
if (job.status !== "running") {
|
|
237
|
+
if (!oldest || job.startTime < oldest.startTime) {
|
|
238
|
+
oldest = job;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (oldest) {
|
|
243
|
+
const timer = this.evictionTimers.get(oldest.id);
|
|
244
|
+
if (timer) clearTimeout(timer);
|
|
245
|
+
this.evictionTimers.delete(oldest.id);
|
|
246
|
+
this.jobs.delete(oldest.id);
|
|
247
|
+
this.acknowledgedJobs.delete(oldest.id);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
@@ -673,10 +673,11 @@ export class GitServiceImpl {
|
|
|
673
673
|
try {
|
|
674
674
|
this.git(mergeArgs);
|
|
675
675
|
} catch (mergeError) {
|
|
676
|
-
// Check if conflicts
|
|
676
|
+
// Check if conflicts can be auto-resolved (#189, #218)
|
|
677
677
|
const conflicted = this.git(["diff", "--name-only", "--diff-filter=U"], { allowFailure: true });
|
|
678
678
|
if (conflicted) {
|
|
679
679
|
const conflictedFiles = conflicted.split("\n").filter(Boolean);
|
|
680
|
+
const allGsd = conflictedFiles.every(f => f.startsWith(".gsd/"));
|
|
680
681
|
const allRuntime = conflictedFiles.every(f =>
|
|
681
682
|
RUNTIME_EXCLUSION_PATHS.some(excl => f.startsWith(excl.replace(/\/$/, ""))),
|
|
682
683
|
);
|
|
@@ -688,12 +689,21 @@ export class GitServiceImpl {
|
|
|
688
689
|
}
|
|
689
690
|
this.git(["add", "-A"], { allowFailure: true });
|
|
690
691
|
// Don't throw — let the merge proceed
|
|
692
|
+
} else if (allGsd) {
|
|
693
|
+
// Non-runtime .gsd/ conflicts (DECISIONS.md, REQUIREMENTS.md, ROADMAP.md, etc.):
|
|
694
|
+
// The slice branch has the authoritative .gsd/ state since the LLM just finished
|
|
695
|
+
// updating these artifacts during complete-slice. Take theirs (the slice branch).
|
|
696
|
+
for (const f of conflictedFiles) {
|
|
697
|
+
this.git(["checkout", "--theirs", "--", f], { allowFailure: true });
|
|
698
|
+
}
|
|
699
|
+
this.git(["add", "-A"], { allowFailure: true });
|
|
700
|
+
// Don't throw — let the merge proceed
|
|
691
701
|
} else {
|
|
692
|
-
// Non
|
|
702
|
+
// Non-.gsd/ conflicts: reset and throw as before
|
|
693
703
|
this.git(["reset", "--hard", "HEAD"], { allowFailure: true });
|
|
694
704
|
const msg = mergeError instanceof Error ? mergeError.message : String(mergeError);
|
|
695
705
|
throw new Error(
|
|
696
|
-
`${strategy === "merge" ? "Merge" : "Squash-merge"} of "${branch}" into "${mainBranch}" failed with conflicts. ` +
|
|
706
|
+
`${strategy === "merge" ? "Merge" : "Squash-merge"} of "${branch}" into "${mainBranch}" failed with conflicts in non-.gsd/ files. ` +
|
|
697
707
|
`Working tree has been reset to a clean state. ` +
|
|
698
708
|
`Resolve manually: git checkout ${mainBranch} && git merge ${strategy === "merge" ? "--no-ff" : "--squash"} ${branch}\n` +
|
|
699
709
|
`Original error: ${msg}`,
|
|
@@ -127,8 +127,9 @@ Use the lightest sufficient tool first.
|
|
|
127
127
|
- Broad unfamiliar subsystem mapping -> `subagent` with `scout`
|
|
128
128
|
- Library, package, or framework truth -> `resolve_library` then `get_library_docs`
|
|
129
129
|
- Current external facts -> `search-the-web` + `fetch_page`, or `search_and_read` for one-call extraction
|
|
130
|
-
- Long-running
|
|
130
|
+
- Long-running processes (servers, watchers, persistent daemons) -> `bg_shell` with `start` + `wait_for_ready`
|
|
131
131
|
- Background process status -> `bg_shell` with `digest` (not `output`). Token budget: `digest` (~30 tokens) < `highlights` (~100) < `output` (~2000).
|
|
132
|
+
- One-shot commands where you want the result delivered back (builds, tests, installs) -> `async_bash`; result is pushed to you automatically when the command exits.
|
|
132
133
|
- Secrets -> `secure_env_collect`
|
|
133
134
|
|
|
134
135
|
### Ask vs infer
|
|
@@ -161,7 +162,9 @@ Fix the root cause, not symptoms. When applying a temporary mitigation, label it
|
|
|
161
162
|
|
|
162
163
|
### Background processes
|
|
163
164
|
|
|
164
|
-
Use `bg_shell` for anything
|
|
165
|
+
Use `bg_shell` for persistent processes — servers, watchers, anything that keeps running. Set `type:'server'` + `ready_port` for dev servers, `group:'name'` for related processes. Use `wait_for_ready` instead of polling. Use `digest` for status checks, `highlights` for significant output, `output` only when debugging. Use `send_and_wait` for interactive CLIs. Kill processes when done.
|
|
166
|
+
|
|
167
|
+
Use `async_bash` for one-shot commands (builds, tests, installs) where you want the output delivered back automatically. Result arrives as a follow-up message when the command exits — no polling needed. Use `await_job` to explicitly wait for a specific job, `cancel_job` to stop one, `/jobs` to see what's running.
|
|
165
168
|
|
|
166
169
|
### Web behavior
|
|
167
170
|
|
|
@@ -953,6 +953,42 @@ async function main(): Promise<void> {
|
|
|
953
953
|
rmSync(repo, { recursive: true, force: true });
|
|
954
954
|
}
|
|
955
955
|
|
|
956
|
+
// ─── mergeSliceToMain: auto-resolve .gsd/ planning artifact conflicts ──
|
|
957
|
+
|
|
958
|
+
console.log("\n=== mergeSliceToMain: auto-resolve .gsd/ planning conflicts ===");
|
|
959
|
+
|
|
960
|
+
{
|
|
961
|
+
const repo = initBranchTestRepo();
|
|
962
|
+
const svc = new GitServiceImpl(repo);
|
|
963
|
+
|
|
964
|
+
// Create a .gsd/ planning artifact on main (simulates reassess-roadmap)
|
|
965
|
+
createFile(repo, ".gsd/DECISIONS.md", "# Decisions\n\n- D001: Original decision\n");
|
|
966
|
+
run("git add -A", repo);
|
|
967
|
+
run("git commit -m 'add decisions on main'", repo);
|
|
968
|
+
|
|
969
|
+
// Create slice branch and modify the same .gsd/ file differently
|
|
970
|
+
svc.ensureSliceBranch("M001", "S01");
|
|
971
|
+
createFile(repo, ".gsd/DECISIONS.md", "# Decisions\n\n- D001: Original decision\n- D002: New decision from slice\n");
|
|
972
|
+
createFile(repo, "src/feature.ts", "export const x = 1;");
|
|
973
|
+
run("git add -A", repo);
|
|
974
|
+
run("git commit -m 'slice work with .gsd/ changes'", repo);
|
|
975
|
+
|
|
976
|
+
// Back on main, modify the same .gsd/ file to create a conflict
|
|
977
|
+
svc.switchToMain();
|
|
978
|
+
createFile(repo, ".gsd/DECISIONS.md", "# Decisions\n\n- D001: Updated decision on main\n");
|
|
979
|
+
run("git add -A", repo);
|
|
980
|
+
run("git commit -m 'update decisions on main'", repo);
|
|
981
|
+
|
|
982
|
+
// Merge should auto-resolve .gsd/ conflicts by taking theirs (slice branch)
|
|
983
|
+
const result = svc.mergeSliceToMain("M001", "S01", "Feature with .gsd/ conflicts");
|
|
984
|
+
assertEq(result.deletedBranch, true, ".gsd/ conflict auto-resolved: branch deleted");
|
|
985
|
+
|
|
986
|
+
// Verify the merge succeeded and src file is present
|
|
987
|
+
assert(existsSync(join(repo, "src/feature.ts")), ".gsd/ conflict auto-resolved: src file merged");
|
|
988
|
+
|
|
989
|
+
rmSync(repo, { recursive: true, force: true });
|
|
990
|
+
}
|
|
991
|
+
|
|
956
992
|
// ═══════════════════════════════════════════════════════════════════════
|
|
957
993
|
// S05: Enhanced features — merge guards, snapshots, auto-push, rich commits
|
|
958
994
|
// ═══════════════════════════════════════════════════════════════════════
|