@hadooppei/hwcode 1.0.7 → 1.0.9
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 +224 -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 +260 -0
- package/.pi/lib/knowledge/sanitize.ts +26 -0
- package/.pi/lib/knowledge/session-scanner.ts +155 -0
- package/.pi/lib/knowledge/store.ts +365 -0
- package/.pi/lib/knowledge/types.ts +91 -0
- package/.pi/lib/knowledge/worker-protocol.ts +20 -0
- package/.pi/lib/runtime/defaults.ts +31 -0
- package/.pi/lib/runtime/paths.ts +33 -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 +36 -31
- package/bin/hwcode.js +2 -3
- package/package.json +1 -1
- package/.pi/extensions/workflows/cloud/shared.ts +0 -230
- package/.pi/lib/workflows/cloud/template-save.ts +0 -108
- package/.pi/lib/workflows/cloud/templates.ts +0 -314
|
@@ -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,37 @@ 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
|
-
under
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
156
|
+
Knowledge capture is independent from workflow and session completion. Every
|
|
157
|
+
HWCode process owns a lightweight Worker, but a user-private Unix socket elects
|
|
158
|
+
exactly one machine-local Leader. The Leader checks every open or closed session
|
|
159
|
+
once per minute and serially asks its configured model to review uncommitted
|
|
160
|
+
session deltas. Workers without model access remain ineligible; they rejoin the
|
|
161
|
+
election automatically after model authentication becomes available.
|
|
162
|
+
|
|
163
|
+
Reusable knowledge is stored as immutable generations under
|
|
164
|
+
`~/.hwcode/knowledge-v3/` using two tracks. Short, high-confidence rules live
|
|
165
|
+
under each generation's `rules/` directory and every applicable rule is loaded
|
|
166
|
+
when an agent starts. Detailed SOPs and evolving engineering experience live
|
|
167
|
+
under `topics/`; only the bounded keyword index is loaded into the prompt. The
|
|
168
|
+
`hwcode_knowledge_lookup` tool loads a detailed topic on demand. A generation
|
|
169
|
+
atomically publishes rules, topics, the complete catalog, and per-session review
|
|
170
|
+
cursors, so a catalog can never reference an unpublished body. Exact repeats
|
|
171
|
+
are reinforced, safe explicit revisions replace their source, and ambiguous
|
|
172
|
+
conflicts are quarantined under `pending/`. Earlier knowledge layouts are
|
|
173
|
+
neither loaded nor migrated.
|
|
174
|
+
|
|
175
|
+
A successful managed Terraform apply additionally produces a schema-v2
|
|
176
|
+
Terraform Template bundle containing filtered Terraform, Helm, discovery and
|
|
177
|
+
report artifacts plus verification notes, exclusion reasons, and
|
|
178
|
+
content/metadata integrity digests. Bundles are stored under
|
|
179
|
+
`~/.hwcode/cloud/templates/terraform/` with user-only permissions; credential
|
|
180
|
+
values are excluded and redacted before artifacts are persisted. A successful
|
|
181
|
+
managed apply automatically creates a new bundle, or atomically updates the
|
|
182
|
+
source bundle when the run started from one. `/hwcode-cloud` offers saved
|
|
183
|
+
bundles when it starts without an inline request. Bundles remain
|
|
184
|
+
extension-private executable artifacts, separate from the knowledge base, and
|
|
185
|
+
reuse always goes through the normal Cloud credential isolation and approval
|
|
186
|
+
guards.
|
|
183
187
|
|
|
184
188
|
## Internal architecture
|
|
185
189
|
|
|
@@ -197,10 +201,11 @@ Reusable behavior lives under `.pi/lib/`:
|
|
|
197
201
|
|
|
198
202
|
Runtime data is separated by ownership: project SDD specifications live under
|
|
199
203
|
`.hwcode/specs/`, retained Cloud run artifacts live under
|
|
200
|
-
`.hwcode/cloud/runs/<run-id>/`,
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
+
`.hwcode/cloud/runs/<run-id>/`, user-private credentials, SSH trust, and
|
|
205
|
+
Terraform Templates live under `~/.hwcode/cloud/`, and the cross-workflow
|
|
206
|
+
knowledge base lives under `~/.hwcode/knowledge-v3/`. Cloud run artifacts are
|
|
207
|
+
deliberately retained for inspection and automatic bundle extraction; HWCode
|
|
208
|
+
does not apply a time-based cleanup policy.
|
|
204
209
|
|
|
205
210
|
`settings.json` is layered as defaults → profile → project for settings such as
|
|
206
211
|
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",
|