@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.
Files changed (35) hide show
  1. package/.pi/extensions/command-filter.ts +2 -3
  2. package/.pi/extensions/cwd.ts +1 -4
  3. package/.pi/extensions/knowledge.ts +279 -0
  4. package/.pi/extensions/workflows/cloud/activation.ts +57 -43
  5. package/.pi/extensions/workflows/cloud/commands.ts +1 -86
  6. package/.pi/extensions/workflows/cloud/events.ts +16 -19
  7. package/.pi/extensions/workflows/cloud/interactions.ts +102 -0
  8. package/.pi/extensions/workflows/cloud/provider-tools.ts +15 -9
  9. package/.pi/extensions/workflows/cloud/runner-tools.ts +21 -20
  10. package/.pi/extensions/workflows/cloud/runtime.ts +41 -4
  11. package/.pi/extensions/workflows/cloud/terraform-tools.ts +43 -30
  12. package/.pi/extensions/workflows/sdd.ts +2 -1
  13. package/.pi/extensions/workflows/vibe.ts +2 -1
  14. package/.pi/extensions/workflows/workspace-guard.ts +1 -5
  15. package/.pi/lib/extension-ui.ts +52 -0
  16. package/.pi/lib/knowledge/extractor.ts +35 -0
  17. package/.pi/lib/knowledge/matcher.ts +122 -0
  18. package/.pi/lib/knowledge/review-worker.ts +64 -0
  19. package/.pi/lib/knowledge/sanitize.ts +26 -0
  20. package/.pi/lib/knowledge/store.ts +251 -0
  21. package/.pi/lib/knowledge/types.ts +59 -0
  22. package/.pi/lib/knowledge/worker-protocol.ts +12 -0
  23. package/.pi/lib/runtime/defaults.ts +28 -0
  24. package/.pi/lib/runtime/paths.ts +28 -16
  25. package/.pi/lib/tool-result.ts +7 -0
  26. package/.pi/lib/workflows/cloud/bundles.ts +73 -40
  27. package/.pi/lib/workflows/cloud/workspace.ts +6 -0
  28. package/.pi/lib/workflows/state.ts +6 -26
  29. package/.pi/skills/hwcode-cloud/SKILL.md +2 -2
  30. package/README.md +33 -31
  31. package/bin/hwcode.js +2 -3
  32. package/package.json +1 -1
  33. package/.pi/extensions/workflows/cloud/shared.ts +0 -224
  34. package/.pi/lib/workflows/cloud/template-save.ts +0 -108
  35. package/.pi/lib/workflows/cloud/templates.ts +0 -314
@@ -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
- cloudPromptTemplates: string;
22
- cloudDeploymentTemplates: string;
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, "cloud");
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, "cloud");
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, "credentials.enc"),
45
- cloudKnownHosts: join(cloud, "known_hosts"),
46
- cloudPromptTemplates: join(cloud, "templates", "prompts"),
47
- cloudDeploymentTemplates: join(cloud, "templates", "deployments"),
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 { legacyUserCloudTemplatePaths, userRuntimePaths } from "../../runtime/paths.ts";
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 CloudDeploymentTemplate {
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 cloudDeploymentTemplateSource(template: CloudDeploymentTemplate): {
45
- id: string; name: string; createdAt: string; updatedAt: string; kind: "terraform";
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, kind: "terraform" };
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).cloudDeploymentTemplates;
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 saveCloudDeploymentTemplate(
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
- ): CloudDeploymentTemplate {
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("Cloud Deployment Template name is required");
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"), `${renderCloudPromptTemplate(state)}\n`);
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(`Cloud Deployment Template already exists: ${manifest.id}`);
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 updateCloudDeploymentTemplate(
282
- template: CloudDeploymentTemplate,
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
- ): CloudDeploymentTemplate {
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"), `${renderCloudPromptTemplate(state)}\n`);
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 parseCloudDeploymentTemplate(path: string): CloudDeploymentTemplate | undefined {
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 (![1, 2].includes(raw.schemaVersion as number) || typeof raw.id !== "string" || typeof raw.name !== "string"
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
- const oldArtifacts = (raw.artifacts && typeof raw.artifacts === "object" ? raw.artifacts : {}) as TerraformTemplateManifest["artifacts"];
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 listCloudDeploymentTemplates(directory?: string, home = homedir()): CloudDeploymentTemplate[] {
335
- const directories = directory
336
- ? [directory]
337
- : [...new Set([defaultCloudBundleDirectory(home), legacyUserCloudTemplatePaths(home).deployments])];
338
- return directories
339
- .filter(existsSync)
340
- .flatMap((path) => readdirSync(path, { withFileTypes: true })
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 materializeCloudDeploymentTemplate(template: CloudDeploymentTemplate, workspace: string): { terraformPath: string } {
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("Cloud Deployment Template content digest mismatch; reuse is blocked");
351
- if (template.manifest.metadataDigest !== digestManifestMetadata(template.manifest)) throw new Error("Cloud Deployment Template metadata digest mismatch; reuse is blocked");
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 `/hwcode-cloud-save-template` can safely extract its validated artifacts. Do not move or duplicate these files into another temporary directory.
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
- When saving a successful Terraform run, the local Deployment Template is a schema-v2 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; schema-v1 bundles are migrated in memory when read.
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
- After at least one cloud command succeeds, run
157
- `/hwcode-cloud-save-template [name]` to save the objective, validated execution
158
- sequence, failed approaches, prerequisites, and optional lessons learned as a
159
- local Prompt Template. A successful managed Terraform apply instead produces a
160
- schema-v2 Deployment Template bundle containing filtered Terraform, Helm,
161
- discovery and report artifacts plus verification notes, exclusion reasons, and
162
- content/metadata integrity digests. Each template records its user-facing name plus creation
163
- and update timestamps. Its prompt is layered into stable intent, prerequisites,
164
- the preferred known-good path, expensive failed paths that must not be retried,
165
- and the guarded execution contract. Prompt Templates are stored under
166
- `~/.hwcode/cloud/templates/prompts/`; Deployment Template bundles are stored
167
- under `~/.hwcode/cloud/templates/deployments/`, both with user-only permissions.
168
- Existing resources under the legacy `~/.hwcode/cloud/prompts/` and
169
- `~/.hwcode/cloud/templates/` locations remain discoverable and are updated in
170
- place when reused. Credential values are
171
- excluded and redacted before steps are persisted.
172
-
173
- Use `/hwcode-cloud-template [additional instructions]` to select and start a
174
- saved template by name and last-updated time. `/hwcode-cloud` also offers saved
175
- templates when it starts without an inline request. If a session started from a
176
- template, saving again asks whether to update that source template or create a
177
- new one. The files use a Pi-compatible
178
- Markdown/frontmatter format, but HWCode intentionally keeps them out of Pi
179
- resource discovery so each saved template does not become another slash
180
- command. Templates are extension-private resources and always run through the
181
- fixed `/hwcode-cloud-template` entry point, preserving credential isolation and
182
- Cloud approval guards.
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>/`, and user-private credentials, SSH trust, and
201
- templates live under `~/.hwcode/cloud/`. Cloud run artifacts are deliberately
202
- retained for inspection and later template extraction; HWCode does not apply a
203
- time-based cleanup policy.
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-cloud-template [补充] 从本地成功模板启动 Cloud workflow
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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hadooppei/hwcode",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "A customizable terminal coding agent with local-model support and HWCode workflows.",
5
5
  "type": "module",
6
6
  "bin": {