@hadooppei/hwcode 1.0.6 → 1.0.8
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/.pi/extensions/command-filter.ts +2 -3
- package/.pi/extensions/cwd.ts +1 -4
- package/.pi/extensions/knowledge.ts +279 -0
- package/.pi/extensions/workflows/cloud/activation.ts +57 -43
- package/.pi/extensions/workflows/cloud/commands.ts +1 -86
- package/.pi/extensions/workflows/cloud/events.ts +16 -19
- package/.pi/extensions/workflows/cloud/interactions.ts +102 -0
- package/.pi/extensions/workflows/cloud/provider-tools.ts +15 -9
- package/.pi/extensions/workflows/cloud/runner-tools.ts +21 -20
- package/.pi/extensions/workflows/cloud/runtime.ts +41 -4
- package/.pi/extensions/workflows/cloud/terraform-tools.ts +43 -30
- package/.pi/extensions/workflows/sdd.ts +2 -1
- package/.pi/extensions/workflows/vibe.ts +2 -1
- package/.pi/extensions/workflows/workspace-guard.ts +1 -5
- package/.pi/lib/extension-ui.ts +52 -0
- package/.pi/lib/knowledge/extractor.ts +35 -0
- package/.pi/lib/knowledge/matcher.ts +122 -0
- package/.pi/lib/knowledge/review-worker.ts +64 -0
- package/.pi/lib/knowledge/sanitize.ts +26 -0
- package/.pi/lib/knowledge/store.ts +251 -0
- package/.pi/lib/knowledge/types.ts +59 -0
- package/.pi/lib/knowledge/worker-protocol.ts +12 -0
- package/.pi/lib/runtime/defaults.ts +28 -0
- package/.pi/lib/runtime/paths.ts +28 -16
- package/.pi/lib/tool-result.ts +7 -0
- package/.pi/lib/workflows/cloud/bundles.ts +73 -40
- package/.pi/lib/workflows/cloud/workspace.ts +6 -0
- package/.pi/lib/workflows/state.ts +6 -26
- package/.pi/skills/hwcode-cloud/SKILL.md +2 -2
- package/README.md +33 -31
- package/bin/hwcode.js +2 -3
- package/package.json +1 -1
- package/.pi/extensions/workflows/cloud/shared.ts +0 -224
- package/.pi/lib/workflows/cloud/template-save.ts +0 -108
- package/.pi/lib/workflows/cloud/templates.ts +0 -314
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
filterCommandSuggestions,
|
|
6
6
|
parseHiddenCommands,
|
|
7
7
|
} from "../lib/command-filter.ts";
|
|
8
|
+
import { notify } from "../lib/extension-ui.ts";
|
|
8
9
|
import { loadLayeredJson } from "../lib/runtime/config.ts";
|
|
9
10
|
|
|
10
11
|
function loadHiddenCommands(cwd: string, notify: (message: string) => void): Set<string> {
|
|
@@ -21,9 +22,7 @@ function loadHiddenCommands(cwd: string, notify: (message: string) => void): Set
|
|
|
21
22
|
export default function commandFilterExtension(pi: ExtensionAPI) {
|
|
22
23
|
pi.on("session_start", async (_event, ctx) => {
|
|
23
24
|
if (ctx.mode !== "tui") return;
|
|
24
|
-
const hiddenCommands = loadHiddenCommands(ctx.cwd, (message) =>
|
|
25
|
-
if (ctx.hasUI) ctx.ui.notify(message, "warning");
|
|
26
|
-
});
|
|
25
|
+
const hiddenCommands = loadHiddenCommands(ctx.cwd, (message) => notify(ctx, message, "warning"));
|
|
27
26
|
|
|
28
27
|
ctx.ui.addAutocompleteProvider((current) => ({
|
|
29
28
|
triggerCharacters: current.triggerCharacters,
|
package/.pi/extensions/cwd.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
} from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import { createLocalBashOperations } from "@earendil-works/pi-coding-agent";
|
|
7
7
|
|
|
8
|
+
import { notify } from "../lib/extension-ui.ts";
|
|
8
9
|
import {
|
|
9
10
|
canonicalizeDirectory,
|
|
10
11
|
clearWorkingDirectoryState,
|
|
@@ -28,10 +29,6 @@ interface ChangeResult {
|
|
|
28
29
|
error?: string;
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
function notify(ctx: ExtensionContext, message: string, level: "info" | "warning" | "error" = "info"): void {
|
|
32
|
-
if (ctx.hasUI) ctx.ui.notify(message, level);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
32
|
function commandArgument(args: string): string {
|
|
36
33
|
const trimmed = args.trim();
|
|
37
34
|
if (!trimmed) return "";
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { Worker } from "node:worker_threads";
|
|
3
|
+
|
|
4
|
+
import { Type } from "@earendil-works/pi-ai";
|
|
5
|
+
import { defineTool, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
buildKnowledgeExtractionPrompt, knowledgeDeltaDigest, KNOWLEDGE_EXTRACTION_SYSTEM_PROMPT,
|
|
9
|
+
} from "../lib/knowledge/extractor.ts";
|
|
10
|
+
import { matchKnowledge } from "../lib/knowledge/matcher.ts";
|
|
11
|
+
import {
|
|
12
|
+
loadKnowledgeById, loadKnowledgeSnapshot, projectKnowledgeKey,
|
|
13
|
+
} from "../lib/knowledge/store.ts";
|
|
14
|
+
import type { KnowledgeReviewState, KnowledgeSnapshot } from "../lib/knowledge/types.ts";
|
|
15
|
+
import type { KnowledgeWorkerInput, KnowledgeWorkerOutput } from "../lib/knowledge/worker-protocol.ts";
|
|
16
|
+
import { KNOWLEDGE_RUNTIME_DEFAULTS } from "../lib/runtime/defaults.ts";
|
|
17
|
+
import { getWorkingDirectory } from "../lib/working-directory.ts";
|
|
18
|
+
|
|
19
|
+
export const KNOWLEDGE_REVIEW_STATE_TYPE = "hwcode-knowledge-review-state";
|
|
20
|
+
|
|
21
|
+
interface PendingReview {
|
|
22
|
+
requestId: string;
|
|
23
|
+
lastEntryId: string;
|
|
24
|
+
deltaDigest: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function textContent(content: unknown): string {
|
|
28
|
+
if (typeof content === "string") return content;
|
|
29
|
+
if (!Array.isArray(content)) return "";
|
|
30
|
+
return content.map((item) => {
|
|
31
|
+
if (!item || typeof item !== "object") return "";
|
|
32
|
+
const value = item as Record<string, unknown>;
|
|
33
|
+
return typeof value.text === "string" ? value.text : "";
|
|
34
|
+
}).filter(Boolean).join("\n");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function entryMessageText(entry: unknown): string {
|
|
38
|
+
if (!entry || typeof entry !== "object") return "";
|
|
39
|
+
const data = entry as Record<string, unknown>;
|
|
40
|
+
if (data.type === "compaction" || data.type === "branch_summary") {
|
|
41
|
+
return typeof data.summary === "string" ? `[summary]\n${data.summary}` : "";
|
|
42
|
+
}
|
|
43
|
+
if (data.type !== "message" || !data.message || typeof data.message !== "object") return "";
|
|
44
|
+
const message = data.message as Record<string, unknown>;
|
|
45
|
+
const role = typeof message.role === "string" ? message.role : "message";
|
|
46
|
+
const content = textContent(message.content);
|
|
47
|
+
return content ? `[${role}]\n${content}` : "";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function entryId(entry: unknown): string | undefined {
|
|
51
|
+
if (!entry || typeof entry !== "object") return undefined;
|
|
52
|
+
const id = (entry as Record<string, unknown>).id;
|
|
53
|
+
return typeof id === "string" ? id : undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function reviewableEntries(ctx: ExtensionContext): readonly unknown[] {
|
|
57
|
+
return ctx.sessionManager.getBranch().filter((entry) => Boolean(entryMessageText(entry)));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function lastReviewableEntryId(ctx: ExtensionContext): string | undefined {
|
|
61
|
+
const entries = reviewableEntries(ctx);
|
|
62
|
+
return entries.length > 0 ? entryId(entries[entries.length - 1]) : undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function buildConversationDelta(ctx: ExtensionContext, lastReviewedEntryId?: string): { text: string; lastEntryId?: string } {
|
|
66
|
+
const entries = reviewableEntries(ctx);
|
|
67
|
+
const lastIndex = lastReviewedEntryId ? entries.findIndex((entry) => entryId(entry) === lastReviewedEntryId) : -1;
|
|
68
|
+
const pending = entries.slice(lastIndex + 1);
|
|
69
|
+
const lastEntryId = pending.length > 0 ? entryId(pending[pending.length - 1]) : undefined;
|
|
70
|
+
const parts: string[] = [];
|
|
71
|
+
let chars = 0;
|
|
72
|
+
for (const entry of pending.slice().reverse()) {
|
|
73
|
+
const part = entryMessageText(entry);
|
|
74
|
+
if (!part) continue;
|
|
75
|
+
const remaining = KNOWLEDGE_RUNTIME_DEFAULTS.review.maxDeltaChars - chars;
|
|
76
|
+
if (remaining <= 0) break;
|
|
77
|
+
const clipped = part.slice(0, remaining);
|
|
78
|
+
parts.unshift(clipped);
|
|
79
|
+
chars += clipped.length;
|
|
80
|
+
if (clipped.length < part.length) break;
|
|
81
|
+
}
|
|
82
|
+
return { text: parts.join("\n\n"), lastEntryId };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function loadReviewState(ctx: ExtensionContext): KnowledgeReviewState {
|
|
86
|
+
for (const entry of ctx.sessionManager.getBranch().slice().reverse()) {
|
|
87
|
+
if (entry.type !== "custom" || entry.customType !== KNOWLEDGE_REVIEW_STATE_TYPE) continue;
|
|
88
|
+
const data = entry.data as Partial<KnowledgeReviewState> | undefined;
|
|
89
|
+
if (data?.version === 1) return data as KnowledgeReviewState;
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
return { version: 1 };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export default function registerKnowledgeExtension(pi: ExtensionAPI): void {
|
|
96
|
+
let worker: Worker | undefined;
|
|
97
|
+
let activeContext: ExtensionContext | undefined;
|
|
98
|
+
let activeProjectRoot = "";
|
|
99
|
+
let activeProjectKey = "";
|
|
100
|
+
let snapshot: KnowledgeSnapshot = { rulesPrompt: "", memoryPrompt: "", catalog: { version: 2, updatedAt: "", items: [] } };
|
|
101
|
+
let reviewState: KnowledgeReviewState = { version: 1 };
|
|
102
|
+
let pendingReview: PendingReview | undefined;
|
|
103
|
+
let reviewController: AbortController | undefined;
|
|
104
|
+
|
|
105
|
+
const post = (message: KnowledgeWorkerInput) => worker?.postMessage(message);
|
|
106
|
+
|
|
107
|
+
function cancelReview(reason: string): void {
|
|
108
|
+
reviewController?.abort();
|
|
109
|
+
reviewController = undefined;
|
|
110
|
+
if (pendingReview) {
|
|
111
|
+
post({ type: "review_result", requestId: pendingReview.requestId, error: reason });
|
|
112
|
+
pendingReview = undefined;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function runReview(requestId: string): Promise<void> {
|
|
117
|
+
const ctx = activeContext;
|
|
118
|
+
if (!ctx || !ctx.isIdle() || ctx.hasPendingMessages()) {
|
|
119
|
+
post({ type: "review_result", requestId, error: "session-is-busy" });
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const delta = buildConversationDelta(ctx, reviewState.lastReviewedEntryId);
|
|
123
|
+
if (!delta.text || !delta.lastEntryId) {
|
|
124
|
+
post({ type: "review_result", requestId, raw: '{"candidates":[]}' });
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
pendingReview = { requestId, lastEntryId: delta.lastEntryId, deltaDigest: knowledgeDeltaDigest(delta.text) };
|
|
128
|
+
if (!ctx.model || !ctx.modelRegistry.hasConfiguredAuth(ctx.model)) {
|
|
129
|
+
post({ type: "review_result", requestId, error: "no-configured-model" });
|
|
130
|
+
pendingReview = undefined;
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const controller = new AbortController();
|
|
134
|
+
reviewController = controller;
|
|
135
|
+
try {
|
|
136
|
+
const response = await ctx.modelRegistry.complete(
|
|
137
|
+
ctx.model,
|
|
138
|
+
{
|
|
139
|
+
systemPrompt: KNOWLEDGE_EXTRACTION_SYSTEM_PROMPT,
|
|
140
|
+
messages: [{
|
|
141
|
+
role: "user",
|
|
142
|
+
content: [{ type: "text", text: buildKnowledgeExtractionPrompt(activeProjectRoot, delta.text) }],
|
|
143
|
+
timestamp: Date.now(),
|
|
144
|
+
}],
|
|
145
|
+
},
|
|
146
|
+
{ signal: controller.signal, reasoningEffort: "low", cacheRetention: "none", sessionId: randomUUID() },
|
|
147
|
+
);
|
|
148
|
+
if (controller.signal.aborted) return;
|
|
149
|
+
const raw = response.content.filter((item): item is { type: "text"; text: string } => item.type === "text")
|
|
150
|
+
.map((item) => item.text).join("\n");
|
|
151
|
+
post({ type: "review_result", requestId, raw });
|
|
152
|
+
} catch (error) {
|
|
153
|
+
if (!controller.signal.aborted) {
|
|
154
|
+
post({ type: "review_result", requestId, error: error instanceof Error ? error.message : String(error) });
|
|
155
|
+
pendingReview = undefined;
|
|
156
|
+
}
|
|
157
|
+
} finally {
|
|
158
|
+
if (reviewController === controller) reviewController = undefined;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function handleWorkerMessage(message: KnowledgeWorkerOutput): void {
|
|
163
|
+
if (message.type === "review_due") {
|
|
164
|
+
void runReview(message.requestId);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (message.type === "review_saved" && pendingReview?.requestId === message.requestId) {
|
|
168
|
+
const result = message.result.saved + message.result.updated > 0 ? "saved" : "noop";
|
|
169
|
+
reviewState = {
|
|
170
|
+
version: 1,
|
|
171
|
+
lastReviewedEntryId: pendingReview.lastEntryId,
|
|
172
|
+
lastReviewedAt: new Date().toISOString(),
|
|
173
|
+
lastDeltaDigest: pendingReview.deltaDigest,
|
|
174
|
+
lastResult: result,
|
|
175
|
+
};
|
|
176
|
+
pi.appendEntry<KnowledgeReviewState>(KNOWLEDGE_REVIEW_STATE_TYPE, reviewState);
|
|
177
|
+
snapshot = loadKnowledgeSnapshot(activeProjectKey);
|
|
178
|
+
pendingReview = undefined;
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (message.type === "review_failed" && pendingReview?.requestId === message.requestId) {
|
|
182
|
+
pendingReview = undefined;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function startWorker(ctx: ExtensionContext): void {
|
|
187
|
+
cancelReview("session-replaced");
|
|
188
|
+
if (worker) {
|
|
189
|
+
post({ type: "stop" });
|
|
190
|
+
void worker.terminate();
|
|
191
|
+
}
|
|
192
|
+
activeContext = ctx;
|
|
193
|
+
activeProjectRoot = getWorkingDirectory(ctx.sessionManager);
|
|
194
|
+
activeProjectKey = projectKnowledgeKey(activeProjectRoot);
|
|
195
|
+
reviewState = loadReviewState(ctx);
|
|
196
|
+
snapshot = loadKnowledgeSnapshot(activeProjectKey);
|
|
197
|
+
const startedWorker = new Worker(new URL("../lib/knowledge/review-worker.ts", import.meta.url));
|
|
198
|
+
worker = startedWorker;
|
|
199
|
+
startedWorker.on("message", (message: KnowledgeWorkerOutput) => handleWorkerMessage(message));
|
|
200
|
+
startedWorker.on("error", () => { if (worker === startedWorker) worker = undefined; });
|
|
201
|
+
const latest = lastReviewableEntryId(ctx);
|
|
202
|
+
post({
|
|
203
|
+
type: "configure",
|
|
204
|
+
sessionId: ctx.sessionManager.getSessionId(),
|
|
205
|
+
projectKey: activeProjectKey,
|
|
206
|
+
dirty: Boolean(latest && latest !== reviewState.lastReviewedEntryId),
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
pi.registerTool(defineTool({
|
|
211
|
+
name: "hwcode_knowledge_lookup",
|
|
212
|
+
label: "HWCode Knowledge Lookup",
|
|
213
|
+
description: "Search the shared HWCode knowledge catalog and load a detailed topic by ID.",
|
|
214
|
+
promptSnippet: "Use the compact knowledge index, then load a detailed topic only when it is relevant.",
|
|
215
|
+
parameters: Type.Object({
|
|
216
|
+
id: Type.Optional(Type.String({ description: "Canonical topic ID from the loaded HWCode knowledge index" })),
|
|
217
|
+
query: Type.Optional(Type.String({ description: "Keywords to search in the complete local knowledge catalog" })),
|
|
218
|
+
}),
|
|
219
|
+
executionMode: "sequential",
|
|
220
|
+
async execute(_id, params) {
|
|
221
|
+
if (params.id) {
|
|
222
|
+
const found = loadKnowledgeById(params.id, activeProjectKey);
|
|
223
|
+
if (!found) return { content: [{ type: "text", text: `No applicable knowledge found with ID: ${params.id}` }], isError: true, details: {} };
|
|
224
|
+
return { content: [{ type: "text", text: found.content }], details: {} };
|
|
225
|
+
}
|
|
226
|
+
if (params.query) {
|
|
227
|
+
const matches = matchKnowledge(params.query, snapshot.catalog)
|
|
228
|
+
.filter((item) => item.scope === "global" || item.scope === `project:${activeProjectKey}`);
|
|
229
|
+
if (matches.length === 0) return { content: [{ type: "text", text: `No relevant knowledge found for: ${params.query}` }], details: {} };
|
|
230
|
+
return {
|
|
231
|
+
content: [{ type: "text", text: matches.map((item) => `- [${item.id}] ${item.title}: ${item.summary}`).join("\n") }],
|
|
232
|
+
details: {},
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
return { content: [{ type: "text", text: "Provide either id or query." }], isError: true, details: {} };
|
|
236
|
+
},
|
|
237
|
+
}));
|
|
238
|
+
|
|
239
|
+
pi.on("session_start", (_event, ctx) => startWorker(ctx));
|
|
240
|
+
|
|
241
|
+
pi.on("before_agent_start", (event, ctx) => {
|
|
242
|
+
activeContext = ctx;
|
|
243
|
+
const sections = [
|
|
244
|
+
snapshot.rulesPrompt ? `<hwcode_rules>\n${snapshot.rulesPrompt}\n</hwcode_rules>` : "",
|
|
245
|
+
snapshot.memoryPrompt ? `<hwcode_knowledge_index>\n${snapshot.memoryPrompt}\n</hwcode_knowledge_index>` : "",
|
|
246
|
+
].filter(Boolean);
|
|
247
|
+
if (sections.length === 0) return undefined;
|
|
248
|
+
return { systemPrompt: `${event.systemPrompt}\n\n${sections.join("\n\n")}` };
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
pi.on("input", (_event, ctx) => {
|
|
252
|
+
activeContext = ctx;
|
|
253
|
+
cancelReview("new-user-input");
|
|
254
|
+
post({ type: "activity", state: "busy", dirty: true });
|
|
255
|
+
return undefined;
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
pi.on("agent_start", (_event, ctx) => {
|
|
259
|
+
activeContext = ctx;
|
|
260
|
+
cancelReview("agent-started");
|
|
261
|
+
post({ type: "activity", state: "busy", dirty: true });
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
pi.on("agent_settled", (_event, ctx) => {
|
|
265
|
+
activeContext = ctx;
|
|
266
|
+
const latest = lastReviewableEntryId(ctx);
|
|
267
|
+
post({ type: "activity", state: "settled", dirty: Boolean(latest && latest !== reviewState.lastReviewedEntryId) });
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
pi.on("session_shutdown", async () => {
|
|
271
|
+
cancelReview("session-shutdown");
|
|
272
|
+
if (worker) {
|
|
273
|
+
post({ type: "stop" });
|
|
274
|
+
await worker.terminate();
|
|
275
|
+
}
|
|
276
|
+
worker = undefined;
|
|
277
|
+
activeContext = undefined;
|
|
278
|
+
});
|
|
279
|
+
}
|
|
@@ -4,29 +4,29 @@ import { resolve } from "node:path";
|
|
|
4
4
|
|
|
5
5
|
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
|
|
7
|
+
import { notify } from "../../../lib/extension-ui.ts";
|
|
7
8
|
import { modelConfigurationIssue } from "../../../lib/models/readiness.ts";
|
|
8
9
|
import { CLOUD_RUNTIME_DEFAULTS } from "../../../lib/runtime/defaults.ts";
|
|
9
10
|
import { remoteRunnerPaths, userRuntimePaths } from "../../../lib/runtime/paths.ts";
|
|
10
11
|
import { canonicalizeWorkspaceRoot } from "../../../lib/workflow-guard.ts";
|
|
11
12
|
import { getWorkingDirectory } from "../../../lib/working-directory.ts";
|
|
12
13
|
import { checkProviderCli, formatValidationFailure, validateCloudCredentials } from "../../../lib/workflows/cloud/adapters.ts";
|
|
13
|
-
import {
|
|
14
|
+
import { cloudTerraformTemplateSource, listCloudTerraformTemplates, materializeCloudTerraformTemplate, type CloudTerraformTemplate } from "../../../lib/workflows/cloud/bundles.ts";
|
|
14
15
|
import { CLOUD_PROVIDERS, getCloudProvider, inaccessibleCloudCliMessage, missingCloudCliMessage, type CloudCredentials, type CloudVendorId } from "../../../lib/workflows/cloud/providers.ts";
|
|
15
16
|
import { connectRemoteTarget } from "../../../lib/workflows/cloud/remote/connect.ts";
|
|
16
17
|
import { remoteTargetLabel, remoteTargetSummary, type RemoteTargetProfile } from "../../../lib/workflows/cloud/remote/profiles.ts";
|
|
17
|
-
import { cloudPromptTemplateSource, expandCloudPromptTemplate, listCloudPromptTemplates, type CloudPromptTemplate } from "../../../lib/workflows/cloud/templates.ts";
|
|
18
18
|
import {
|
|
19
19
|
cloudCredentialProfileLabel, defaultCloudVaultPath, listCloudCredentialProfiles,
|
|
20
20
|
listRemoteTargetProfiles, saveCloudCredentialProfile, saveRemoteTargetProfile,
|
|
21
21
|
writeCloudVault, type CloudCredentialProfile,
|
|
22
22
|
} from "../../../lib/workflows/cloud/vault.ts";
|
|
23
|
-
import { createCloudRunWorkspace } from "../../../lib/workflows/cloud/workspace.ts";
|
|
24
|
-
import { activeWorkflow, createCloudWorkflowState, type CloudWorkflowDetails, type WorkflowState } from "../../../lib/workflows/state.ts";
|
|
25
|
-
import type { CloudExtensionRuntime } from "./runtime.ts";
|
|
23
|
+
import { cloudArtifactDirectory, createCloudRunWorkspace } from "../../../lib/workflows/cloud/workspace.ts";
|
|
26
24
|
import {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
} from "
|
|
25
|
+
activeWorkflow, cloudDetails, createCloudWorkflowState,
|
|
26
|
+
type CloudWorkflowDetails, type WorkflowState,
|
|
27
|
+
} from "../../../lib/workflows/state.ts";
|
|
28
|
+
import { collectCredentials, remoteTrustInteraction, unlockVault } from "./interactions.ts";
|
|
29
|
+
import type { CloudExtensionRuntime } from "./runtime.ts";
|
|
30
30
|
|
|
31
31
|
type RunnerSelection =
|
|
32
32
|
| { kind: "cancel" }
|
|
@@ -34,6 +34,34 @@ type RunnerSelection =
|
|
|
34
34
|
| { kind: "deferred" }
|
|
35
35
|
| { kind: "profile"; profile: RemoteTargetProfile };
|
|
36
36
|
|
|
37
|
+
function formatTemplateTime(value: string): string {
|
|
38
|
+
const date = new Date(value);
|
|
39
|
+
if (Number.isNaN(date.valueOf())) return value;
|
|
40
|
+
const pad = (part: number) => String(part).padStart(2, "0");
|
|
41
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function activationPrompt(state: WorkflowState): string {
|
|
45
|
+
const details = cloudDetails(state)!;
|
|
46
|
+
const provider = getCloudProvider(details.vendor);
|
|
47
|
+
const guidance = details.templateGuidance
|
|
48
|
+
? `\n\nReusable template guidance (${details.sourceTemplate?.name ?? "saved template"}):\n${details.templateGuidance}`
|
|
49
|
+
: "";
|
|
50
|
+
const runner = details.runner?.name
|
|
51
|
+
?? (details.runnerPreference === "automatic" ? "automatic temporary Runner requested" : "not selected");
|
|
52
|
+
return `/skill:hwcode-cloud HWCode Cloud workflow activated.
|
|
53
|
+
|
|
54
|
+
Locked project root: ${state.root}
|
|
55
|
+
Task artifact workspace: ${cloudArtifactDirectory(state)}
|
|
56
|
+
Cloud provider: ${getCloudProvider(details.vendor).label}
|
|
57
|
+
Terraform Runner: ${runner}(Note: Runner is only for Terraform; regular discovery and CLI queries run locally via ${provider.cli})
|
|
58
|
+
Deploy current project: ${details.deployCurrentProject ? "yes" : "no"}
|
|
59
|
+
User objective:\n${details.request}${guidance}
|
|
60
|
+
|
|
61
|
+
Note: If the cloud CLI is unavailable on this machine, you may use Python scripts, Node.js SDKs, or curl to invoke the cloud provider's REST APIs directly. Otherwise, prefer hwcode_cloud_exec.
|
|
62
|
+
`;
|
|
63
|
+
}
|
|
64
|
+
|
|
37
65
|
async function chooseRunner(ctx: ExtensionCommandContext, payload: ReturnType<typeof import("../../../lib/workflows/cloud/vault.ts").createEmptyVault>, vendor: CloudVendorId, root: string, region: string): Promise<RunnerSelection> {
|
|
38
66
|
const existing = listRemoteTargetProfiles(payload).filter((profile) => profile.vendor === vendor);
|
|
39
67
|
const labels = existing.map(remoteTargetLabel);
|
|
@@ -81,25 +109,19 @@ async function ensureConversationModel(ctx: ExtensionCommandContext): Promise<bo
|
|
|
81
109
|
return false;
|
|
82
110
|
}
|
|
83
111
|
|
|
84
|
-
export async function
|
|
85
|
-
const templates =
|
|
86
|
-
if (templates.length === 0) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
return index >= 0 ? templates[index] : undefined;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export async function chooseDeploymentTemplate(ctx: ExtensionCommandContext): Promise<CloudDeploymentTemplate | undefined> {
|
|
94
|
-
const templates = listCloudDeploymentTemplates();
|
|
95
|
-
if (templates.length === 0) { notify(ctx, "还没有 Terraform Deployment Template。成功 apply 后使用 /hwcode-cloud-save-template 保存。", "warning"); return undefined; }
|
|
112
|
+
export async function chooseTerraformTemplate(ctx: ExtensionCommandContext): Promise<CloudTerraformTemplate | undefined> {
|
|
113
|
+
const templates = listCloudTerraformTemplates();
|
|
114
|
+
if (templates.length === 0) {
|
|
115
|
+
notify(ctx, "还没有 Terraform Template。首次成功完成 managed apply 后会自动保存。", "warning");
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
96
118
|
const labels = templates.map((template) => `${template.manifest.name} · ${template.manifest.vendor} · ${formatTemplateTime(template.manifest.updatedAt)}`);
|
|
97
|
-
const selected = await ctx.ui.select("选择 Terraform
|
|
119
|
+
const selected = await ctx.ui.select("选择 Terraform Template", labels);
|
|
98
120
|
const index = selected ? labels.indexOf(selected) : -1;
|
|
99
121
|
return index >= 0 ? templates[index] : undefined;
|
|
100
122
|
}
|
|
101
123
|
|
|
102
|
-
export async function activateCloudWorkflow(runtime: CloudExtensionRuntime, args: string, ctx: ExtensionCommandContext,
|
|
124
|
+
export async function activateCloudWorkflow(runtime: CloudExtensionRuntime, args: string, ctx: ExtensionCommandContext, initialBundle?: CloudTerraformTemplate): Promise<void> {
|
|
103
125
|
if (ctx.mode !== "tui") { notify(ctx, "HWCode Cloud 要求在交互式 TUI 中启动,以安全遮罩凭据输入。", "error"); return; }
|
|
104
126
|
if (!ctx.isIdle()) { notify(ctx, "请等待当前响应完成后再启动 HWCode Cloud。", "warning"); return; }
|
|
105
127
|
if (!(await ensureConversationModel(ctx))) return;
|
|
@@ -110,7 +132,7 @@ export async function activateCloudWorkflow(runtime: CloudExtensionRuntime, args
|
|
|
110
132
|
notify(ctx, `当前会话已有 ${currentWorkflow.mode} workflow。请新建 session 后再启动 HWCode Cloud。`, "warning");
|
|
111
133
|
return;
|
|
112
134
|
}
|
|
113
|
-
const storedState =
|
|
135
|
+
const storedState = runtime.restore(ctx);
|
|
114
136
|
let resumedState: WorkflowState | undefined;
|
|
115
137
|
if (storedState && storedState.root === root && !cloudDetails(storedState)?.terminalFailure) {
|
|
116
138
|
const storedDetails = cloudDetails(storedState)!;
|
|
@@ -122,14 +144,11 @@ export async function activateCloudWorkflow(runtime: CloudExtensionRuntime, args
|
|
|
122
144
|
if (choice.startsWith("恢复 ")) resumedState = storedState;
|
|
123
145
|
}
|
|
124
146
|
|
|
125
|
-
let
|
|
126
|
-
if (!resumedState && !
|
|
127
|
-
const source = await ctx.ui.select("Cloud 任务来源", ["新建任务", "
|
|
128
|
-
.filter((item) => item !== "使用已有 Prompt Template" || listCloudPromptTemplates().length > 0)
|
|
129
|
-
.filter((item) => item !== "使用 Terraform Deployment Template" || listCloudDeploymentTemplates().length > 0));
|
|
147
|
+
let bundle = initialBundle;
|
|
148
|
+
if (!resumedState && !bundle && !args.trim() && listCloudTerraformTemplates().length > 0) {
|
|
149
|
+
const source = await ctx.ui.select("Cloud 任务来源", ["新建任务", "使用 Terraform Template", "取消"]);
|
|
130
150
|
if (!source || source === "取消") return;
|
|
131
|
-
if (source === "
|
|
132
|
-
if (source === "使用 Terraform Deployment Template") { initialBundle = await chooseDeploymentTemplate(ctx); if (!initialBundle) return; }
|
|
151
|
+
if (source === "使用 Terraform Template") { bundle = await chooseTerraformTemplate(ctx); if (!bundle) return; }
|
|
133
152
|
}
|
|
134
153
|
|
|
135
154
|
let vendor: CloudVendorId;
|
|
@@ -139,16 +158,11 @@ export async function activateCloudWorkflow(runtime: CloudExtensionRuntime, args
|
|
|
139
158
|
if (resumedState) {
|
|
140
159
|
const details = cloudDetails(resumedState)!;
|
|
141
160
|
({ vendor, deployCurrentProject, request } = details);
|
|
142
|
-
} else if (
|
|
143
|
-
vendor =
|
|
161
|
+
} else if (bundle) {
|
|
162
|
+
vendor = bundle.manifest.vendor;
|
|
144
163
|
deployCurrentProject = true;
|
|
145
|
-
request = `复用 Terraform
|
|
146
|
-
templateGuidance = existsSync(
|
|
147
|
-
} else if (template) {
|
|
148
|
-
vendor = template.vendor;
|
|
149
|
-
deployCurrentProject = template.deployCurrentProject;
|
|
150
|
-
request = template.objective || template.name;
|
|
151
|
-
templateGuidance = expandCloudPromptTemplate(template, args);
|
|
164
|
+
request = `复用 Terraform Template「${bundle.manifest.name}」并完成其验证、计划与部署`;
|
|
165
|
+
templateGuidance = existsSync(bundle.summaryPath) ? readFileSync(bundle.summaryPath, "utf8") : `sourceDigest=${bundle.manifest.sourceDigest}`;
|
|
152
166
|
} else {
|
|
153
167
|
const label = await ctx.ui.select("选择需要对接的云计算厂商", CLOUD_PROVIDERS.map((provider) => provider.label));
|
|
154
168
|
if (!label) return;
|
|
@@ -237,16 +251,16 @@ export async function activateCloudWorkflow(runtime: CloudExtensionRuntime, args
|
|
|
237
251
|
runtime.cleanupCredentialDirectories();
|
|
238
252
|
runtime.activeCredentials = credentials;
|
|
239
253
|
const artifactWorkspace = resumedState ? undefined : createCloudRunWorkspace(root);
|
|
240
|
-
const materializedBundle =
|
|
254
|
+
const materializedBundle = bundle && artifactWorkspace ? materializeCloudTerraformTemplate(bundle, artifactWorkspace.path) : undefined;
|
|
241
255
|
runtime.activeState = resumedState ?? createCloudWorkflowState(
|
|
242
256
|
root, vendor, deployCurrentProject, request,
|
|
243
|
-
|
|
244
|
-
source:
|
|
257
|
+
bundle && templateGuidance ? {
|
|
258
|
+
source: cloudTerraformTemplateSource(bundle),
|
|
245
259
|
guidance: templateGuidance,
|
|
246
260
|
} : undefined,
|
|
247
261
|
artifactWorkspace?.path,
|
|
248
262
|
);
|
|
249
|
-
if (
|
|
263
|
+
if (bundle && !resumedState) runtime.activeState = runtime.replaceDetails(runtime.activeState, { ...cloudDetails(runtime.activeState)!, terraformSourcePath: materializedBundle!.terraformPath }, runtime.activeState.phase);
|
|
250
264
|
if (runtime.activeRunner) runtime.activeState = runtime.replaceDetails(runtime.activeState, { ...cloudDetails(runtime.activeState)!, runnerPreference, runner: remoteTargetSummary(runtime.activeRunner) }, runtime.activeState.phase);
|
|
251
265
|
else runtime.activeState = runtime.replaceDetails(runtime.activeState, { ...cloudDetails(runtime.activeState)!, runnerPreference }, runtime.activeState.phase);
|
|
252
266
|
notify(ctx, `${vendorLabel} 连接成功。凭据已加密保存,项目根目录锁定为 ${root}。`);
|
|
@@ -1,96 +1,11 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { redactCredentialValues } from "../../../lib/workflows/cloud/providers.ts";
|
|
5
|
-
import { saveCloudWorkflowTemplate } from "../../../lib/workflows/cloud/template-save.ts";
|
|
6
|
-
import { listCloudPromptTemplates, saveCloudPromptTemplate, updateCloudPromptTemplate } from "../../../lib/workflows/cloud/templates.ts";
|
|
7
|
-
import { activateCloudWorkflow, chooseDeploymentTemplate, choosePromptTemplate } from "./activation.ts";
|
|
3
|
+
import { activateCloudWorkflow } from "./activation.ts";
|
|
8
4
|
import type { CloudExtensionRuntime } from "./runtime.ts";
|
|
9
|
-
import { cloudDetails, formatTemplateTime, notify } from "./shared.ts";
|
|
10
5
|
|
|
11
6
|
export function registerCloudCommands(pi: ExtensionAPI, runtime: CloudExtensionRuntime): void {
|
|
12
7
|
pi.registerCommand("hwcode-cloud", {
|
|
13
8
|
description: "Start or resume the guarded HWCode cloud workflow",
|
|
14
9
|
handler: (args, ctx) => activateCloudWorkflow(runtime, args, ctx),
|
|
15
10
|
});
|
|
16
|
-
|
|
17
|
-
pi.registerCommand("hwcode-cloud-template", {
|
|
18
|
-
description: "Start HWCode Cloud from a locally saved prompt or Terraform deployment template",
|
|
19
|
-
handler: async (args, ctx) => {
|
|
20
|
-
const promptTemplates = listCloudPromptTemplates();
|
|
21
|
-
const deploymentTemplates = listCloudDeploymentTemplates();
|
|
22
|
-
const source = await ctx.ui.select("选择本地 Cloud Template 类型", [
|
|
23
|
-
...(promptTemplates.length > 0 ? ["Prompt Template"] : []),
|
|
24
|
-
...(deploymentTemplates.length > 0 ? ["Terraform Deployment Template"] : []),
|
|
25
|
-
"取消",
|
|
26
|
-
]);
|
|
27
|
-
if (source === "Prompt Template") {
|
|
28
|
-
const template = await choosePromptTemplate(ctx);
|
|
29
|
-
if (template) await activateCloudWorkflow(runtime, args, ctx, template);
|
|
30
|
-
} else if (source === "Terraform Deployment Template") {
|
|
31
|
-
const bundle = await chooseDeploymentTemplate(ctx);
|
|
32
|
-
if (bundle) await activateCloudWorkflow(runtime, args, ctx, undefined, bundle);
|
|
33
|
-
}
|
|
34
|
-
},
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
pi.registerCommand("hwcode-cloud-save-template", {
|
|
38
|
-
description: "Save or update the successful Cloud path as a Prompt or Terraform Deployment Template",
|
|
39
|
-
handler: async (args, ctx) => {
|
|
40
|
-
const activeState = runtime.restore(ctx);
|
|
41
|
-
const details = activeState && cloudDetails(activeState);
|
|
42
|
-
if (!activeState || !details || (details.successfulSteps.length === 0 && details.terraformRun?.phase !== "applied")) {
|
|
43
|
-
notify(ctx, "当前 Cloud workflow 还没有成功执行步骤,无法生成可复用模板。", "warning");
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
if (details.terraformRun?.phase === "applied" && details.terraformRun.sourcePath) {
|
|
47
|
-
const existing = details.sourceTemplate?.kind === "terraform"
|
|
48
|
-
? listCloudDeploymentTemplates().find((template) => template.manifest.id === details.sourceTemplate?.id)
|
|
49
|
-
: undefined;
|
|
50
|
-
let action: "update" | "create" = "create";
|
|
51
|
-
if (existing) {
|
|
52
|
-
const choice = await ctx.ui.select(`当前会话关联模板「${existing.manifest.name}」`, ["更新原模板", "创建新模板", "取消"]);
|
|
53
|
-
if (!choice || choice === "取消") return;
|
|
54
|
-
action = choice === "更新原模板" ? "update" : "create";
|
|
55
|
-
}
|
|
56
|
-
const name = (action === "update" ? existing!.manifest.name : args.trim() || await ctx.ui.input("Terraform Deployment Template 名称", `${details.vendor}-${new Date().toISOString().slice(0, 10)}`))?.trim();
|
|
57
|
-
if (!name) return;
|
|
58
|
-
try {
|
|
59
|
-
const template = action === "update"
|
|
60
|
-
? updateCloudDeploymentTemplate(existing!, details.terraformRun.sourcePath, activeState, new Date(), details.artifactDirectory)
|
|
61
|
-
: saveCloudDeploymentTemplate(name, details.terraformRun.sourcePath, activeState, undefined, new Date(), details.artifactDirectory);
|
|
62
|
-
runtime.replaceDetails(activeState, { ...details, sourceTemplate: cloudDeploymentTemplateSource(template) });
|
|
63
|
-
notify(ctx, `${action === "update" ? "已更新" : "已保存"} Terraform Deployment Template「${template.manifest.name}」,包含完整性清单、Terraform、Helm、脱敏 discovery/reports 与验证摘要。`);
|
|
64
|
-
} catch (error) {
|
|
65
|
-
notify(ctx, `Terraform Deployment Template 保存失败:${error instanceof Error ? error.message : String(error)}`, "error");
|
|
66
|
-
}
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const result = await saveCloudWorkflowTemplate({
|
|
71
|
-
state: activeState,
|
|
72
|
-
requestedName: args,
|
|
73
|
-
redact: runtime.activeCredentials ? (value) => redactCredentialValues(value, runtime.activeCredentials!) : undefined,
|
|
74
|
-
interaction: {
|
|
75
|
-
async chooseSourceAction(source) {
|
|
76
|
-
const updateLabel = `更新原模板「${source.name}」`;
|
|
77
|
-
const choice = await ctx.ui.select(`当前会话关联模板「${details.sourceTemplate?.name ?? source.name}」`, [updateLabel, "创建新模板", "取消"]);
|
|
78
|
-
if (!choice || choice === "取消") return "cancel";
|
|
79
|
-
return choice === updateLabel ? "update" : "create";
|
|
80
|
-
},
|
|
81
|
-
inputName: (defaultName) => ctx.ui.input("模板名称", defaultName),
|
|
82
|
-
editNotes: (initialNotes) => ctx.ui.editor("补充必要前置条件、成功经验及必须避免的高消耗错误路径(可留空;不得包含凭据)", initialNotes),
|
|
83
|
-
sourceMissing(sourceName) { notify(ctx, `本次使用的原模板「${sourceName}」已不存在,将创建新模板。`, "warning"); },
|
|
84
|
-
},
|
|
85
|
-
store: {
|
|
86
|
-
list: () => listCloudPromptTemplates(),
|
|
87
|
-
create: (name, state, notes) => saveCloudPromptTemplate(name, state, undefined, notes),
|
|
88
|
-
update: (template, state, notes) => updateCloudPromptTemplate(template, state, notes),
|
|
89
|
-
},
|
|
90
|
-
});
|
|
91
|
-
if (result.status === "cancelled") return;
|
|
92
|
-
runtime.replaceDetails(activeState, result.details);
|
|
93
|
-
notify(ctx, `${result.action === "updated" ? "已更新" : "已创建"}模板「${result.template.name}」(${formatTemplateTime(result.template.updatedAt)})。使用 /hwcode-cloud-template 可立即复用。`);
|
|
94
|
-
},
|
|
95
|
-
});
|
|
96
11
|
}
|
|
@@ -1,20 +1,15 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
|
|
3
|
+
import { notify } from "../../../lib/extension-ui.ts";
|
|
3
4
|
import { CLOUD_RUNTIME_DEFAULTS } from "../../../lib/runtime/defaults.ts";
|
|
4
|
-
import {
|
|
5
|
+
import { getCloudProvider } from "../../../lib/workflows/cloud/providers.ts";
|
|
6
|
+
import { cloudArtifactDirectory } from "../../../lib/workflows/cloud/workspace.ts";
|
|
7
|
+
import { cloudDetails } from "../../../lib/workflows/state.ts";
|
|
5
8
|
import type { CloudExtensionRuntime } from "./runtime.ts";
|
|
6
|
-
import {
|
|
7
|
-
appendWorkflowState, cloudArtifactDirectory, cloudDetails, notify,
|
|
8
|
-
restoreLegacyCloudWorkflow,
|
|
9
|
-
} from "./shared.ts";
|
|
10
9
|
|
|
11
10
|
export function registerCloudEvents(pi: ExtensionAPI, runtime: CloudExtensionRuntime): void {
|
|
12
11
|
pi.on("session_start", async (_event, ctx) => {
|
|
13
12
|
runtime.restore(ctx);
|
|
14
|
-
if (!runtime.activeState) {
|
|
15
|
-
runtime.activeState = restoreLegacyCloudWorkflow(ctx);
|
|
16
|
-
if (runtime.activeState) appendWorkflowState(pi, runtime.activeState);
|
|
17
|
-
}
|
|
18
13
|
runtime.clearSecrets();
|
|
19
14
|
if (!runtime.activeState) return;
|
|
20
15
|
const details = cloudDetails(runtime.activeState)!;
|
|
@@ -29,20 +24,22 @@ export function registerCloudEvents(pi: ExtensionAPI, runtime: CloudExtensionRun
|
|
|
29
24
|
? "Three distinct approaches already failed. Only summarize causes, progress, and changes; do not continue execution."
|
|
30
25
|
: `Distinct failed approaches: ${details.failedApproaches.length}/${CLOUD_RUNTIME_DEFAULTS.workflow.maxFailedApproaches}. After the third distinct approach fails, stop and summarize.`;
|
|
31
26
|
return {
|
|
32
|
-
systemPrompt: `${event.systemPrompt}\n\nHWCODE CLOUD ACTIVE\nProvider: ${details.vendor}
|
|
27
|
+
systemPrompt: `${event.systemPrompt}\n\nHWCODE CLOUD ACTIVE\nProvider: ${details.vendor}
|
|
28
|
+
Objective: ${details.request}\nArtifact workspace: ${cloudArtifactDirectory(activeState)}
|
|
29
|
+
Credentials must never be requested in chat, printed, placed in tool arguments, or read from the vault. \
|
|
30
|
+
Write all generated Cloud artifacts only under the artifact workspace: discovery/ for CLI skeletons and \
|
|
31
|
+
snapshots, terraform/ for IaC, charts/ for Helm sources/packages, reports/ for plans and summaries. \
|
|
32
|
+
Project source remains under ${activeState.root}; when a provider CLI needs a project source file, \
|
|
33
|
+
pass its absolute project-root path. Use hwcode_cloud_exec for provider CLI operations. \
|
|
34
|
+
${details.runner ? "Use hwcode_runner_prepare and hwcode_terraform_* for Terraform source \
|
|
35
|
+
synchronization, validation, planning, approval, and apply on the selected Runner." : details.runnerPreference === "automatic" ? "The user requested an automatic temporary Runner. \
|
|
36
|
+
Provision it through approved provider CLI changes with workload identity and cloud-init, \
|
|
37
|
+
then register the discovered endpoint with hwcode_runner_connect." : "No Terraform Runner is selected. \
|
|
38
|
+
Do not provision one unless the user explicitly changes this choice."} Resource creates and changes require approval unless the user opted out; deletion always requires approval. ${failureRule}`
|
|
33
39
|
+ `\nConcurrency rule: Batch all independent read-only discovery calls concurrently in a single response turn. For resource creation, group independent operations into parallel tool calls while keeping dependent operations strictly sequential.`,
|
|
34
40
|
};
|
|
35
41
|
});
|
|
36
42
|
|
|
37
|
-
pi.on("tool_call", async (event, ctx) => {
|
|
38
|
-
return undefined; // TODO: block direct cloud CLI calls during active workflow
|
|
39
|
-
// const activeState = runtime.restore(ctx);
|
|
40
|
-
// if (!activeState || event.toolName !== "bash") return undefined;
|
|
41
|
-
// const input = event.input as Record<string, unknown>;
|
|
42
|
-
// if (typeof input.command !== "string" || !containsCloudCommand(input.command)) return undefined;
|
|
43
|
-
// return { block: true, reason: "Direct cloud and infrastructure CLI use is blocked during HWCode Cloud. Use hwcode_cloud_exec so credentials remain isolated and approvals are enforced." };
|
|
44
|
-
});
|
|
45
|
-
|
|
46
43
|
pi.on("session_shutdown", async () => {
|
|
47
44
|
runtime.activeState = undefined;
|
|
48
45
|
runtime.clearSecrets();
|