@hadooppei/hwcode 0.2.4 → 1.0.0

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 (44) hide show
  1. package/.pi/extensions/hwcode.ts +35 -2
  2. package/.pi/extensions/model-providers.ts +1 -2
  3. package/.pi/extensions/workflows/cloud/activation.ts +235 -0
  4. package/.pi/extensions/workflows/cloud/commands.ts +96 -0
  5. package/.pi/extensions/workflows/cloud/events.ts +58 -0
  6. package/.pi/extensions/workflows/cloud/index.ts +17 -0
  7. package/.pi/extensions/workflows/cloud/provider-tools.ts +127 -0
  8. package/.pi/extensions/workflows/cloud/runner-tools.ts +129 -0
  9. package/.pi/extensions/workflows/cloud/runtime.ts +97 -0
  10. package/.pi/extensions/workflows/cloud/shared.ts +213 -0
  11. package/.pi/extensions/workflows/cloud/terraform-tools.ts +126 -0
  12. package/.pi/extensions/workflows/vibe-sdd.ts +300 -0
  13. package/.pi/extensions/workflows.ts +6 -253
  14. package/.pi/lib/runtime/config.ts +3 -7
  15. package/.pi/lib/runtime/defaults.ts +20 -0
  16. package/.pi/lib/runtime/paths.ts +68 -0
  17. package/.pi/lib/runtime/session-state.ts +0 -34
  18. package/.pi/lib/{cloud → workflows/cloud}/adapters.ts +7 -6
  19. package/.pi/lib/workflows/cloud/bundles.ts +358 -0
  20. package/.pi/lib/workflows/cloud/execution.ts +28 -0
  21. package/.pi/lib/{cloud → workflows/cloud}/process.ts +5 -3
  22. package/.pi/lib/workflows/cloud/remote/bootstrap.ts +23 -0
  23. package/.pi/lib/workflows/cloud/remote/connect.ts +83 -0
  24. package/.pi/lib/workflows/cloud/remote/host-key.ts +35 -0
  25. package/.pi/lib/workflows/cloud/remote/profiles.ts +100 -0
  26. package/.pi/lib/workflows/cloud/remote/ssh-transport.ts +104 -0
  27. package/.pi/lib/workflows/cloud/remote/workspace.ts +109 -0
  28. package/.pi/lib/{cloud → workflows/cloud}/template-save.ts +3 -2
  29. package/.pi/lib/{cloud → workflows/cloud}/templates.ts +18 -10
  30. package/.pi/lib/workflows/cloud/terraform/plan.ts +109 -0
  31. package/.pi/lib/workflows/cloud/terraform/policy.ts +36 -0
  32. package/.pi/lib/workflows/cloud/terraform/runner.ts +64 -0
  33. package/.pi/lib/{cloud-vault.ts → workflows/cloud/vault.ts} +26 -24
  34. package/.pi/lib/workflows/cloud/workspace.ts +37 -0
  35. package/.pi/lib/workflows/sdd.ts +11 -0
  36. package/.pi/lib/workflows/state.ts +114 -2
  37. package/.pi/lib/working-directory.ts +0 -58
  38. package/.pi/skills/hwcode-cloud/SKILL.md +32 -1
  39. package/.pi/skills/hwcode-sdd/SKILL.md +2 -0
  40. package/README.md +41 -8
  41. package/bin/hwcode.js +2 -6
  42. package/package.json +8 -3
  43. package/.pi/extensions/cloud.ts +0 -629
  44. /package/.pi/lib/{cloud-providers.ts → workflows/cloud/providers.ts} +0 -0
@@ -14,9 +14,11 @@ import {
14
14
  writeFileSync,
15
15
  } from "node:fs";
16
16
  import { homedir } from "node:os";
17
- import { dirname, join } from "node:path";
17
+ import { dirname } from "node:path";
18
18
 
19
- import { isCloudVendorId, type CloudCredentials, type CloudVendorId } from "./cloud-providers.ts";
19
+ import { isCloudVendorId, type CloudCredentials, type CloudVendorId } from "./providers.ts";
20
+ import { validateRemoteTargetProfile, type RemoteTargetProfile } from "./remote/profiles.ts";
21
+ import { userRuntimePaths } from "../../runtime/paths.ts";
20
22
 
21
23
  const AAD = Buffer.from("hwcode-cloud-credentials-v1", "utf8");
22
24
  const KEY_LENGTH = 32;
@@ -30,8 +32,9 @@ export interface CloudCredentialProfile {
30
32
  }
31
33
 
32
34
  export interface VaultPayload {
33
- version: 2;
34
- providers: Partial<Record<CloudVendorId, CloudCredentialProfile[]>>;
35
+ version: 2;
36
+ providers: Partial<Record<CloudVendorId, CloudCredentialProfile[]>>;
37
+ runners?: RemoteTargetProfile[];
35
38
  }
