@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
package/.pi/lib/runtime/paths.ts
CHANGED
|
@@ -5,6 +5,15 @@ export const HWCODE_DATA_DIRECTORY = ".hwcode";
|
|
|
5
5
|
export const PROJECT_SDD_SPECS_RELATIVE = `${HWCODE_DATA_DIRECTORY}/specs`;
|
|
6
6
|
export const PROJECT_CLOUD_RUNS_RELATIVE = `${HWCODE_DATA_DIRECTORY}/cloud/runs`;
|
|
7
7
|
|
|
8
|
+
// Canonical subpath layout under ~/.hwcode. Kept together so filesystem topology
|
|
9
|
+
// stays in one place instead of scattered across cloud/knowledge modules.
|
|
10
|
+
const CLOUD_SUBDIR = "cloud";
|
|
11
|
+
const CLOUD_VAULT_FILE = "credentials.enc";
|
|
12
|
+
const CLOUD_KNOWN_HOSTS_FILE = "known_hosts";
|
|
13
|
+
const CLOUD_TEMPLATES_SUBDIR = "templates";
|
|
14
|
+
const CLOUD_TERRAFORM_SUBDIR = "terraform";
|
|
15
|
+
const KNOWLEDGE_SUBDIR = "knowledge";
|
|
16
|
+
|
|
8
17
|
export interface ProjectRuntimePaths {
|
|
9
18
|
root: string;
|
|
10
19
|
hwcode: string;
|
|
@@ -18,14 +27,19 @@ export interface UserRuntimePaths {
|
|
|
18
27
|
cloud: string;
|
|
19
28
|
cloudVault: string;
|
|
20
29
|
cloudKnownHosts: string;
|
|
21
|
-
|
|
22
|
-
|
|
30
|
+
cloudTerraformTemplates: string;
|
|
31
|
+
knowledge: string;
|
|
32
|
+
knowledgeRules: string;
|
|
33
|
+
knowledgeTopics: string;
|
|
34
|
+
knowledgePending: string;
|
|
35
|
+
knowledgeCatalog: string;
|
|
36
|
+
knowledgeMemory: string;
|
|
23
37
|
}
|
|
24
38
|
|
|
25
39
|
export function projectRuntimePaths(projectRoot: string): ProjectRuntimePaths {
|
|
26
40
|
const root = resolve(projectRoot);
|
|
27
41
|
const hwcode = join(root, HWCODE_DATA_DIRECTORY);
|
|
28
|
-
const cloud = join(hwcode,
|
|
42
|
+
const cloud = join(hwcode, CLOUD_SUBDIR);
|
|
29
43
|
return {
|
|
30
44
|
root,
|
|
31
45
|
hwcode,
|
|
@@ -37,25 +51,23 @@ export function projectRuntimePaths(projectRoot: string): ProjectRuntimePaths {
|
|
|
37
51
|
|
|
38
52
|
export function userRuntimePaths(home = homedir()): UserRuntimePaths {
|
|
39
53
|
const root = join(home, HWCODE_DATA_DIRECTORY);
|
|
40
|
-
const cloud = join(root,
|
|
54
|
+
const cloud = join(root, CLOUD_SUBDIR);
|
|
55
|
+
const knowledge = join(root, KNOWLEDGE_SUBDIR);
|
|
41
56
|
return {
|
|
42
57
|
root,
|
|
43
58
|
cloud,
|
|
44
|
-
cloudVault: join(cloud,
|
|
45
|
-
cloudKnownHosts: join(cloud,
|
|
46
|
-
|
|
47
|
-
|
|
59
|
+
cloudVault: join(cloud, CLOUD_VAULT_FILE),
|
|
60
|
+
cloudKnownHosts: join(cloud, CLOUD_KNOWN_HOSTS_FILE),
|
|
61
|
+
cloudTerraformTemplates: join(cloud, CLOUD_TEMPLATES_SUBDIR, CLOUD_TERRAFORM_SUBDIR),
|
|
62
|
+
knowledge,
|
|
63
|
+
knowledgeRules: join(knowledge, "rules"),
|
|
64
|
+
knowledgeTopics: join(knowledge, "topics"),
|
|
65
|
+
knowledgePending: join(knowledge, "pending"),
|
|
66
|
+
knowledgeCatalog: join(knowledge, "catalog.json"),
|
|
67
|
+
knowledgeMemory: join(knowledge, "MEMORY.md"),
|
|
48
68
|
};
|
|
49
69
|
}
|
|
50
70
|
|
|
51
|
-
export function legacyUserCloudTemplatePaths(home = homedir()): {
|
|
52
|
-
prompts: string;
|
|
53
|
-
deployments: string;
|
|
54
|
-
} {
|
|
55
|
-
const cloud = join(home, HWCODE_DATA_DIRECTORY, "cloud");
|
|
56
|
-
return { prompts: join(cloud, "prompts"), deployments: join(cloud, "templates") };
|
|
57
|
-
}
|
|
58
|
-
|
|
59
71
|
export function remoteRunnerPaths(user: string): { root: string; runs: string } {
|
|
60
72
|
const root = posix.join("/home", user, HWCODE_DATA_DIRECTORY);
|
|
61
73
|
return { root, runs: posix.join(root, "runs") };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export function toolError(message: string, details: Record<string, unknown> = {}) {
|
|
2
|
+
return { content: [{ type: "text" as const, text: message }], isError: true, details };
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function toolOk(message: string, details: Record<string, unknown> = {}) {
|
|
6
|
+
return { content: [{ type: "text" as const, text: message }], details };
|
|
7
|
+
}
|
|
@@ -5,9 +5,8 @@ import { dirname, extname, isAbsolute, join, relative, sep } from "node:path";
|
|
|
5
5
|
|
|
6
6
|
import { isCloudVendorId, type CloudVendorId } from "./providers.ts";
|
|
7
7
|
import type { WorkflowState } from "../state.ts";
|
|
8
|
-
import {
|
|
8
|
+
import { userRuntimePaths } from "../../runtime/paths.ts";
|
|
9
9
|
import { CLOUD_RUNTIME_DEFAULTS } from "../../runtime/defaults.ts";
|
|
10
|
-
import { renderCloudPromptTemplate } from "./templates.ts";
|
|
11
10
|
|
|
12
11
|
export interface TerraformTemplateManifest {
|
|
13
12
|
schemaVersion: 2;
|
|
@@ -19,7 +18,6 @@ export interface TerraformTemplateManifest {
|
|
|
19
18
|
sourceDigest: string;
|
|
20
19
|
contentDigest: string;
|
|
21
20
|
metadataDigest?: string;
|
|
22
|
-
migratedFrom?: 1;
|
|
23
21
|
failedApproaches: Array<{ approach: string; reason: string }>;
|
|
24
22
|
artifacts: {
|
|
25
23
|
terraform: boolean;
|
|
@@ -31,7 +29,7 @@ export interface TerraformTemplateManifest {
|
|
|
31
29
|
};
|
|
32
30
|
}
|
|
33
31
|
|
|
34
|
-
export interface
|
|
32
|
+
export interface CloudTerraformTemplate {
|
|
35
33
|
manifest: TerraformTemplateManifest;
|
|
36
34
|
path: string;
|
|
37
35
|
terraformPath: string;
|
|
@@ -41,14 +39,32 @@ export interface CloudDeploymentTemplate {
|
|
|
41
39
|
verificationPath: string;
|
|
42
40
|
}
|
|
43
41
|
|
|
44
|
-
export function
|
|
45
|
-
id: string; name: string; createdAt: string; updatedAt: string;
|
|
42
|
+
export function cloudTerraformTemplateSource(template: CloudTerraformTemplate): {
|
|
43
|
+
id: string; name: string; createdAt: string; updatedAt: string;
|
|
46
44
|
} {
|
|
47
|
-
return { id: template.manifest.id, name: template.manifest.name, createdAt: template.manifest.createdAt, updatedAt: template.manifest.updatedAt
|
|
45
|
+
return { id: template.manifest.id, name: template.manifest.name, createdAt: template.manifest.createdAt, updatedAt: template.manifest.updatedAt };
|
|
48
46
|
}
|
|
49
47
|
|
|
50
48
|
export function defaultCloudBundleDirectory(home = homedir()): string {
|
|
51
|
-
return userRuntimePaths(home).
|
|
49
|
+
return userRuntimePaths(home).cloudTerraformTemplates;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function renderTerraformSummary(state: WorkflowState): string {
|
|
53
|
+
const details = state.details;
|
|
54
|
+
if (!details) return `# Terraform Deployment Summary\n\nProject root: ${state.root}\n`;
|
|
55
|
+
const lines: string[] = [
|
|
56
|
+
`# Terraform Deployment: ${details.vendor}`,
|
|
57
|
+
"",
|
|
58
|
+
`**Objective**: ${details.request}`,
|
|
59
|
+
`**Project Root**: ${state.root}`,
|
|
60
|
+
];
|
|
61
|
+
if (details.successfulSteps.length > 0) {
|
|
62
|
+
lines.push("\n## Validated Steps", ...details.successfulSteps.map((s) => `- ${s.command} ${s.args.join(" ")} (${s.intent})`));
|
|
63
|
+
}
|
|
64
|
+
if (details.failedApproaches.length > 0) {
|
|
65
|
+
lines.push("\n## Avoided Failed Approaches", ...details.failedApproaches.map((f) => `- ⚠️ ${f.approach}: ${f.reason}`));
|
|
66
|
+
}
|
|
67
|
+
return lines.join("\n");
|
|
52
68
|
}
|
|
53
69
|
|
|
54
70
|
function excluded(name: string): boolean {
|
|
@@ -237,27 +253,27 @@ function digestBundleContent(path: string): string {
|
|
|
237
253
|
return createHash("sha256").update(parts.join("\n")).digest("hex");
|
|
238
254
|
}
|
|
239
255
|
|
|
240
|
-
export function
|
|
256
|
+
export function saveCloudTerraformTemplate(
|
|
241
257
|
requestedName: string,
|
|
242
258
|
terraformSource: string,
|
|
243
259
|
state: WorkflowState,
|
|
244
260
|
directory = defaultCloudBundleDirectory(),
|
|
245
261
|
savedAt = new Date(),
|
|
246
262
|
artifactWorkspace?: string,
|
|
247
|
-
):
|
|
263
|
+
): CloudTerraformTemplate {
|
|
248
264
|
if (state.mode !== "cloud" || !state.details) throw new Error("Cloud workflow state is required");
|
|
249
265
|
if (!isAbsolute(terraformSource) || !existsSync(terraformSource) || !statSync(terraformSource).isDirectory()) throw new Error("Terraform source directory is required");
|
|
250
266
|
const name = requestedName.replace(/[\r\n]+/gu, " ").replace(/\s+/gu, " ").trim().slice(0, CLOUD_RUNTIME_DEFAULTS.templates.nameMaxLength);
|
|
251
|
-
if (!name) throw new Error("
|
|
267
|
+
if (!name) throw new Error("Terraform Template name is required");
|
|
252
268
|
const temporary = `${directory}/.${process.pid}.${randomUUID()}.tmp`;
|
|
253
269
|
const artifacts = writeBundleArtifacts(temporary, terraformSource, artifactWorkspace, state);
|
|
254
|
-
writeAtomic(join(temporary, "summary.md"), `${
|
|
270
|
+
writeAtomic(join(temporary, "summary.md"), `${renderTerraformSummary(state)}\n`);
|
|
255
271
|
const sourceDigest = digestDirectory(join(temporary, "terraform"));
|
|
256
272
|
const manifest = sealManifest(buildManifest(state, name, sourceDigest, savedAt.toISOString(), savedAt.toISOString(), artifacts, digestBundleContent(temporary)));
|
|
257
273
|
const path = join(directory, manifest.id);
|
|
258
274
|
if (existsSync(path)) {
|
|
259
275
|
rmSync(temporary, { recursive: true, force: true });
|
|
260
|
-
throw new Error(`
|
|
276
|
+
throw new Error(`Terraform Template already exists: ${manifest.id}`);
|
|
261
277
|
}
|
|
262
278
|
try {
|
|
263
279
|
writeAtomic(join(temporary, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
@@ -278,13 +294,13 @@ export function saveCloudDeploymentTemplate(
|
|
|
278
294
|
};
|
|
279
295
|
}
|
|
280
296
|
|
|
281
|
-
export function
|
|
282
|
-
template:
|
|
297
|
+
export function updateCloudTerraformTemplate(
|
|
298
|
+
template: CloudTerraformTemplate,
|
|
283
299
|
terraformSource: string,
|
|
284
300
|
state: WorkflowState,
|
|
285
301
|
savedAt = new Date(),
|
|
286
302
|
artifactWorkspace?: string,
|
|
287
|
-
):
|
|
303
|
+
): CloudTerraformTemplate {
|
|
288
304
|
if (state.mode !== "cloud" || !state.details) throw new Error("Cloud workflow state is required");
|
|
289
305
|
if (!isAbsolute(terraformSource) || !existsSync(terraformSource) || !statSync(terraformSource).isDirectory()) throw new Error("Terraform source directory is required");
|
|
290
306
|
const temporary = `${template.path}.${process.pid}.${randomUUID()}.tmp`;
|
|
@@ -292,7 +308,7 @@ export function updateCloudDeploymentTemplate(
|
|
|
292
308
|
let manifest: TerraformTemplateManifest;
|
|
293
309
|
try {
|
|
294
310
|
const artifacts = writeBundleArtifacts(temporary, terraformSource, artifactWorkspace, state);
|
|
295
|
-
writeAtomic(join(temporary, "summary.md"), `${
|
|
311
|
+
writeAtomic(join(temporary, "summary.md"), `${renderTerraformSummary(state)}\n`);
|
|
296
312
|
manifest = sealManifest({
|
|
297
313
|
...buildManifest(state, template.manifest.name, digestDirectory(join(temporary, "terraform")), template.manifest.createdAt, savedAt.toISOString(), artifacts, digestBundleContent(temporary)),
|
|
298
314
|
id: template.manifest.id,
|
|
@@ -309,46 +325,63 @@ export function updateCloudDeploymentTemplate(
|
|
|
309
325
|
return { manifest: manifest!, path: template.path, terraformPath: join(template.path, "terraform"), chartsPath: join(template.path, "charts"), discoveryPath: join(template.path, "discovery"), summaryPath: join(template.path, "summary.md"), verificationPath: join(template.path, "verification.md") };
|
|
310
326
|
}
|
|
311
327
|
|
|
312
|
-
export function
|
|
328
|
+
export function parseCloudTerraformTemplate(path: string): CloudTerraformTemplate | undefined {
|
|
313
329
|
const manifestPath = join(path, "manifest.json");
|
|
314
330
|
if (!existsSync(manifestPath)) return undefined;
|
|
315
331
|
const raw = JSON.parse(readFileSync(manifestPath, "utf8")) as Record<string, unknown>;
|
|
316
|
-
if (
|
|
332
|
+
if (raw.schemaVersion !== 2 || typeof raw.id !== "string" || typeof raw.name !== "string"
|
|
317
333
|
|| typeof raw.vendor !== "string" || !isCloudVendorId(raw.vendor)
|
|
318
334
|
|| typeof raw.createdAt !== "string" || typeof raw.updatedAt !== "string"
|
|
319
335
|
|| typeof raw.sourceDigest !== "string" || !existsSync(join(path, "terraform"))) return undefined;
|
|
320
|
-
|
|
321
|
-
let manifest = raw.schemaVersion === 2 ? raw as unknown as TerraformTemplateManifest : ({
|
|
322
|
-
...raw,
|
|
323
|
-
schemaVersion: 2,
|
|
324
|
-
migratedFrom: 1,
|
|
325
|
-
contentDigest: digestBundleContent(path),
|
|
326
|
-
artifacts: { ...oldArtifacts, reports: [], excluded: [] },
|
|
327
|
-
} as unknown as TerraformTemplateManifest);
|
|
336
|
+
let manifest = raw as unknown as TerraformTemplateManifest;
|
|
328
337
|
if (typeof manifest.contentDigest !== "string") return undefined;
|
|
329
338
|
if (manifest.metadataDigest && manifest.metadataDigest !== digestManifestMetadata(manifest)) return undefined;
|
|
330
339
|
if (!manifest.metadataDigest) manifest = sealManifest(manifest);
|
|
331
340
|
return { manifest, path, terraformPath: join(path, "terraform"), chartsPath: join(path, "charts"), discoveryPath: join(path, "discovery"), summaryPath: join(path, "summary.md"), verificationPath: join(path, "verification.md") };
|
|
332
341
|
}
|
|
333
342
|
|
|
334
|
-
export function
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
.
|
|
340
|
-
.
|
|
341
|
-
.filter((entry) => entry.isDirectory())
|
|
342
|
-
.map((entry) => parseCloudDeploymentTemplate(join(path, entry.name))))
|
|
343
|
-
.filter((template): template is CloudDeploymentTemplate => Boolean(template))
|
|
343
|
+
export function listCloudTerraformTemplates(directory?: string, home = homedir()): CloudTerraformTemplate[] {
|
|
344
|
+
const target = directory ?? defaultCloudBundleDirectory(home);
|
|
345
|
+
if (!existsSync(target)) return [];
|
|
346
|
+
return readdirSync(target, { withFileTypes: true })
|
|
347
|
+
.filter((entry) => entry.isDirectory())
|
|
348
|
+
.map((entry) => parseCloudTerraformTemplate(join(target, entry.name)))
|
|
349
|
+
.filter((template): template is CloudTerraformTemplate => Boolean(template))
|
|
344
350
|
.sort((left, right) => right.manifest.updatedAt.localeCompare(left.manifest.updatedAt) || left.manifest.name.localeCompare(right.manifest.name));
|
|
345
351
|
}
|
|
346
352
|
|
|
347
|
-
export function
|
|
353
|
+
export function persistAppliedTerraformArtifact(
|
|
354
|
+
state: WorkflowState,
|
|
355
|
+
terraformSource: string,
|
|
356
|
+
artifactWorkspace?: string,
|
|
357
|
+
savedAt = new Date(),
|
|
358
|
+
directory = defaultCloudBundleDirectory(),
|
|
359
|
+
): { action: "created" | "updated"; template: CloudTerraformTemplate } {
|
|
360
|
+
if (state.mode !== "cloud" || !state.details || state.details.terraformRun?.phase !== "applied") {
|
|
361
|
+
throw new Error("An applied Cloud Terraform run is required");
|
|
362
|
+
}
|
|
363
|
+
const source = state.details.sourceTemplate
|
|
364
|
+
? listCloudTerraformTemplates(directory).find((template) => template.manifest.id === state.details?.sourceTemplate?.id)
|
|
365
|
+
: undefined;
|
|
366
|
+
if (source) {
|
|
367
|
+
return {
|
|
368
|
+
action: "updated",
|
|
369
|
+
template: updateCloudTerraformTemplate(source, terraformSource, state, savedAt, artifactWorkspace),
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
const project = state.root.split("/").filter(Boolean).pop() || "project";
|
|
373
|
+
const name = `${state.details.vendor}-${project}-${savedAt.toISOString().replace(/[:.]/gu, "-")}`;
|
|
374
|
+
return {
|
|
375
|
+
action: "created",
|
|
376
|
+
template: saveCloudTerraformTemplate(name, terraformSource, state, directory, savedAt, artifactWorkspace),
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export function materializeCloudTerraformTemplate(template: CloudTerraformTemplate, workspace: string): { terraformPath: string } {
|
|
348
381
|
if (!isAbsolute(workspace)) throw new Error("Cloud artifact workspace must be absolute");
|
|
349
382
|
const actualDigest = digestBundleContent(template.path);
|
|
350
|
-
if (actualDigest !== template.manifest.contentDigest) throw new Error("
|
|
351
|
-
if (template.manifest.metadataDigest !== digestManifestMetadata(template.manifest)) throw new Error("
|
|
383
|
+
if (actualDigest !== template.manifest.contentDigest) throw new Error("Terraform Template content digest mismatch; reuse is blocked");
|
|
384
|
+
if (template.manifest.metadataDigest !== digestManifestMetadata(template.manifest)) throw new Error("Terraform Template metadata digest mismatch; reuse is blocked");
|
|
352
385
|
for (const name of ["terraform", "charts", "discovery", "reports"]) {
|
|
353
386
|
const source = join(template.path, name);
|
|
354
387
|
if (!existsSync(source)) continue;
|
|
@@ -3,6 +3,7 @@ import { randomUUID } from "node:crypto";
|
|
|
3
3
|
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
4
4
|
|
|
5
5
|
import { PROJECT_CLOUD_RUNS_RELATIVE, projectRuntimePaths } from "../../runtime/paths.ts";
|
|
6
|
+
import { cloudDetails, type WorkflowState } from "../state.ts";
|
|
6
7
|
|
|
7
8
|
export interface CloudRunWorkspace {
|
|
8
9
|
runId: string;
|
|
@@ -35,3 +36,8 @@ export function isCloudRunWorkspace(root: string, path: string): boolean {
|
|
|
35
36
|
return !isAbsolute(remainder) && !remainder.startsWith("..")
|
|
36
37
|
&& remainder.split(/[\\/]/u).slice(0, 3).join("/") === PROJECT_CLOUD_RUNS_RELATIVE;
|
|
37
38
|
}
|
|
39
|
+
|
|
40
|
+
export function cloudArtifactDirectory(state: WorkflowState): string {
|
|
41
|
+
const path = cloudDetails(state)?.artifactDirectory;
|
|
42
|
+
return path && isCloudRunWorkspace(state.root, path) ? path : state.root;
|
|
43
|
+
}
|
|
@@ -43,7 +43,6 @@ export interface CloudTemplateSource {
|
|
|
43
43
|
name: string;
|
|
44
44
|
createdAt: string;
|
|
45
45
|
updatedAt: string;
|
|
46
|
-
kind?: "prompt" | "terraform";
|
|
47
46
|
}
|
|
48
47
|
|
|
49
48
|
export interface CloudRunnerSummary {
|
|
@@ -110,23 +109,13 @@ export interface WorkflowState {
|
|
|
110
109
|
reason?: string;
|
|
111
110
|
}
|
|
112
111
|
|
|
113
|
-
interface LegacyWorkflowState {
|
|
114
|
-
version: 1;
|
|
115
|
-
active: boolean;
|
|
116
|
-
mode?: WorkflowMode;
|
|
117
|
-
root?: string;
|
|
118
|
-
activatedAt?: string;
|
|
119
|
-
deactivatedAt?: string;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
112
|
function isCloudTemplateSource(value: unknown): value is CloudTemplateSource {
|
|
123
113
|
if (!value || typeof value !== "object") return false;
|
|
124
114
|
const data = value as Record<string, unknown>;
|
|
125
115
|
return typeof data.id === "string"
|
|
126
116
|
&& typeof data.name === "string"
|
|
127
117
|
&& typeof data.createdAt === "string"
|
|
128
|
-
&& typeof data.updatedAt === "string"
|
|
129
|
-
&& (data.kind === undefined || data.kind === "prompt" || data.kind === "terraform");
|
|
118
|
+
&& typeof data.updatedAt === "string";
|
|
130
119
|
}
|
|
131
120
|
|
|
132
121
|
function isCloudRunnerSummary(value: unknown): value is CloudRunnerSummary {
|
|
@@ -205,20 +194,7 @@ export function decodeWorkflowState(value: unknown): WorkflowState | undefined {
|
|
|
205
194
|
}
|
|
206
195
|
return data as unknown as WorkflowState;
|
|
207
196
|
}
|
|
208
|
-
|
|
209
|
-
const legacy = data as unknown as LegacyWorkflowState;
|
|
210
|
-
if (legacy.version !== 1 || !legacy.active || !legacy.mode || !legacy.root || !legacy.activatedAt) {
|
|
211
|
-
return undefined;
|
|
212
|
-
}
|
|
213
|
-
return {
|
|
214
|
-
version: 2,
|
|
215
|
-
status: "active",
|
|
216
|
-
mode: legacy.mode,
|
|
217
|
-
root: legacy.root,
|
|
218
|
-
phase: "legacy",
|
|
219
|
-
activatedAt: legacy.activatedAt,
|
|
220
|
-
updatedAt: legacy.activatedAt,
|
|
221
|
-
};
|
|
197
|
+
return undefined;
|
|
222
198
|
}
|
|
223
199
|
|
|
224
200
|
export const WORKFLOW_STATE_CODEC: StateCodec<WorkflowState> = {
|
|
@@ -235,6 +211,10 @@ export function activeWorkflow(entries: readonly CustomSessionEntry[]): Workflow
|
|
|
235
211
|
return state?.status === "active" ? state : undefined;
|
|
236
212
|
}
|
|
237
213
|
|
|
214
|
+
export function cloudDetails(state: WorkflowState): CloudWorkflowDetails | undefined {
|
|
215
|
+
return state.mode === "cloud" ? state.details : undefined;
|
|
216
|
+
}
|
|
217
|
+
|
|
238
218
|
export function createWorkflowState(
|
|
239
219
|
mode: "vibe" | "sdd",
|
|
240
220
|
root: string,
|
|
@@ -27,7 +27,7 @@ The command already collected the provider, deployment choice, objective, and va
|
|
|
27
27
|
|
|
28
28
|
The activation context includes a task artifact workspace inside the locked project root. Keep all generated Cloud-task artifacts there; never write them to the project root or invent top-level task directories.
|
|
29
29
|
|
|
30
|
-
The run workspace is retained after the session so
|
|
30
|
+
The run workspace is retained after the session so a successful managed apply can automatically extract a validated local artifact bundle. Do not move or duplicate these files into another temporary directory.
|
|
31
31
|
|
|
32
32
|
- `discovery/`: provider CLI skeletons, read-only snapshots, and API input exploration;
|
|
33
33
|
- `terraform/`: Terraform source, module lock files, and generated provider configuration (never state files or tfvars with secrets);
|
|
@@ -130,4 +130,4 @@ On success, summarize:
|
|
|
130
130
|
- ongoing cost, security, monitoring, backup, and credential-rotation considerations;
|
|
131
131
|
- rollback and teardown procedure (do not execute teardown unless separately requested and approved).
|
|
132
132
|
|
|
133
|
-
|
|
133
|
+
After a successful managed apply, HWCode automatically creates or updates a local schema-v2 Terraform artifact bundle. It contains filtered Terraform/Helm/discovery artifacts, verification guidance, an exclusion report, and a digest of the actual saved content. Runtime state, plans, tfvars, credentials, and discovered runtime identifiers must not be copied. Reuse is blocked if the bundle content no longer matches its manifest digest.
|
package/README.md
CHANGED
|
@@ -153,33 +153,34 @@ scanned through a verified ProxyJump. Runner execution requires Terraform,
|
|
|
153
153
|
Terraform backend. The exact policy-checked file manifest is uploaded and its
|
|
154
154
|
per-file digest is verified remotely before execution.
|
|
155
155
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
Cloud approval
|
|
156
|
+
Knowledge capture is independent from workflow completion. A background Worker
|
|
157
|
+
checks settled conversations once per minute and asks the active model to
|
|
158
|
+
review only the session delta since the last successful checkpoint. It runs for
|
|
159
|
+
ordinary conversations and Vibe, SDD, and Cloud alike, produces no visible
|
|
160
|
+
assistant turn, and is cancelled when new input or a new agent run starts.
|
|
161
|
+
|
|
162
|
+
Reusable knowledge is stored under `~/.hwcode/knowledge/` using two tracks.
|
|
163
|
+
Short, high-confidence rules live under `rules/` and every applicable rule file
|
|
164
|
+
is loaded when a session starts. Detailed SOPs and evolving engineering
|
|
165
|
+
experience live under `topics/`; only the bounded `MEMORY.md` keyword index is
|
|
166
|
+
loaded into the prompt, while `catalog.json` remains a complete token-free
|
|
167
|
+
machine index. The `hwcode_knowledge_lookup` tool loads a detailed topic on
|
|
168
|
+
demand. Exact repeats are reinforced, safe explicit revisions replace their
|
|
169
|
+
source, and ambiguous conflicts are quarantined under `pending/`. Legacy
|
|
170
|
+
`index.json` and `records/` data are neither loaded nor migrated.
|
|
171
|
+
|
|
172
|
+
A successful managed Terraform apply additionally produces a schema-v2
|
|
173
|
+
Terraform Template bundle containing filtered Terraform, Helm, discovery and
|
|
174
|
+
report artifacts plus verification notes, exclusion reasons, and
|
|
175
|
+
content/metadata integrity digests. Bundles are stored under
|
|
176
|
+
`~/.hwcode/cloud/templates/terraform/` with user-only permissions; credential
|
|
177
|
+
values are excluded and redacted before artifacts are persisted. A successful
|
|
178
|
+
managed apply automatically creates a new bundle, or atomically updates the
|
|
179
|
+
source bundle when the run started from one. `/hwcode-cloud` offers saved
|
|
180
|
+
bundles when it starts without an inline request. Bundles remain
|
|
181
|
+
extension-private executable artifacts, separate from the knowledge base, and
|
|
182
|
+
reuse always goes through the normal Cloud credential isolation and approval
|
|
183
|
+
guards.
|
|
183
184
|
|
|
184
185
|
## Internal architecture
|
|
185
186
|
|
|
@@ -197,10 +198,11 @@ Reusable behavior lives under `.pi/lib/`:
|
|
|
197
198
|
|
|
198
199
|
Runtime data is separated by ownership: project SDD specifications live under
|
|
199
200
|
`.hwcode/specs/`, retained Cloud run artifacts live under
|
|
200
|
-
`.hwcode/cloud/runs/<run-id>/`,
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
201
|
+
`.hwcode/cloud/runs/<run-id>/`, user-private credentials, SSH trust, and
|
|
202
|
+
Terraform Templates live under `~/.hwcode/cloud/`, and the cross-workflow
|
|
203
|
+
knowledge base lives under `~/.hwcode/knowledge/`. Cloud run artifacts are
|
|
204
|
+
deliberately retained for inspection and automatic bundle extraction; HWCode
|
|
205
|
+
does not apply a time-based cleanup policy.
|
|
204
206
|
|
|
205
207
|
`settings.json` is layered as defaults → profile → project for settings such as
|
|
206
208
|
context and hidden commands. `welcome.json` uses a single replacing resource.
|
package/bin/hwcode.js
CHANGED
|
@@ -56,9 +56,7 @@ HWCode 交互命令:
|
|
|
56
56
|
/hwcode-vibe [需求] 启动持续对话式 Vibe Coding workflow
|
|
57
57
|
/hwcode-sdd [需求] 启动测试优先的 Spec-Driven workflow
|
|
58
58
|
/hwcode-cloud [需求] 启动凭据隔离、变更审批的云部署 workflow
|
|
59
|
-
/hwcode-
|
|
60
|
-
/hwcode-cloud-save-template 保存或更新 Prompt/Terraform Deployment Template
|
|
61
|
-
/hwcode 查看、完成或取消当前 workflow
|
|
59
|
+
/hwcode-status 查看、完成或取消当前 workflow
|
|
62
60
|
/cd <目录> 持久切换当前会话工作目录
|
|
63
61
|
/model 选择模型
|
|
64
62
|
/login 登录或配置模型提供商
|
|
@@ -115,6 +113,7 @@ for (const extension of [
|
|
|
115
113
|
"cwd.ts",
|
|
116
114
|
"footer-tps.ts",
|
|
117
115
|
"hwcode.ts",
|
|
116
|
+
"knowledge.ts",
|
|
118
117
|
"model-providers.ts",
|
|
119
118
|
"welcome.ts",
|
|
120
119
|
"workflows.ts",
|