claude-comms 1.0.45 → 1.0.46
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/.agents/tools/agent-job/__pycache__/agent_monitor.cpython-312.pyc +0 -0
- package/.agents/tools/agent-job/agent_job.py +71 -1
- package/.agents/tools/workflow/CONSUMER_SETUP.md +114 -0
- package/.agents/tools/workflow/ONBOARD.md +167 -0
- package/.agents/tools/workflow/README.md +169 -0
- package/.agents/tools/workflow/cli.ts +726 -0
- package/.agents/tools/workflow/config.example.json +16 -0
- package/.agents/tools/workflow/init.ts +76 -0
- package/.agents/tools/workflow/lib/file-tracker.test.ts +81 -0
- package/.agents/tools/workflow/lib/file-tracker.ts +387 -0
- package/.agents/tools/workflow/lib/harness-executor.ts +649 -0
- package/.agents/tools/workflow/lib/prompts.scenario.test.ts +262 -0
- package/.agents/tools/workflow/lib/prompts.ts +548 -0
- package/.agents/tools/workflow/lib/streams.test.ts +140 -0
- package/.agents/tools/workflow/lib/streams.ts +254 -0
- package/.agents/tools/workflow/runner.ts +994 -0
- package/.agents/tools/workflow/templates/document.md +42 -0
- package/.agents/tools/workflow/templates/implement.md +117 -0
- package/.agents/tools/workflow/templates/plan.md +54 -0
- package/.agents/tools/workflow/templates/pm.md +84 -0
- package/.agents/tools/workflow/templates/product-owner.md +211 -0
- package/.agents/tools/workflow/templates/review.md +56 -0
- package/.agents/tools/workflow/templates/uat.md +71 -0
- package/.claude/agents/core/engineer.md +2 -13
- package/.claude/commands/action-feedback-item.md +14 -9
- package/.claude/hooks/comms/__pycache__/register_subagent.cpython-312.pyc +0 -0
- package/.claude/hooks/comms/__pycache__/update_subagent_completion.cpython-312.pyc +0 -0
- package/.claude/hooks/utils/__pycache__/server_config.cpython-312.pyc +0 -0
- package/.claude/settings.local.json +46 -0
- package/CLAUDE.md +1 -1
- package/bin/claude-setup.js +3 -4
- package/package.json +1 -1
- package/src/orchestrator/installer.js +5 -63
- package/.claude/agents/core/architect.md +0 -316
- package/.claude/agents/core/consultants/codex.md +0 -39
- package/.claude/agents/core/consultants/gemini.md +0 -39
- package/.claude/agents/core/designer.md +0 -181
- package/.claude/agents/core/planner.md +0 -251
- package/.claude/agents/core/uat.md +0 -147
- package/.claude/agents/util/feedback-executor.md +0 -29
- package/.claude/agents/util/meta-agent-builder.md +0 -309
- package/.claude/agents/util/session-review.md +0 -228
- package/.claude/commands/analyse-document.md +0 -10
- package/.claude/commands/await-feedback-orchestrator.md +0 -12
- package/.claude/commands/await-feedback.md +0 -21
- package/.claude/commands/cook.md +0 -119
- package/.claude/commands/feedback.md +0 -140
- package/.claude/commands/hello-world.md +0 -1
- package/.claude/commands/uat-refine.md +0 -121
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"convexUrl": "https://your-project.convex.cloud",
|
|
3
|
+
"namespace": "my-project",
|
|
4
|
+
"timeoutMs": 3600000,
|
|
5
|
+
"idleTimeoutMs": 600000,
|
|
6
|
+
"harnessDefaults": {
|
|
7
|
+
"default": "claude",
|
|
8
|
+
"plan": "claude",
|
|
9
|
+
"implement": "claude",
|
|
10
|
+
"review": "claude",
|
|
11
|
+
"uat": "claude",
|
|
12
|
+
"document": "claude",
|
|
13
|
+
"pm": "claude",
|
|
14
|
+
"chat": "claude"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* Workflow Init Script
|
|
4
|
+
*
|
|
5
|
+
* Initializes the namespace in the Convex database based on config.json.
|
|
6
|
+
* This script is idempotent - running it multiple times won't create duplicates.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* npx tsx init.ts
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { ConvexClient } from "convex/browser";
|
|
13
|
+
import { anyApi } from "convex/server";
|
|
14
|
+
import { readFileSync, existsSync } from "fs";
|
|
15
|
+
import { dirname, join } from "path";
|
|
16
|
+
import { fileURLToPath } from "url";
|
|
17
|
+
|
|
18
|
+
const api = anyApi;
|
|
19
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
20
|
+
const __dirname = dirname(__filename);
|
|
21
|
+
|
|
22
|
+
interface Config {
|
|
23
|
+
convexUrl: string;
|
|
24
|
+
namespace: string;
|
|
25
|
+
timeoutMs?: number;
|
|
26
|
+
harnessDefaults?: {
|
|
27
|
+
default: "claude" | "codex" | "gemini";
|
|
28
|
+
[jobType: string]: "claude" | "codex" | "gemini";
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function loadConfig(): Config {
|
|
33
|
+
const configPath = join(__dirname, "config.json");
|
|
34
|
+
if (!existsSync(configPath)) {
|
|
35
|
+
console.error("Error: config.json not found at", configPath);
|
|
36
|
+
console.error("Please copy config.example.json to config.json and configure it.");
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
return JSON.parse(readFileSync(configPath, "utf-8"));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function main() {
|
|
43
|
+
const config = loadConfig();
|
|
44
|
+
console.log(`Initializing namespace: ${config.namespace}`);
|
|
45
|
+
console.log(`Convex URL: ${config.convexUrl}`);
|
|
46
|
+
|
|
47
|
+
const client = new ConvexClient(config.convexUrl);
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
// Check if namespace already exists
|
|
51
|
+
const existing = await client.query(api.namespaces.getByName, {
|
|
52
|
+
name: config.namespace,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (existing) {
|
|
56
|
+
console.log(`Namespace "${config.namespace}" already exists with ID: ${existing._id}`);
|
|
57
|
+
console.log("No action needed - idempotent check passed.");
|
|
58
|
+
} else {
|
|
59
|
+
// Create the namespace
|
|
60
|
+
const namespaceId = await client.mutation(api.namespaces.create, {
|
|
61
|
+
name: config.namespace,
|
|
62
|
+
description: `Namespace for ${config.namespace} repo`,
|
|
63
|
+
});
|
|
64
|
+
console.log(`Created namespace "${config.namespace}" with ID: ${namespaceId}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.log("\nInit complete!");
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error("Error during init:", error);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
} finally {
|
|
72
|
+
await client.close();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
main();
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for file-based job tracking module
|
|
3
|
+
*
|
|
4
|
+
* Run with: npx tsx --test lib/file-tracker.test.ts
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, before, after } from "node:test";
|
|
8
|
+
import assert from "node:assert";
|
|
9
|
+
import { rmSync, existsSync, readFileSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { JobTracker } from "./file-tracker.js";
|
|
13
|
+
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// JobTracker tests - the high-level API that matters
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
describe("JobTracker", () => {
|
|
19
|
+
const TEST_ROOT = join(tmpdir(), "agent_jobs_test_" + Date.now());
|
|
20
|
+
|
|
21
|
+
before(() => {
|
|
22
|
+
process.env.AGENT_JOBS_ROOT = TEST_ROOT;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
after(() => {
|
|
26
|
+
delete process.env.AGENT_JOBS_ROOT;
|
|
27
|
+
if (existsSync(TEST_ROOT)) {
|
|
28
|
+
rmSync(TEST_ROOT, { recursive: true, force: true });
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("creates status.json with correct initial state", () => {
|
|
33
|
+
const tracker = new JobTracker("init-test", "claude", 12345);
|
|
34
|
+
const status = tracker.getStatus();
|
|
35
|
+
|
|
36
|
+
assert.strictEqual(status.job_id, "init-test");
|
|
37
|
+
assert.strictEqual(status.harness, "claude");
|
|
38
|
+
assert.strictEqual(status.pid, 12345);
|
|
39
|
+
assert.strictEqual(status.status, "running");
|
|
40
|
+
assert.ok(status.logs?.endsWith("agent.log"));
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("records events with operations count and status reason", () => {
|
|
44
|
+
const tracker = new JobTracker("events-test", "codex", 99999);
|
|
45
|
+
|
|
46
|
+
tracker.recordEvent("assistant");
|
|
47
|
+
tracker.recordEvent("tool_use");
|
|
48
|
+
tracker.recordEvent();
|
|
49
|
+
|
|
50
|
+
const status = tracker.getStatus();
|
|
51
|
+
assert.strictEqual(status.operations, 3);
|
|
52
|
+
assert.strictEqual(status.status_reason, "tool_use"); // last explicit
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("logs lines to agent.log", () => {
|
|
56
|
+
const tracker = new JobTracker("log-test", "gemini", 11111);
|
|
57
|
+
|
|
58
|
+
tracker.logLine('{"type":"init"}');
|
|
59
|
+
tracker.logLine('{"type":"result"}');
|
|
60
|
+
|
|
61
|
+
const content = readFileSync(tracker.getLogPath(), "utf-8");
|
|
62
|
+
assert.ok(content.includes("init"));
|
|
63
|
+
assert.ok(content.includes("result"));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("marks complete/fail/timeout with end_time", () => {
|
|
67
|
+
const t1 = new JobTracker("complete-test", "claude", 1);
|
|
68
|
+
t1.complete("Done");
|
|
69
|
+
assert.strictEqual(t1.getStatus().status, "complete");
|
|
70
|
+
assert.ok(t1.getStatus().end_time);
|
|
71
|
+
|
|
72
|
+
const t2 = new JobTracker("fail-test", "claude", 2);
|
|
73
|
+
t2.fail("exit_1");
|
|
74
|
+
assert.strictEqual(t2.getStatus().status, "error");
|
|
75
|
+
assert.strictEqual(t2.getStatus().status_reason, "exit_1");
|
|
76
|
+
|
|
77
|
+
const t3 = new JobTracker("timeout-test", "claude", 3);
|
|
78
|
+
t3.timeout();
|
|
79
|
+
assert.strictEqual(t3.getStatus().status, "timeout");
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-based Job Tracking
|
|
3
|
+
*
|
|
4
|
+
* Compatible with agent_monitor.py TUI. Writes status.json and agent.log
|
|
5
|
+
* files in the same format as agent_job.py for process-level monitoring.
|
|
6
|
+
*
|
|
7
|
+
* This is separate from Convex tracking - used for debugging/monitoring
|
|
8
|
+
* when you need to see verbose JSON streams and process-level telemetry.
|
|
9
|
+
*
|
|
10
|
+
* Enhanced to support:
|
|
11
|
+
* - read_position tracking for recovery after runner restart
|
|
12
|
+
* - Orphan detection and reconciliation
|
|
13
|
+
* - Reading status from disk
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
writeFileSync,
|
|
18
|
+
mkdirSync,
|
|
19
|
+
appendFileSync,
|
|
20
|
+
renameSync,
|
|
21
|
+
existsSync,
|
|
22
|
+
readFileSync,
|
|
23
|
+
readdirSync,
|
|
24
|
+
statSync,
|
|
25
|
+
} from "fs";
|
|
26
|
+
import { join } from "path";
|
|
27
|
+
import { tmpdir } from "os";
|
|
28
|
+
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// Types
|
|
31
|
+
// ============================================================================
|
|
32
|
+
|
|
33
|
+
export interface FileJobStatus {
|
|
34
|
+
job_id: string;
|
|
35
|
+
harness: string;
|
|
36
|
+
agent_id: string | null;
|
|
37
|
+
pid: number | null;
|
|
38
|
+
logs: string | null;
|
|
39
|
+
status: "running" | "complete" | "error" | "timeout";
|
|
40
|
+
status_reason: string | null;
|
|
41
|
+
start_time: string | null;
|
|
42
|
+
last_event_time: string | null;
|
|
43
|
+
end_time: string | null;
|
|
44
|
+
operations: number;
|
|
45
|
+
/** Byte position in log file that has been processed (for recovery) */
|
|
46
|
+
read_position?: number;
|
|
47
|
+
completion: {
|
|
48
|
+
messages: string[];
|
|
49
|
+
final_message: string | null;
|
|
50
|
+
tokens: {
|
|
51
|
+
input: number | null;
|
|
52
|
+
output: number | null;
|
|
53
|
+
total: number | null;
|
|
54
|
+
};
|
|
55
|
+
duration_ms: number | null;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface JobPaths {
|
|
60
|
+
statusPath: string;
|
|
61
|
+
logPath: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ============================================================================
|
|
65
|
+
// Utilities
|
|
66
|
+
// ============================================================================
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get the root directory for job files.
|
|
70
|
+
* Respects AGENT_JOBS_ROOT env var, defaults to /tmp/agent_jobs/$USER
|
|
71
|
+
*/
|
|
72
|
+
export function getJobsRoot(): string {
|
|
73
|
+
if (process.env.AGENT_JOBS_ROOT) {
|
|
74
|
+
return process.env.AGENT_JOBS_ROOT;
|
|
75
|
+
}
|
|
76
|
+
const user = process.env.USER || "unknown";
|
|
77
|
+
return join(tmpdir(), "agent_jobs", user);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get UTC timestamp in ISO format (matches agent_job.py format)
|
|
82
|
+
*/
|
|
83
|
+
export function utcNowIso(): string {
|
|
84
|
+
return new Date().toISOString().replace(/\.\d{3}Z$/, ".000Z");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get the directory path for a specific job
|
|
89
|
+
*/
|
|
90
|
+
export function getJobDir(jobId: string): string {
|
|
91
|
+
return join(getJobsRoot(), jobId);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Check if a process is alive by PID
|
|
96
|
+
*/
|
|
97
|
+
export function isPidAlive(pid: number | null): boolean {
|
|
98
|
+
if (!pid) return false;
|
|
99
|
+
try {
|
|
100
|
+
process.kill(pid, 0); // Signal 0 = check existence
|
|
101
|
+
return true;
|
|
102
|
+
} catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* List all job directories in the jobs root
|
|
109
|
+
*/
|
|
110
|
+
export function listJobDirs(): string[] {
|
|
111
|
+
const root = getJobsRoot();
|
|
112
|
+
if (!existsSync(root)) return [];
|
|
113
|
+
|
|
114
|
+
return readdirSync(root).filter((name) => {
|
|
115
|
+
const dir = join(root, name);
|
|
116
|
+
try {
|
|
117
|
+
return statSync(dir).isDirectory();
|
|
118
|
+
} catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Read status.json for a job, returns null if not found or invalid
|
|
126
|
+
*/
|
|
127
|
+
export function readJobStatus(jobId: string): FileJobStatus | null {
|
|
128
|
+
const statusPath = join(getJobDir(jobId), "status.json");
|
|
129
|
+
if (!existsSync(statusPath)) return null;
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const content = readFileSync(statusPath, "utf-8");
|
|
133
|
+
return JSON.parse(content) as FileJobStatus;
|
|
134
|
+
} catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Get paths for a job (without creating the directory)
|
|
141
|
+
*/
|
|
142
|
+
export function getJobPaths(jobId: string): JobPaths {
|
|
143
|
+
const jobDir = getJobDir(jobId);
|
|
144
|
+
return {
|
|
145
|
+
statusPath: join(jobDir, "status.json"),
|
|
146
|
+
logPath: join(jobDir, "agent.log"),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Find orphaned jobs: status="running" but process is dead
|
|
152
|
+
*/
|
|
153
|
+
export function findOrphanedJobs(): Array<{
|
|
154
|
+
jobId: string;
|
|
155
|
+
status: FileJobStatus;
|
|
156
|
+
paths: JobPaths;
|
|
157
|
+
}> {
|
|
158
|
+
const orphans: Array<{ jobId: string; status: FileJobStatus; paths: JobPaths }> = [];
|
|
159
|
+
|
|
160
|
+
for (const jobId of listJobDirs()) {
|
|
161
|
+
const status = readJobStatus(jobId);
|
|
162
|
+
if (!status) continue;
|
|
163
|
+
|
|
164
|
+
// Only care about "running" jobs
|
|
165
|
+
if (status.status !== "running") continue;
|
|
166
|
+
|
|
167
|
+
// Check if process is still alive
|
|
168
|
+
if (!isPidAlive(status.pid)) {
|
|
169
|
+
orphans.push({
|
|
170
|
+
jobId,
|
|
171
|
+
status,
|
|
172
|
+
paths: getJobPaths(jobId),
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return orphans;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ============================================================================
|
|
181
|
+
// Job Status Management
|
|
182
|
+
// ============================================================================
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Create initial job status object
|
|
186
|
+
*/
|
|
187
|
+
export function createFileJobStatus(
|
|
188
|
+
jobId: string,
|
|
189
|
+
harness: string,
|
|
190
|
+
pid: number
|
|
191
|
+
): FileJobStatus {
|
|
192
|
+
return {
|
|
193
|
+
job_id: jobId,
|
|
194
|
+
harness,
|
|
195
|
+
agent_id: jobId,
|
|
196
|
+
pid,
|
|
197
|
+
logs: null, // Set after dir creation
|
|
198
|
+
status: "running",
|
|
199
|
+
status_reason: "initializing",
|
|
200
|
+
start_time: utcNowIso(),
|
|
201
|
+
last_event_time: utcNowIso(),
|
|
202
|
+
end_time: null,
|
|
203
|
+
operations: 0,
|
|
204
|
+
read_position: 0,
|
|
205
|
+
completion: {
|
|
206
|
+
messages: [],
|
|
207
|
+
final_message: null,
|
|
208
|
+
tokens: { input: null, output: null, total: null },
|
|
209
|
+
duration_ms: null,
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Create job directory and return paths to status.json and agent.log
|
|
216
|
+
*/
|
|
217
|
+
export function ensureJobDir(jobId: string): JobPaths {
|
|
218
|
+
const jobDir = getJobDir(jobId);
|
|
219
|
+
mkdirSync(jobDir, { recursive: true });
|
|
220
|
+
return {
|
|
221
|
+
statusPath: join(jobDir, "status.json"),
|
|
222
|
+
logPath: join(jobDir, "agent.log"),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Write status.json atomically (via temp file + rename)
|
|
228
|
+
*/
|
|
229
|
+
export function writeJobStatus(statusPath: string, status: FileJobStatus): void {
|
|
230
|
+
const tmpPath = statusPath + ".tmp";
|
|
231
|
+
writeFileSync(tmpPath, JSON.stringify(status, null, 2));
|
|
232
|
+
renameSync(tmpPath, statusPath);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Append a line to the agent.log file
|
|
237
|
+
*/
|
|
238
|
+
export function appendToLog(logPath: string, line: string): void {
|
|
239
|
+
appendFileSync(logPath, line + "\n");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ============================================================================
|
|
243
|
+
// High-level Job Tracker
|
|
244
|
+
// ============================================================================
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* JobTracker manages file-based status for a single job.
|
|
248
|
+
* Use this to track process-level telemetry alongside Convex.
|
|
249
|
+
*/
|
|
250
|
+
export class JobTracker {
|
|
251
|
+
private status: FileJobStatus;
|
|
252
|
+
private paths: JobPaths;
|
|
253
|
+
|
|
254
|
+
constructor(jobId: string, harness: string, pid: number) {
|
|
255
|
+
this.paths = ensureJobDir(jobId);
|
|
256
|
+
this.status = createFileJobStatus(jobId, harness, pid);
|
|
257
|
+
this.status.logs = this.paths.logPath;
|
|
258
|
+
this.writeStatus();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Create a JobTracker from an existing status on disk.
|
|
263
|
+
* Used for recovery/reconciliation of orphaned jobs.
|
|
264
|
+
*/
|
|
265
|
+
static fromExisting(
|
|
266
|
+
jobId: string,
|
|
267
|
+
status: FileJobStatus,
|
|
268
|
+
paths: JobPaths
|
|
269
|
+
): JobTracker {
|
|
270
|
+
const tracker = Object.create(JobTracker.prototype) as JobTracker;
|
|
271
|
+
tracker.status = { ...status };
|
|
272
|
+
tracker.paths = paths;
|
|
273
|
+
return tracker;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Update status reason (current activity) */
|
|
277
|
+
updateStatusReason(reason: string): void {
|
|
278
|
+
this.status.status_reason = reason;
|
|
279
|
+
this.status.last_event_time = utcNowIso();
|
|
280
|
+
this.writeStatus();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Increment operation count */
|
|
284
|
+
incrementOperations(): void {
|
|
285
|
+
this.status.operations++;
|
|
286
|
+
this.status.last_event_time = utcNowIso();
|
|
287
|
+
this.writeStatus();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Record an event (increments ops, updates timestamp, optionally updates reason) */
|
|
291
|
+
recordEvent(statusReason?: string): void {
|
|
292
|
+
this.status.operations++;
|
|
293
|
+
this.status.last_event_time = utcNowIso();
|
|
294
|
+
if (statusReason) {
|
|
295
|
+
this.status.status_reason = statusReason;
|
|
296
|
+
}
|
|
297
|
+
this.writeStatus();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** Append raw JSON line to log file */
|
|
301
|
+
logLine(line: string): void {
|
|
302
|
+
appendToLog(this.paths.logPath, line);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Mark job as complete */
|
|
306
|
+
complete(finalMessage?: string): void {
|
|
307
|
+
this.status.status = "complete";
|
|
308
|
+
this.status.status_reason = "completed";
|
|
309
|
+
this.status.end_time = utcNowIso();
|
|
310
|
+
if (finalMessage) {
|
|
311
|
+
this.status.completion.final_message = finalMessage;
|
|
312
|
+
this.status.completion.messages.push(finalMessage);
|
|
313
|
+
}
|
|
314
|
+
this.writeStatus();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** Mark job as failed */
|
|
318
|
+
fail(reason: string): void {
|
|
319
|
+
this.status.status = "error";
|
|
320
|
+
this.status.status_reason = reason;
|
|
321
|
+
this.status.end_time = utcNowIso();
|
|
322
|
+
this.writeStatus();
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** Mark job as timed out */
|
|
326
|
+
timeout(): void {
|
|
327
|
+
this.status.status = "timeout";
|
|
328
|
+
this.status.status_reason = "idle_timeout";
|
|
329
|
+
this.status.end_time = utcNowIso();
|
|
330
|
+
this.writeStatus();
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** Update token stats */
|
|
334
|
+
updateTokens(input: number | null, output: number | null): void {
|
|
335
|
+
this.status.completion.tokens.input = input;
|
|
336
|
+
this.status.completion.tokens.output = output;
|
|
337
|
+
if (input != null && output != null) {
|
|
338
|
+
this.status.completion.tokens.total = input + output;
|
|
339
|
+
}
|
|
340
|
+
this.writeStatus();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** Get current status */
|
|
344
|
+
getStatus(): FileJobStatus {
|
|
345
|
+
return { ...this.status };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** Get log path */
|
|
349
|
+
getLogPath(): string {
|
|
350
|
+
return this.paths.logPath;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** Get status path */
|
|
354
|
+
getStatusPath(): string {
|
|
355
|
+
return this.paths.statusPath;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Get current read position in log file */
|
|
359
|
+
getReadPosition(): number {
|
|
360
|
+
return this.status.read_position ?? 0;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Update read position (bytes processed in log file) */
|
|
364
|
+
setReadPosition(position: number): void {
|
|
365
|
+
this.status.read_position = position;
|
|
366
|
+
this.writeStatus();
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** Get job ID */
|
|
370
|
+
getJobId(): string {
|
|
371
|
+
return this.status.job_id;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** Get harness type */
|
|
375
|
+
getHarness(): string {
|
|
376
|
+
return this.status.harness;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** Check if job is in a terminal state */
|
|
380
|
+
isTerminal(): boolean {
|
|
381
|
+
return this.status.status !== "running";
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
private writeStatus(): void {
|
|
385
|
+
writeJobStatus(this.paths.statusPath, this.status);
|
|
386
|
+
}
|
|
387
|
+
}
|