36
39
 
37
40
  interface EncryptedVault {
@@ -45,7 +48,7 @@ interface EncryptedVault {
45
48
  }
46
49
 
47
50
  export function defaultCloudVaultPath(home = homedir()): string {
48
- return join(home, ".hwcode", "cloud", "credentials.enc");
51
+ return userRuntimePaths(home).cloudVault;
49
52
  }
50
53
 
51
54
  export function cloudVaultExists(path = defaultCloudVaultPath()): boolean {
@@ -69,7 +72,7 @@ export function normalizeCloudVaultPayload(value: unknown): VaultPayload {
69
72
  if (!payload.providers || typeof payload.providers !== "object" || Array.isArray(payload.providers)) {
70
73
  throw new Error("invalid vault payload");
71
74
  }
72
- const providers: VaultPayload["providers"] = {};
75
+ const providers: VaultPayload["providers"] = {};
73
76
  for (const [vendor, stored] of Object.entries(payload.providers)) {
74
77
  if (!isCloudVendorId(vendor)) throw new Error("unsupported cloud provider in vault");
75
78
  if (payload.version === 1) {
@@ -96,7 +99,10 @@ export function normalizeCloudVaultPayload(value: unknown): VaultPayload {
96
99
  });
97
100
  }
98
101
  if (payload.version !== 1 && payload.version !== 2) throw new Error("unsupported vault payload");
99
- return { version: 2, providers };
102
+ const runners = payload.runners === undefined
103
+ ? undefined
104
+ : Array.isArray(payload.runners) ? payload.runners.map(validateRemoteTargetProfile) : (() => { throw new Error("invalid remote runner profiles"); })();
105
+ return { version: 2, providers, ...(runners ? { runners } : {}) };
100
106
  }
101
107
 
102
108
  export function readCloudVault(password: string, path = defaultCloudVaultPath()): VaultPayload {
@@ -151,23 +157,6 @@ export function writeCloudVault(payload: VaultPayload, password: string, path =
151
157
  chmodSync(path, 0o600);
152
158
  }
153
159
 
154
- export function setCloudCredentials(
155
- payload: VaultPayload,
156
- vendor: CloudVendorId,
157
- credentials: CloudCredentials,
158
- ): VaultPayload {
159
- const first = payload.providers[vendor]?.[0];
160
- return saveCloudCredentialProfile(payload, vendor, credentials, first?.id);
161
- }
162
-
163
- export function getCloudCredentials(
164
- payload: VaultPayload,
165
- vendor: CloudVendorId,
166
- ): CloudCredentials | undefined {
167
- const credentials = payload.providers[vendor]?.[0]?.credentials;
168
- return credentials ? { ...credentials } : undefined;
169
- }
170
-
171
160
  export function listCloudCredentialProfiles(
172
161
  payload: VaultPayload,
173
162
  vendor: CloudVendorId,
@@ -207,3 +196,16 @@ export function cloudCredentialProfileLabel(
207
196
  const region = profile.credentials.region?.trim() || "未配置";
208
197
  return `使用已有凭据${number} · Region: ${region}`;
209
198
  }
199
+
200
+ export function listRemoteTargetProfiles(payload: VaultPayload): RemoteTargetProfile[] {
201
+ return (payload.runners ?? []).map((profile) => ({ ...profile }));
202
+ }
203
+
204
+ export function saveRemoteTargetProfile(payload: VaultPayload, profile: RemoteTargetProfile): VaultPayload {
205
+ const validated = validateRemoteTargetProfile(profile);
206
+ const profiles = listRemoteTargetProfiles(payload);
207
+ const existingIndex = profiles.findIndex((entry) => entry.id === validated.id);
208
+ if (existingIndex >= 0) profiles[existingIndex] = validated;
209
+ else profiles.push(validated);
210
+ return { version: 2, providers: { ...payload.providers }, runners: profiles };
211
+ }
@@ -0,0 +1,37 @@
1
+ import { mkdirSync } from "node:fs";
2
+ import { randomUUID } from "node:crypto";
3
+ import { isAbsolute, join, relative, resolve } from "node:path";
4
+
5
+ import { PROJECT_CLOUD_RUNS_RELATIVE, projectRuntimePaths } from "../../runtime/paths.ts";
6
+
7
+ export interface CloudRunWorkspace {
8
+ runId: string;
9
+ path: string;
10
+ discoveryPath: string;
11
+ terraformPath: string;
12
+ chartsPath: string;
13
+ reportsPath: string;
14
+ }
15
+
16
+ export function createCloudRunWorkspace(root: string, runId = randomUUID()): CloudRunWorkspace {
17
+ const path = join(projectRuntimePaths(root).cloudRuns, runId);
18
+ const workspace: CloudRunWorkspace = {
19
+ runId, path,
20
+ discoveryPath: join(path, "discovery"),
21
+ terraformPath: join(path, "terraform"),
22
+ chartsPath: join(path, "charts"),
23
+ reportsPath: join(path, "reports"),
24
+ };
25
+ for (const directory of [workspace.discoveryPath, workspace.terraformPath, workspace.chartsPath, workspace.reportsPath]) {
26
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
27
+ }
28
+ return workspace;
29
+ }
30
+
31
+ export function isCloudRunWorkspace(root: string, path: string): boolean {
32
+ const resolvedRoot = resolve(root);
33
+ const resolvedPath = resolve(path);
34
+ const remainder = relative(resolvedRoot, resolvedPath);
35
+ return !isAbsolute(remainder) && !remainder.startsWith("..")
36
+ && remainder.split(/[\\/]/u).slice(0, 3).join("/") === PROJECT_CLOUD_RUNS_RELATIVE;
37
+ }
@@ -0,0 +1,11 @@
1
+ import type { SddPhase, SddWorkflowProgress } from "./state.ts";
2
+
3
+ export const SDD_PHASES: readonly SddPhase[] = ["discovery", "requirements", "design", "test-plan", "tasks", "tests", "implementation", "verification"];
4
+
5
+ export function advanceSddProgress(progress: SddWorkflowProgress, nextPhase: SddPhase, evidence: string, approvedAt = new Date()): SddWorkflowProgress {
6
+ const currentIndex = SDD_PHASES.indexOf(progress.phase);
7
+ if (SDD_PHASES[currentIndex + 1] !== nextPhase) throw new Error(`Invalid SDD transition ${progress.phase} → ${nextPhase}. Complete phases in order.`);
8
+ const normalizedEvidence = evidence.replace(/[\r\n]+/gu, " ").trim().slice(0, 1_000);
9
+ if (!normalizedEvidence) throw new Error("SDD phase approval evidence is required");
10
+ return { phase: nextPhase, approvals: [...progress.approvals, { phase: nextPhase, evidence: normalizedEvidence, approvedAt: approvedAt.toISOString() }] };
11
+ }
@@ -5,10 +5,18 @@ export const WORKFLOW_EXTERNAL_AUDIT_TYPE = "hwcode-workflow-external-approval";
5
5
 
6
6
  export type WorkflowMode = "vibe" | "sdd" | "cloud";
7
7
  export type WorkflowStatus = "active" | "completed" | "cancelled" | "failed";
8
+ export type SddPhase = "discovery" | "requirements" | "design" | "test-plan" | "tasks" | "tests" | "implementation" | "verification";
9
+
10
+ export interface SddWorkflowProgress {
11
+ phase: SddPhase;
12
+ approvals: Array<{ phase: SddPhase; evidence: string; approvedAt: string }>;
13
+ }
8
14
 
9
15
  export interface FailedApproach {
10
16
  approach: string;
11
17
  reason: string;
18
+ stage?: string;
19
+ attempts?: number;
12
20
  failedAt: string;
13
21
  }
14
22
 
@@ -21,11 +29,54 @@ export interface CloudExecutionStep {
21
29
  completedAt: string;
22
30
  }
23
31
 
32
+ export interface CloudResourceRecord {
33
+ id: string;
34
+ type: string;
35
+ region: string;
36
+ ownership: "existing" | "workflow-created";
37
+ status: "active" | "deleted";
38
+ updatedAt: string;
39
+ }
40
+
24
41
  export interface CloudTemplateSource {
25
42
  id: string;
26
43
  name: string;
27
44
  createdAt: string;
28
45
  updatedAt: string;
46
+ kind?: "prompt" | "terraform";
47
+ }
48
+
49
+ export interface CloudRunnerSummary {
50
+ id: string;
51
+ name: string;
52
+ vendor: CloudWorkflowDetails["vendor"];
53
+ region: string;
54
+ host: string;
55
+ port: number;
56
+ user: string;
57
+ remoteRoot: string;
58
+ identityType: "instance-role" | "agency" | "ssh-only";
59
+ hostKeyFingerprint: string;
60
+ }
61
+
62
+ export interface TerraformRunState {
63
+ runId: string;
64
+ runnerId: string;
65
+ phase: "synced" | "validated" | "planned" | "applied" | "failed";
66
+ sourceDigest: string;
67
+ sourcePath?: string;
68
+ remoteWorkspace: string;
69
+ planDigest?: string;
70
+ planSummary?: {
71
+ create: number;
72
+ update: number;
73
+ delete: number;
74
+ replace: number;
75
+ read: number;
76
+ sensitiveChangesHidden: number;
77
+ };
78
+ startedAt: string;
79
+ updatedAt: string;
29
80
  }
30
81
 
31
82
  export interface CloudWorkflowDetails {
@@ -35,9 +86,15 @@ export interface CloudWorkflowDetails {
35
86
  allowNonDeleteChanges: boolean;
36
87
  failedApproaches: FailedApproach[];
37
88
  successfulSteps: CloudExecutionStep[];
89
+ resources?: CloudResourceRecord[];
38
90
  terminalFailure: boolean;
91
+ artifactDirectory?: string;
39
92
  templateGuidance?: string;
40
93
  sourceTemplate?: CloudTemplateSource;
94
+ terraformSourcePath?: string;
95
+ runner?: CloudRunnerSummary;
96
+ runnerPreference?: "automatic" | "deferred" | "existing";
97
+ terraformRun?: TerraformRunState;
41
98
  }
42
99
 
43
100
  export interface WorkflowState {
@@ -49,6 +106,7 @@ export interface WorkflowState {
49
106
  activatedAt: string;
50
107
  updatedAt: string;
51
108
  details?: CloudWorkflowDetails;
109
+ sdd?: SddWorkflowProgress;
52
110
  reason?: string;
53
111
  }
54
112
 
@@ -67,7 +125,39 @@ function isCloudTemplateSource(value: unknown): value is CloudTemplateSource {
67
125
  return typeof data.id === "string"
68
126
  && typeof data.name === "string"
69
127
  && typeof data.createdAt === "string"
70
- && typeof data.updatedAt === "string";
128
+ && typeof data.updatedAt === "string"
129
+ && (data.kind === undefined || data.kind === "prompt" || data.kind === "terraform");
130
+ }
131
+
132
+ function isCloudRunnerSummary(value: unknown): value is CloudRunnerSummary {
133
+ if (!value || typeof value !== "object") return false;
134
+ const data = value as Record<string, unknown>;
135
+ return typeof data.id === "string"
136
+ && typeof data.name === "string"
137
+ && typeof data.vendor === "string"
138
+ && typeof data.region === "string"
139
+ && typeof data.host === "string"
140
+ && Number.isInteger(data.port)
141
+ && typeof data.user === "string"
142
+ && typeof data.remoteRoot === "string"
143
+ && typeof data.hostKeyFingerprint === "string"
144
+ && ["instance-role", "agency", "ssh-only"].includes(data.identityType as string);
145
+ }
146
+
147
+ function isTerraformRunState(value: unknown): value is TerraformRunState {
148
+ if (!value || typeof value !== "object") return false;
149
+ const data = value as Record<string, unknown>;
150
+ const summary = data.planSummary;
151
+ return typeof data.runId === "string"
152
+ && typeof data.runnerId === "string"
153
+ && ["synced", "validated", "planned", "applied", "failed"].includes(data.phase as string)
154
+ && typeof data.sourceDigest === "string"
155
+ && typeof data.remoteWorkspace === "string"
156
+ && typeof data.startedAt === "string"
157
+ && typeof data.updatedAt === "string"
158
+ && (data.planDigest === undefined || typeof data.planDigest === "string")
159
+ && (summary === undefined || (summary !== null && typeof summary === "object"
160
+ && Object.values(summary as Record<string, unknown>).every((entry) => Number.isInteger(entry))));
71
161
  }
72
162
 
73
163
  function isCloudDetails(value: unknown): value is CloudWorkflowDetails {
@@ -79,9 +169,22 @@ function isCloudDetails(value: unknown): value is CloudWorkflowDetails {
79
169
  && typeof data.allowNonDeleteChanges === "boolean"
80
170
  && Array.isArray(data.failedApproaches)
81
171
  && Array.isArray(data.successfulSteps)
172
+ && (data.resources === undefined || (Array.isArray(data.resources) && data.resources.every((resource) => {
173
+ if (!resource || typeof resource !== "object") return false;
174
+ const entry = resource as Record<string, unknown>;
175
+ return typeof entry.id === "string" && typeof entry.type === "string" && typeof entry.region === "string"
176
+ && ["existing", "workflow-created"].includes(entry.ownership as string)
177
+ && ["active", "deleted"].includes(entry.status as string)
178
+ && typeof entry.updatedAt === "string";
179
+ })))
82
180
  && typeof data.terminalFailure === "boolean"
181
+ && (data.artifactDirectory === undefined || typeof data.artifactDirectory === "string")
83
182
  && (data.templateGuidance === undefined || typeof data.templateGuidance === "string")
84
- && (data.sourceTemplate === undefined || isCloudTemplateSource(data.sourceTemplate));
183
+ && (data.terraformSourcePath === undefined || typeof data.terraformSourcePath === "string")
184
+ && (data.sourceTemplate === undefined || isCloudTemplateSource(data.sourceTemplate))
185
+ && (data.runner === undefined || isCloudRunnerSummary(data.runner))
186
+ && (data.runnerPreference === undefined || ["automatic", "deferred", "existing"].includes(data.runnerPreference as string))
187
+ && (data.terraformRun === undefined || isTerraformRunState(data.terraformRun));
85
188
  }
86
189
 
87
190
  export function decodeWorkflowState(value: unknown): WorkflowState | undefined {
@@ -95,6 +198,11 @@ export function decodeWorkflowState(value: unknown): WorkflowState | undefined {
95
198
  || typeof data.activatedAt !== "string"
96
199
  || typeof data.updatedAt !== "string") return undefined;
97
200
  if (data.mode === "cloud" && !isCloudDetails(data.details)) return undefined;
201
+ if (data.mode === "sdd" && data.sdd !== undefined) {
202
+ const sdd = data.sdd as Record<string, unknown>;
203
+ if (!( ["discovery", "requirements", "design", "test-plan", "tasks", "tests", "implementation", "verification"] as unknown[]).includes(sdd.phase)
204
+ || !Array.isArray(sdd.approvals)) return undefined;
205
+ }
98
206
  return data as unknown as WorkflowState;
99
207
  }
100
208
 
@@ -141,6 +249,7 @@ export function createWorkflowState(
141
249
  phase,
142
250
  activatedAt: now,
143
251
  updatedAt: now,
252
+ ...(mode === "sdd" ? { sdd: { phase: "discovery", approvals: [] } } : {}),
144
253
  };
145
254
  }
146
255
 
@@ -150,6 +259,7 @@ export function createCloudWorkflowState(
150
259
  deployCurrentProject: boolean,
151
260
  request: string,
152
261
  template?: { source: CloudTemplateSource; guidance: string },
262
+ artifactDirectory?: string,
153
263
  ): WorkflowState {
154
264
  const now = new Date().toISOString();
155
265
  return {
@@ -167,7 +277,9 @@ export function createCloudWorkflowState(
167
277
  allowNonDeleteChanges: false,
168
278
  failedApproaches: [],
169
279
  successfulSteps: [],
280
+ resources: [],
170
281
  terminalFailure: false,
282
+ ...(artifactDirectory ? { artifactDirectory } : {}),
171
283
  ...(template ? {
172
284
  templateGuidance: template.guidance,
173
285
  sourceTemplate: template.source,
@@ -29,11 +29,6 @@ export interface DirectoryChange {
29
29
  remainder: string;
30
30
  }
31
31
 
32
- export interface ChainedDirectoryChange {
33
- argument: string;
34
- standalone: boolean;
35
- }
36
-
37
32
  const workingDirectories = new Map<string, WorkingDirectoryState>();
38
33
 
39
34
  function isWorkingDirectoryState(value: unknown): value is WorkingDirectoryState {
@@ -190,59 +185,6 @@ export function parseLeadingDirectoryChange(command: string): DirectoryChange |
190
185
  return { argument, remainder: command.slice(index).trimStart() };
191
186
  }
192
187
 
193
- /** Find a standalone `cd` segment in a top-level `&&` or `;` command chain. */
194
- export function findChainedDirectoryChange(command: string): ChainedDirectoryChange | undefined {
195
- const segments: string[] = [];
196
- let segmentStart = 0;
197
- let quote: "'" | '"' | undefined;
198
- let escaped = false;
199
- let nesting = 0;
200
-
201
- for (let index = 0; index < command.length; index += 1) {
202
- const character = command[index];
203
- if (escaped) {
204
- escaped = false;
205
- continue;
206
- }
207
- if (character === "\\" && quote !== "'") {
208
- escaped = true;
209
- continue;
210
- }
211
- if (quote) {
212
- if (character === quote) quote = undefined;
213
- continue;
214
- }
215
- if (character === "'" || character === '"') {
216
- quote = character;
217
- continue;
218
- }
219
- if (character === "(" || character === "{") {
220
- nesting += 1;
221
- continue;
222
- }
223
- if (character === ")" || character === "}") {
224
- nesting = Math.max(0, nesting - 1);
225
- continue;
226
- }
227
- if (nesting > 0) continue;
228
-
229
- const separatorLength = command.slice(index, index + 2) === "&&" ? 2 : character === ";" ? 1 : 0;
230
- if (!separatorLength) continue;
231
- segments.push(command.slice(segmentStart, index));
232
- index += separatorLength - 1;
233
- segmentStart = index + 1;
234
- }
235
- segments.push(command.slice(segmentStart));
236
-
237
- for (const segment of segments) {
238
- const parsed = parseLeadingDirectoryChange(segment);
239
- if (parsed && !parsed.remainder) {
240
- return { argument: parsed.argument, standalone: segments.length === 1 };
241
- }
242
- }
243
- return undefined;
244
- }
245
-
246
188
  export function getActiveWorkflowRoot(entries: readonly SessionEntry[]): string | undefined {
247
189
  return activeWorkflow(entries)?.root;
248
190
  }
@@ -11,7 +11,7 @@ Use this skill only after `/hwcode-cloud` has activated the workflow. The activa
11
11
 
12
12
  - Never ask for, display, infer, copy, log, summarize, or place credentials in chat, shell text, source files, environment files, tool arguments, plans, or session artifacts.
13
13
  - Never read `~/.hwcode/cloud/credentials.enc`. The extension owns encryption, decryption, and credential injection.
14
- - Use `hwcode_cloud_exec` for every provider CLI, Terraform/OpenTofu, Pulumi, kubectl, or Helm operation. Direct Bash use for those commands is forbidden.
14
+ - Use `hwcode_cloud_exec` for provider CLI operations. Use `hwcode_runner_prepare` and `hwcode_terraform_*` for Terraform execution. Direct Bash use for those commands is forbidden.
15
15
  - Keep project reads, writes, builds, manifests, and generated artifacts inside the locked project root. External paths require the workflow guard's one-call approval.
16
16
  - Prefer short-lived, least-privilege identities and narrowly scoped roles. Never widen permissions merely to bypass an authorization error without explaining the exact missing permission and obtaining approval.
17
17
  - Do not expose sensitive values returned by a provider. If output unexpectedly contains a secret, do not repeat it; tell the user to rotate it.
@@ -20,6 +20,19 @@ Use this skill only after `/hwcode-cloud` has activated the workflow. The activa
20
20
 
21
21
  The command already collected the provider, deployment choice, objective, and validated account connection. Do not repeat those questions unless the activation context is contradictory.
22
22
 
23
+ ## Artifact workspace
24
+
25
+ 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.
26
+
27
+ 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.
28
+
29
+ - `discovery/`: provider CLI skeletons, read-only snapshots, and API input exploration;
30
+ - `terraform/`: Terraform source, module lock files, and generated provider configuration (never state files or tfvars with secrets);
31
+ - `charts/`: Helm chart sources and package archives;
32
+ - `reports/`: sanitized plans, verification results, and final summaries.
33
+
34
+ Provider CLIs run with this workspace as their current directory. When an argument must reference an existing project source file, use its absolute path under the locked project root.
35
+
23
36
  Inspect the project only as needed to determine:
24
37
 
25
38
  - application type, build and runtime requirements;
@@ -43,6 +56,19 @@ Before modifying account resources, present a plan containing:
43
56
 
44
57
  Prefer declarative, reviewable, idempotent infrastructure as code. Use a plan/dry-run command before apply when the selected tooling supports it. Pin important versions and avoid provider defaults that materially affect cost or exposure.
45
58
 
59
+ ### Terraform Runner
60
+
61
+ Do not ask the user to name or manually configure a Runner. If the activation context has no selected Runner and Terraform is required:
62
+
63
+ 1. design a minimal temporary Runner (image, network exposure, instance role/agency, SSH access, estimated cost);
64
+ 2. ask for approval to create those billable cloud resources;
65
+ 3. create them through `hwcode_cloud_exec`, preferring provider identity on the Runner over copying AK/SK;
66
+ 4. install Terraform and `tar` through the instance image or cloud-init, then pass the resulting endpoint to `hwcode_runner_connect`; it selects a local default SSH key, shows standard OpenSSH SHA256 host-key fingerprints for verification, and stores the Runner profile encrypted;
67
+ 5. when the target is private, use the tool's ProxyJump fields with the approved bastion instead of copying credentials or opening broad public access;
68
+ 6. call `hwcode_runner_prepare` to create the managed Runner directories and verify required capabilities, then use only `hwcode_terraform_*` for Terraform.
69
+
70
+ If an existing SSH host is selected, reuse it. Only ask for SSH information when it cannot be derived from the provider result or saved Runner profile.
71
+
46
72
  ## Phase 3: Execute with approval gates
47
73
 
48
74
  For every `hwcode_cloud_exec` call:
@@ -52,6 +78,7 @@ For every `hwcode_cloud_exec` call:
52
78
  - use a stable `approach` name for the current technical strategy;
53
79
  - pass an executable and argument vector, never shell syntax;
54
80
  - inspect the result before proceeding.
81
+ - after discovering, creating, or deleting a resource, call `hwcode_cloud_record_resource` with its non-secret provider identifier, type, region, ownership, and current status. This inventory is required for completion and cleanup reporting.
55
82
 
56
83
  The extension confirms every account-resource create or modification unless the user selects session-wide approval for non-delete changes. A deletion is always confirmed separately, even after that opt-out.
57
84
 
@@ -79,6 +106,8 @@ After three genuinely different approaches fail, stop all execution. Tell the us
79
106
 
80
107
  Do not attempt a fourth approach.
81
108
 
109
+ Read-only inspection and explicitly confirmed cleanup deletions remain allowed after the failure budget is reached; new resource changes do not.
110
+
82
111
  ## Completion
83
112
 
84
113
  On success, summarize:
@@ -89,3 +118,5 @@ On success, summarize:
89
118
  - verification results;
90
119
  - ongoing cost, security, monitoring, backup, and credential-rotation considerations;
91
120
  - rollback and teardown procedure (do not execute teardown unless separately requested and approved).
121
+
122
+ 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.
@@ -11,6 +11,8 @@ Make the approved specification the source of truth. Move through discovery, spe
11
11
 
12
12
  Confirm that the activation message states a locked project root and Git status. If this skill was invoked directly without the `/hwcode-sdd` project command, do not begin work; ask the user to run `/hwcode-sdd [initial requirement]`. The command confirms the directory, enforces the path boundary, initializes Git when approved, and verifies that the current directory is the repository root.
13
13
 
14
+ The activation starts a persisted `discovery` phase. After completing and presenting each phase artifact, call `hwcode_sdd_advance` for the next phase. The extension asks the user for explicit approval and rejects skipped transitions. Before `tests`, built-in write/edit tools are limited to `.hwcode/specs/`; during `tests`, they may also create test files. Production-file changes begin only in `implementation`.
15
+
14
16
  Use the locked root as the only project workspace. Use in-root paths silently. Explain a genuine external-path need and rely on the one-call approval prompt; never bypass it.
15
17
 
16
18
  Do not implement production behavior until the user has explicitly approved the requirements and then the design, test plan, and task plan. If later evidence exposes ambiguity, return to the earliest affected artifact and obtain approval again.
package/README.md CHANGED
@@ -106,6 +106,13 @@ iterative build-and-verify loops. `/hwcode-sdd` additionally requires the curren
106
106
  to be the Git repository root, inventories the codebase, resolves requirement
107
107
  questions, and persists approved artifacts under
108
108
  `.hwcode/specs/<requirement-slug>/` before test-first implementation begins.
109
+ SDD phases are persisted and advance one step at a time through an interactive
110
+ approval gate. Before the tests phase, built-in file writes are restricted to
111
+ the spec directory; production writes begin only in implementation.
112
+
113
+ Use `/hwcode` to inspect, complete, or cancel the active workflow. Completing
114
+ SDD is allowed only after verification. Cloud completion warns about any
115
+ workflow-created resources that remain active in its resource inventory.
109
116
 
110
117
  ### Cloud workflow
111
118
 
@@ -136,15 +143,31 @@ confirmation unless the user approves remaining non-delete changes for the
136
143
  session. Resource deletion is always confirmed. After three genuinely distinct
137
144
  technical approaches fail, the workflow stops and reports causes, progress,
138
145
  remaining resources, and local changes instead of attempting a fourth approach.
146
+ Read-only inspection and confirmed cleanup deletion remain available after the
147
+ failure budget is reached.
148
+
149
+ Terraform execution uses a pinned SSH Runner connection. New and existing hosts
150
+ share the same OpenSSH SHA256 host-key confirmation path; private targets can be
151
+ scanned through a verified ProxyJump. Runner execution requires Terraform,
152
+ `tar`, `sha256sum`, a declared cloud workload identity, and an explicit non-local
153
+ Terraform backend. The exact policy-checked file manifest is uploaded and its
154
+ per-file digest is verified remotely before execution.
139
155
 
140
156
  After at least one cloud command succeeds, run
141
157
  `/hwcode-cloud-save-template [name]` to save the objective, validated execution
142
158
  sequence, failed approaches, prerequisites, and optional lessons learned as a
143
- local Prompt Template. Each template records its user-facing name plus creation
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
144
163
  and update timestamps. Its prompt is layered into stable intent, prerequisites,
145
164
  the preferred known-good path, expensive failed paths that must not be retried,
146
- and the guarded execution contract. Templates are stored under
147
- `~/.hwcode/cloud/prompts/` with user-only permissions. Credential values are
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
148
171
  excluded and redacted before steps are persisted.
149
172
 
150
173
  Use `/hwcode-cloud-template [additional instructions]` to select and start a
@@ -161,14 +184,24 @@ Cloud approval guards.
161
184
  ## Internal architecture
162
185
 
163
186
  Extensions under `.pi/extensions/` are Pi-facing adapters: they register events,
164
- commands, tools, and UI. Reusable behavior lives under `.pi/lib/`:
165
-
166
- - `runtime/` owns layered/replacing configuration and session-state primitives.
167
- - `workflows/` owns the shared Vibe/SDD/Cloud lifecycle schema.
187
+ commands, tools, and UI. `extensions/workflows.ts` is the single Workflow entry;
188
+ Vibe/SDD live in `extensions/workflows/vibe-sdd.ts`, while the Cloud adapter is
189
+ split under `extensions/workflows/cloud/` into activation, commands, events,
190
+ Provider tools, Runner tools, Terraform tools, shared UI, and session runtime.
191
+ Reusable behavior lives under `.pi/lib/`:
192
+
193
+ - `runtime/` owns layered/replacing configuration, canonical runtime paths, and session-state primitives.
194
+ - `workflows/` owns the shared lifecycle schema; `workflows/cloud/` contains the complete Cloud workflow domain, including its `remote/` and `terraform/` execution components.
168
195
  - `workspace/` owns tool and command path-boundary decisions.
169
- - `cloud/` owns provider adapters, isolated processes, and prompt templates.
170
196
  - `context/` and `models/` own compaction and provider-configuration policy.
171
197
 
198
+ Runtime data is separated by ownership: project SDD specifications live under
199
+ `.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.
204
+
172
205
  `settings.json` is layered as defaults → profile → project for settings such as
173
206
  context and hidden commands. `welcome.json` uses a single replacing resource.
174
207
  `model-providers.json` is deliberately package-scoped so a project cannot
package/bin/hwcode.js CHANGED
@@ -57,7 +57,8 @@ HWCode 交互命令:
57
57
  /hwcode-sdd [需求] 启动测试优先的 Spec-Driven workflow
58
58
  /hwcode-cloud [需求] 启动凭据隔离、变更审批的云部署 workflow
59
59
  /hwcode-cloud-template [补充] 从本地成功模板启动 Cloud workflow
60
- /hwcode-cloud-save-template 将成功执行路径保存为本地 Prompt Template
60
+ /hwcode-cloud-save-template 保存或更新 Prompt/Terraform Deployment Template
61
+ /hwcode 查看、完成或取消当前 workflow
61
62
  /cd <目录> 持久切换当前会话工作目录
62
63
  /model 选择模型
63
64
  /login 登录或配置模型提供商
@@ -104,17 +105,12 @@ const piManagementCommands = new Set([
104
105
  ]);
105
106
 
106
107
  const profileArgs = [
107
- "--tui-mode",
108
- "fullscreen",
109
- "--thinking",
110
- "medium",
111
108
  "--append-system-prompt",
112
109
  join(profileDirectory, "APPEND_SYSTEM.md"),
113
110
  ];
114
111
 
115
112
  for (const extension of [
116
113
  "command-filter.ts",
117
- "cloud.ts",
118
114
  "context-policy.ts",
119
115
  "cwd.ts",
120
116
  "footer-tps.ts",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hadooppei/hwcode",
3
- "version": "0.2.4",
3
+ "version": "1.0.0",
4
4
  "description": "A customizable terminal coding agent with local-model support and HWCode workflows.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,6 +14,7 @@
14
14
  ".pi/extensions",
15
15
  ".pi/lib",
16
16
  "!.pi/lib/*.test.ts",
17
+ "!.pi/lib/**/*.test.ts",
17
18
  ".pi/model-providers.json",
18
19
  ".pi/skills/hwcode-sdd",
19
20
  ".pi/skills/hwcode-vibe",
@@ -24,9 +25,10 @@
24
25
  "scripts": {
25
26
  "start": "node --env-file-if-exists=.env ./bin/hwcode.js",
26
27
  "pi": "node --env-file-if-exists=.env ./bin/hwcode.js",
27
- "test": "node --test .pi/lib/*.test.ts",
28
+ "test": "node scripts/run-tests.mjs",
29
+ "typecheck": "tsc -p tsconfig.json",
28
30
  "test:workflows": "npm test",
29
- "prepack": "npm test && node scripts/audit-package.mjs"
31
+ "prepack": "npm run typecheck && npm test && node scripts/audit-package.mjs"
30
32
  },
31
33
  "engines": {
32
34
  "node": ">=22.19.0"
@@ -48,5 +50,8 @@
48
50
  "@earendil-works/pi-ai": "0.84.2",
49
51
  "@earendil-works/pi-coding-agent": "0.84.2",
50
52
  "@earendil-works/pi-tui": "0.84.2"
53
+ },
54
+ "devDependencies": {
55
+ "typescript": "^5.9.2"
51
56
  }
52
57
  }