@tea-agent/loop-agent 0.17.0 → 0.17.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/CHANGELOG.md +36 -0
- package/dist/application/dag/args.js +9 -2
- package/dist/executors/dag-pi-executor.js +11 -0
- package/dist/worker/console/draft-store.js +81 -3
- package/dist/worker/console/interview/grill-me.js +253 -0
- package/dist/worker/console/operation-runner.js +2 -1
- package/dist/worker/console/operator-actions.js +609 -20
- package/dist/worker/console/pi-readiness.js +192 -18
- package/dist/worker/console/prd-identity.js +102 -0
- package/dist/worker/console/resolve-dag-run-for-task.js +76 -0
- package/dist/worker/console/server.js +11 -2
- package/dist/worker/console/static/assets/index-KUSib7aM.js +16 -0
- package/dist/worker/console/static/assets/index-ucIzpaGJ.css +1 -0
- package/dist/worker/console/static/index.html +3 -3
- package/dist/worker/console/workflow-kinds.js +46 -0
- package/dist/worker/materialize/harness-task-materializer.js +34 -0
- package/dist/worker/observe/routes.js +8 -1
- package/dist/worker/observe/static/index.html +8 -8
- package/dist/worker/observe/static/styles.css +296 -181
- package/package.json +1 -1
- package/dist/worker/console/static/assets/index-CbnMgdWa.js +0 -9
- package/dist/worker/console/static/assets/index-Dnj0RVs8.css +0 -1
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
* Pi readiness gate for Official Console Happy Path.
|
|
3
3
|
* setup-required | ready | degraded — no pure form fallback.
|
|
4
4
|
*/
|
|
5
|
+
import path from "node:path";
|
|
5
6
|
const SETUP_BLOCKED = Object.freeze([
|
|
6
7
|
"newTask",
|
|
7
8
|
"importPrd",
|
|
9
|
+
"bootstrapFromPrd",
|
|
8
10
|
"contractApply",
|
|
9
11
|
"contractAdopt",
|
|
10
12
|
"dagRunTask",
|
|
@@ -12,6 +14,7 @@ const SETUP_BLOCKED = Object.freeze([
|
|
|
12
14
|
"prepareDagConfirmation",
|
|
13
15
|
"interviewStart",
|
|
14
16
|
"interviewTurn",
|
|
17
|
+
"interviewSkip",
|
|
15
18
|
]);
|
|
16
19
|
const DEGRADED_BLOCKED = Object.freeze([
|
|
17
20
|
"contractApply",
|
|
@@ -20,6 +23,13 @@ const DEGRADED_BLOCKED = Object.freeze([
|
|
|
20
23
|
"runDag",
|
|
21
24
|
// new Interview may resume; brand-new apply/run still blocked
|
|
22
25
|
]);
|
|
26
|
+
/** Default Interview / DAG model matrix (provider, modelId). */
|
|
27
|
+
const INTERVIEW_MODEL_CANDIDATES = Object.freeze([
|
|
28
|
+
["openai", "gpt-5.5"],
|
|
29
|
+
["openai", "gpt-5.3-codex-spark"],
|
|
30
|
+
["zai", "glm-5.2"],
|
|
31
|
+
["zhipu", "glm-5.2"],
|
|
32
|
+
]);
|
|
23
33
|
export function classifyPiReadiness(probe) {
|
|
24
34
|
if (!probe.sdkAvailable ||
|
|
25
35
|
!probe.authReady ||
|
|
@@ -57,30 +67,194 @@ export function isActionBlockedByReadiness(state, action) {
|
|
|
57
67
|
}
|
|
58
68
|
return DEGRADED_BLOCKED.includes(action);
|
|
59
69
|
}
|
|
70
|
+
function countAvailableModels(snapshot) {
|
|
71
|
+
return Array.isArray(snapshot.available) ? snapshot.available.length : 0;
|
|
72
|
+
}
|
|
73
|
+
function hasProviderConfig(snapshot) {
|
|
74
|
+
const authKeys = snapshot.auth && typeof snapshot.auth === "object"
|
|
75
|
+
? Object.keys(snapshot.auth).length
|
|
76
|
+
: 0;
|
|
77
|
+
const configured = snapshot.configuredProviders &&
|
|
78
|
+
typeof snapshot.configuredProviders === "object"
|
|
79
|
+
? Object.keys(snapshot.configuredProviders).length
|
|
80
|
+
: 0;
|
|
81
|
+
const stored = snapshot.storedProviders && typeof snapshot.storedProviders === "object"
|
|
82
|
+
? Object.keys(snapshot.storedProviders).length
|
|
83
|
+
: 0;
|
|
84
|
+
return authKeys > 0 || configured > 0 || stored > 0;
|
|
85
|
+
}
|
|
60
86
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
87
|
+
* Discover live Pi SDK / auth / model readiness via ModelRuntime.
|
|
88
|
+
* Kept inside worker/console (dynamic import) so Worker does not depend on
|
|
89
|
+
* executors; contract matches pi-sdk-executor 0.80.10.
|
|
64
90
|
*/
|
|
65
|
-
export async function
|
|
66
|
-
|
|
67
|
-
|
|
91
|
+
export async function discoverPiReadinessProbe() {
|
|
92
|
+
let sdk;
|
|
93
|
+
try {
|
|
94
|
+
sdk = (await import("@earendil-works/pi-coding-agent"));
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
98
|
+
return {
|
|
99
|
+
sdkAvailable: false,
|
|
100
|
+
authReady: false,
|
|
101
|
+
modelsAvailable: false,
|
|
102
|
+
interviewSessionOk: false,
|
|
103
|
+
details: {
|
|
104
|
+
sdkMessage: `Pi SDK import failed: ${message}`,
|
|
105
|
+
authMessage: "skipped (sdk unavailable)",
|
|
106
|
+
modelsMessage: "skipped (sdk unavailable)",
|
|
107
|
+
sessionMessage: "skipped (sdk unavailable)",
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const ModelRuntime = sdk.ModelRuntime;
|
|
112
|
+
if (typeof sdk.createAgentSession !== "function" ||
|
|
113
|
+
typeof sdk.getAgentDir !== "function" ||
|
|
114
|
+
typeof ModelRuntime?.create !== "function") {
|
|
115
|
+
return {
|
|
116
|
+
sdkAvailable: false,
|
|
117
|
+
authReady: false,
|
|
118
|
+
modelsAvailable: false,
|
|
119
|
+
interviewSessionOk: false,
|
|
120
|
+
details: {
|
|
121
|
+
sdkMessage: "Pi SDK incompatible: requires createAgentSession, getAgentDir, and ModelRuntime.create (0.80.10)",
|
|
122
|
+
authMessage: "skipped (sdk incompatible)",
|
|
123
|
+
modelsMessage: "skipped (sdk incompatible)",
|
|
124
|
+
sessionMessage: "skipped (sdk incompatible)",
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
let agentDir;
|
|
129
|
+
try {
|
|
130
|
+
agentDir = sdk.getAgentDir();
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
134
|
+
return {
|
|
135
|
+
sdkAvailable: false,
|
|
136
|
+
authReady: false,
|
|
137
|
+
modelsAvailable: false,
|
|
138
|
+
interviewSessionOk: false,
|
|
139
|
+
details: {
|
|
140
|
+
sdkMessage: `getAgentDir failed: ${message}`,
|
|
141
|
+
authMessage: "skipped",
|
|
142
|
+
modelsMessage: "skipped",
|
|
143
|
+
sessionMessage: "skipped",
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
let runtime;
|
|
148
|
+
try {
|
|
149
|
+
runtime = await ModelRuntime.create({
|
|
150
|
+
authPath: path.join(agentDir, "auth.json"),
|
|
151
|
+
modelsPath: path.join(agentDir, "models.json"),
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
156
|
+
return {
|
|
157
|
+
sdkAvailable: true,
|
|
158
|
+
authReady: false,
|
|
159
|
+
modelsAvailable: false,
|
|
160
|
+
interviewSessionOk: false,
|
|
161
|
+
details: {
|
|
162
|
+
sdkMessage: "Pi SDK 0.80.10 contract available",
|
|
163
|
+
authMessage: `ModelRuntime.create failed: ${message}`,
|
|
164
|
+
modelsMessage: "skipped (runtime init failed)",
|
|
165
|
+
sessionMessage: "skipped (runtime init failed)",
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
const snapshot = runtime.snapshot ?? {};
|
|
170
|
+
const availableCount = countAvailableModels(snapshot);
|
|
171
|
+
const providerConfigured = hasProviderConfig(snapshot);
|
|
172
|
+
const resolvedInterviewModels = [];
|
|
173
|
+
if (typeof runtime.getModel === "function") {
|
|
174
|
+
for (const [provider, modelId] of INTERVIEW_MODEL_CANDIDATES) {
|
|
175
|
+
try {
|
|
176
|
+
if (runtime.getModel(provider, modelId)) {
|
|
177
|
+
resolvedInterviewModels.push(`${provider}/${modelId}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
// ignore per-model resolution errors
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const authReady = availableCount > 0 || providerConfigured;
|
|
186
|
+
const modelsAvailable = availableCount > 0 || resolvedInterviewModels.length > 0;
|
|
187
|
+
let interviewSessionOk = false;
|
|
188
|
+
let sessionMessage;
|
|
189
|
+
if (!authReady || !modelsAvailable) {
|
|
190
|
+
sessionMessage =
|
|
191
|
+
"Interview session preflight skipped (auth/models not ready)";
|
|
192
|
+
}
|
|
193
|
+
else if (typeof sdk.SessionManager?.inMemory !== "function") {
|
|
194
|
+
sessionMessage =
|
|
195
|
+
"SessionManager.inMemory unavailable; Interview session preflight failed";
|
|
68
196
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
197
|
+
else {
|
|
198
|
+
try {
|
|
199
|
+
sdk.SessionManager.inMemory(process.cwd());
|
|
200
|
+
interviewSessionOk = true;
|
|
201
|
+
sessionMessage = `Interview session preflight ok (${availableCount} available model(s); matrix=${resolvedInterviewModels.join(",") || "none"})`;
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
205
|
+
sessionMessage = `Interview session preflight failed: ${message}`;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
sdkAvailable: true,
|
|
210
|
+
authReady,
|
|
211
|
+
modelsAvailable,
|
|
212
|
+
interviewSessionOk,
|
|
76
213
|
details: {
|
|
77
|
-
sdkMessage:
|
|
78
|
-
authMessage:
|
|
79
|
-
|
|
80
|
-
|
|
214
|
+
sdkMessage: `Pi SDK 0.80.10 contract available (agentDir=${agentDir})`,
|
|
215
|
+
authMessage: authReady
|
|
216
|
+
? `credentials/providers ready (available=${availableCount}, providerConfig=${providerConfigured})`
|
|
217
|
+
: "no available models and no stored/configured provider credentials (set Pi auth or provider API keys)",
|
|
218
|
+
modelsMessage: modelsAvailable
|
|
219
|
+
? `models ready (available=${availableCount}, interviewMatrix=${resolvedInterviewModels.join(",") || "none"})`
|
|
220
|
+
: "no available models for Interview",
|
|
221
|
+
sessionMessage,
|
|
81
222
|
},
|
|
82
223
|
};
|
|
83
|
-
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Default probe for process boot. Discovers live Pi ModelRuntime when no
|
|
227
|
+
* override is injected. Tests inject override for determinism.
|
|
228
|
+
*/
|
|
229
|
+
export async function probePiReadiness(options) {
|
|
230
|
+
if (options?.override) {
|
|
231
|
+
return buildPiReadinessReport(options.override);
|
|
232
|
+
}
|
|
233
|
+
try {
|
|
234
|
+
const probe = await discoverPiReadinessProbe();
|
|
235
|
+
if (options?.siblingControllerOk && !probe.details) {
|
|
236
|
+
probe.details = {};
|
|
237
|
+
}
|
|
238
|
+
if (options?.siblingControllerOk && probe.details) {
|
|
239
|
+
probe.details.sdkMessage = `${probe.details.sdkMessage ?? "sdk probed"}; sibling controller identity ok`;
|
|
240
|
+
}
|
|
241
|
+
return buildPiReadinessReport(probe);
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
245
|
+
return buildPiReadinessReport({
|
|
246
|
+
sdkAvailable: false,
|
|
247
|
+
authReady: false,
|
|
248
|
+
modelsAvailable: false,
|
|
249
|
+
interviewSessionOk: false,
|
|
250
|
+
details: {
|
|
251
|
+
sdkMessage: `Pi readiness discovery error: ${message}`,
|
|
252
|
+
authMessage: "no credential probe completed",
|
|
253
|
+
modelsMessage: "no model list available",
|
|
254
|
+
sessionMessage: "Interview session preflight not run",
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
}
|
|
84
258
|
}
|
|
85
259
|
export function readinessGateFailure(report, action) {
|
|
86
260
|
if (!isActionBlockedByReadiness(report.state, action))
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derive Console task title + canonical taskId from imported PRD markdown.
|
|
3
|
+
* Deterministic (no LLM): first ATX heading, else first meaningful line.
|
|
4
|
+
*/
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { formatLocalBusinessDate } from "../../task/runtime.js";
|
|
7
|
+
const GENERIC_SLUGS = new Set([
|
|
8
|
+
"test",
|
|
9
|
+
"fix",
|
|
10
|
+
"tmp",
|
|
11
|
+
"new-task",
|
|
12
|
+
"requirement",
|
|
13
|
+
"req",
|
|
14
|
+
"prd",
|
|
15
|
+
"untitled",
|
|
16
|
+
]);
|
|
17
|
+
function cleanTitleLine(line) {
|
|
18
|
+
return line
|
|
19
|
+
.replace(/^#{1,6}\s+/, "")
|
|
20
|
+
.replace(/^>\s+/, "")
|
|
21
|
+
.replace(/^[*_\s]+|[*_\s]+$/g, "")
|
|
22
|
+
.replace(/\s+/g, " ")
|
|
23
|
+
.trim();
|
|
24
|
+
}
|
|
25
|
+
function isNoiseLine(line) {
|
|
26
|
+
const t = line.trim();
|
|
27
|
+
if (!t)
|
|
28
|
+
return true;
|
|
29
|
+
if (/^---+$/.test(t))
|
|
30
|
+
return true;
|
|
31
|
+
if (/^```/.test(t))
|
|
32
|
+
return true;
|
|
33
|
+
if (/^<!--/.test(t))
|
|
34
|
+
return true;
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
export function extractPrdTitle(content) {
|
|
38
|
+
const lines = content.split(/\r?\n/);
|
|
39
|
+
for (const raw of lines) {
|
|
40
|
+
const trimmed = raw.trim();
|
|
41
|
+
if (!/^#\s+\S/.test(trimmed))
|
|
42
|
+
continue;
|
|
43
|
+
const title = cleanTitleLine(trimmed);
|
|
44
|
+
if (title)
|
|
45
|
+
return { title: title.slice(0, 120), source: "heading" };
|
|
46
|
+
}
|
|
47
|
+
for (const raw of lines) {
|
|
48
|
+
if (isNoiseLine(raw))
|
|
49
|
+
continue;
|
|
50
|
+
const trimmed = raw.trim();
|
|
51
|
+
if (/^#{2,6}\s+/.test(trimmed))
|
|
52
|
+
continue;
|
|
53
|
+
const title = cleanTitleLine(trimmed);
|
|
54
|
+
if (title.length >= 2) {
|
|
55
|
+
return { title: title.slice(0, 120), source: "first-line" };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
for (const raw of lines) {
|
|
59
|
+
const trimmed = raw.trim();
|
|
60
|
+
if (!/^#{2,6}\s+\S/.test(trimmed))
|
|
61
|
+
continue;
|
|
62
|
+
const title = cleanTitleLine(trimmed);
|
|
63
|
+
if (title)
|
|
64
|
+
return { title: title.slice(0, 120), source: "heading" };
|
|
65
|
+
}
|
|
66
|
+
const hash = createHash("sha256").update(content).digest("hex").slice(0, 8);
|
|
67
|
+
return { title: `需求任务 ${hash}`, source: "content-hash" };
|
|
68
|
+
}
|
|
69
|
+
export function asciiSlugFromTitle(title, contentForFallback) {
|
|
70
|
+
const ascii = title
|
|
71
|
+
.normalize("NFKD")
|
|
72
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
73
|
+
.toLowerCase()
|
|
74
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
75
|
+
.replace(/^-+|-+$/g, "")
|
|
76
|
+
.replace(/-{2,}/g, "-")
|
|
77
|
+
.slice(0, 48);
|
|
78
|
+
if (ascii.length >= 3 && !GENERIC_SLUGS.has(ascii))
|
|
79
|
+
return ascii;
|
|
80
|
+
const hash = createHash("sha256")
|
|
81
|
+
.update(contentForFallback || title)
|
|
82
|
+
.digest("hex")
|
|
83
|
+
.slice(0, 8);
|
|
84
|
+
return `req-${hash}`;
|
|
85
|
+
}
|
|
86
|
+
export function deriveTaskIdentityFromPrd(content, now = new Date()) {
|
|
87
|
+
const extracted = extractPrdTitle(content);
|
|
88
|
+
const slug = asciiSlugFromTitle(extracted.title, content);
|
|
89
|
+
const date = formatLocalBusinessDate(now);
|
|
90
|
+
return {
|
|
91
|
+
title: extracted.title,
|
|
92
|
+
slug,
|
|
93
|
+
taskId: `${date}-${slug}`,
|
|
94
|
+
source: extracted.source,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
export function nextTaskIdRevision(taskId, revision) {
|
|
98
|
+
if (revision <= 1)
|
|
99
|
+
return taskId;
|
|
100
|
+
const base = taskId.replace(/-r\d+$/, "");
|
|
101
|
+
return `${base}-r${revision}`;
|
|
102
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the newest DAG run id for a harness task under `.harness/dag-runs`.
|
|
3
|
+
* Used by Console observeLink so task-scoped opens land on `#/dag/<runId>`.
|
|
4
|
+
*/
|
|
5
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
const LIFECYCLE_ORDER = ["active", "paused", "completed"];
|
|
8
|
+
async function readTaskIdFromRunDir(runDir) {
|
|
9
|
+
const runJsonPath = path.join(runDir, "run.json");
|
|
10
|
+
try {
|
|
11
|
+
const raw = JSON.parse(await readFile(runJsonPath, "utf8"));
|
|
12
|
+
const fromBinding = raw.taskContractBinding?.taskId?.trim() ||
|
|
13
|
+
raw.sourceBinding?.taskId?.trim();
|
|
14
|
+
if (fromBinding)
|
|
15
|
+
return fromBinding;
|
|
16
|
+
const title = raw.title?.trim() ?? "";
|
|
17
|
+
const hybrid = /^Hybrid DAG:\s*(.+)$/i.exec(title);
|
|
18
|
+
if (hybrid?.[1]?.trim())
|
|
19
|
+
return hybrid[1].trim();
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
/* missing/invalid run.json — skip */
|
|
23
|
+
}
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Prefer active/paused over completed; within a lifecycle, newest mtime wins.
|
|
28
|
+
*/
|
|
29
|
+
export async function resolveLatestDagRunIdForTask(repoRoot, taskId) {
|
|
30
|
+
const wanted = taskId.trim();
|
|
31
|
+
if (!wanted)
|
|
32
|
+
return undefined;
|
|
33
|
+
const matches = [];
|
|
34
|
+
for (const lifecycle of LIFECYCLE_ORDER) {
|
|
35
|
+
const lifecycleDir = path.join(repoRoot, ".harness", "dag-runs", lifecycle);
|
|
36
|
+
let entries = [];
|
|
37
|
+
try {
|
|
38
|
+
entries = await readdir(lifecycleDir);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
for (const name of entries) {
|
|
44
|
+
if (!name || name === ".gitkeep")
|
|
45
|
+
continue;
|
|
46
|
+
const runDir = path.join(lifecycleDir, name);
|
|
47
|
+
let st;
|
|
48
|
+
try {
|
|
49
|
+
st = await stat(runDir);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (!st.isDirectory())
|
|
55
|
+
continue;
|
|
56
|
+
const boundTaskId = await readTaskIdFromRunDir(runDir);
|
|
57
|
+
if (boundTaskId !== wanted)
|
|
58
|
+
continue;
|
|
59
|
+
matches.push({
|
|
60
|
+
dagRunId: name,
|
|
61
|
+
lifecycle,
|
|
62
|
+
mtimeMs: st.mtimeMs,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (matches.length === 0)
|
|
67
|
+
return undefined;
|
|
68
|
+
const lifecycleRank = (value) => LIFECYCLE_ORDER.indexOf(value);
|
|
69
|
+
matches.sort((a, b) => {
|
|
70
|
+
const life = lifecycleRank(a.lifecycle) - lifecycleRank(b.lifecycle);
|
|
71
|
+
if (life !== 0)
|
|
72
|
+
return life;
|
|
73
|
+
return b.mtimeMs - a.mtimeMs;
|
|
74
|
+
});
|
|
75
|
+
return matches[0];
|
|
76
|
+
}
|
|
@@ -48,6 +48,7 @@ export async function createConsoleServer(options) {
|
|
|
48
48
|
const readiness = await probePiReadiness({
|
|
49
49
|
override: options.piReadinessProbe,
|
|
50
50
|
});
|
|
51
|
+
let readinessCache = readiness;
|
|
51
52
|
const artifactRoot = path.join(appData.root, "client-artifacts", appData.fingerprint);
|
|
52
53
|
const client = options.createClient?.(artifactRoot) ??
|
|
53
54
|
new LoopAgentClient({
|
|
@@ -58,7 +59,14 @@ export async function createConsoleServer(options) {
|
|
|
58
59
|
if (!options.skipReconcile) {
|
|
59
60
|
await operations.reconcileOnBoot();
|
|
60
61
|
}
|
|
61
|
-
const getReadiness = () =>
|
|
62
|
+
const getReadiness = () => readinessCache;
|
|
63
|
+
const refreshReadiness = async () => {
|
|
64
|
+
readinessCache = await probePiReadiness({
|
|
65
|
+
override: options.piReadinessProbe,
|
|
66
|
+
});
|
|
67
|
+
actionContext.readiness = readinessCache;
|
|
68
|
+
return readinessCache;
|
|
69
|
+
};
|
|
62
70
|
const actionContext = {
|
|
63
71
|
repoRoot,
|
|
64
72
|
appData,
|
|
@@ -70,8 +78,9 @@ export async function createConsoleServer(options) {
|
|
|
70
78
|
confirmations,
|
|
71
79
|
interviews,
|
|
72
80
|
assessments,
|
|
73
|
-
readiness,
|
|
81
|
+
readiness: readinessCache,
|
|
74
82
|
getReadiness,
|
|
83
|
+
refreshReadiness,
|
|
75
84
|
observeBaseUrl: options.observeBaseUrl,
|
|
76
85
|
fetchImpl: options.fetchImpl,
|
|
77
86
|
};
|