@hadooppei/hwcode 0.2.1 → 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.
- package/.pi/extensions/hwcode.ts +35 -2
- package/.pi/extensions/model-providers.ts +1 -2
- package/.pi/extensions/workflows/cloud/activation.ts +235 -0
- package/.pi/extensions/workflows/cloud/commands.ts +96 -0
- package/.pi/extensions/workflows/cloud/events.ts +58 -0
- package/.pi/extensions/workflows/cloud/index.ts +17 -0
- package/.pi/extensions/workflows/cloud/provider-tools.ts +127 -0
- package/.pi/extensions/workflows/cloud/runner-tools.ts +129 -0
- package/.pi/extensions/workflows/cloud/runtime.ts +97 -0
- package/.pi/extensions/workflows/cloud/shared.ts +213 -0
- package/.pi/extensions/workflows/cloud/terraform-tools.ts +126 -0
- package/.pi/extensions/workflows/vibe-sdd.ts +300 -0
- package/.pi/extensions/workflows.ts +6 -253
- package/.pi/lib/runtime/config.ts +3 -7
- package/.pi/lib/runtime/defaults.ts +20 -0
- package/.pi/lib/runtime/paths.ts +68 -0
- package/.pi/lib/runtime/session-state.ts +0 -34
- package/.pi/lib/workflow-guard.ts +8 -1
- package/.pi/lib/{cloud → workflows/cloud}/adapters.ts +7 -6
- package/.pi/lib/workflows/cloud/bundles.ts +358 -0
- package/.pi/lib/workflows/cloud/execution.ts +28 -0
- package/.pi/lib/{cloud → workflows/cloud}/process.ts +5 -3
- package/.pi/lib/workflows/cloud/remote/bootstrap.ts +23 -0
- package/.pi/lib/workflows/cloud/remote/connect.ts +83 -0
- package/.pi/lib/workflows/cloud/remote/host-key.ts +35 -0
- package/.pi/lib/workflows/cloud/remote/profiles.ts +100 -0
- package/.pi/lib/workflows/cloud/remote/ssh-transport.ts +104 -0
- package/.pi/lib/workflows/cloud/remote/workspace.ts +109 -0
- package/.pi/lib/workflows/cloud/template-save.ts +108 -0
- package/.pi/lib/workflows/cloud/templates.ts +256 -0
- package/.pi/lib/workflows/cloud/terraform/plan.ts +109 -0
- package/.pi/lib/workflows/cloud/terraform/policy.ts +36 -0
- package/.pi/lib/workflows/cloud/terraform/runner.ts +64 -0
- package/.pi/lib/{cloud-vault.ts → workflows/cloud/vault.ts} +26 -24
- package/.pi/lib/workflows/cloud/workspace.ts +37 -0
- package/.pi/lib/workflows/sdd.ts +11 -0
- package/.pi/lib/workflows/state.ts +165 -1
- package/.pi/lib/working-directory.ts +0 -58
- package/.pi/lib/workspace/access-policy.ts +2 -2
- package/.pi/skills/hwcode-cloud/SKILL.md +32 -1
- package/.pi/skills/hwcode-sdd/SKILL.md +2 -0
- package/README.md +52 -12
- package/bin/hwcode.js +2 -6
- package/package.json +8 -3
- package/.pi/extensions/cloud.ts +0 -587
- package/.pi/lib/cloud/templates.ts +0 -148
- /package/.pi/lib/{cloud-providers.ts → workflows/cloud/providers.ts} +0 -0
|
@@ -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,6 +29,56 @@ 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
|
+
|
|
41
|
+
export interface CloudTemplateSource {
|
|
42
|
+
id: string;
|
|
43
|
+
name: string;
|
|
44
|
+
createdAt: string;
|
|
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;
|
|
80
|
+
}
|
|
81
|
+
|
|
24
82
|
export interface CloudWorkflowDetails {
|
|
25
83
|
vendor: "aws" | "azure" | "gcp" | "huawei" | "alibaba" | "tencent";
|
|
26
84
|
deployCurrentProject: boolean;
|
|
@@ -28,7 +86,15 @@ export interface CloudWorkflowDetails {
|
|
|
28
86
|
allowNonDeleteChanges: boolean;
|
|
29
87
|
failedApproaches: FailedApproach[];
|
|
30
88
|
successfulSteps: CloudExecutionStep[];
|
|
89
|
+
resources?: CloudResourceRecord[];
|
|
31
90
|
terminalFailure: boolean;
|
|
91
|
+
artifactDirectory?: string;
|
|
92
|
+
templateGuidance?: string;
|
|
93
|
+
sourceTemplate?: CloudTemplateSource;
|
|
94
|
+
terraformSourcePath?: string;
|
|
95
|
+
runner?: CloudRunnerSummary;
|
|
96
|
+
runnerPreference?: "automatic" | "deferred" | "existing";
|
|
97
|
+
terraformRun?: TerraformRunState;
|
|
32
98
|
}
|
|
33
99
|
|
|
34
100
|
export interface WorkflowState {
|
|
@@ -40,6 +106,7 @@ export interface WorkflowState {
|
|
|
40
106
|
activatedAt: string;
|
|
41
107
|
updatedAt: string;
|
|
42
108
|
details?: CloudWorkflowDetails;
|
|
109
|
+
sdd?: SddWorkflowProgress;
|
|
43
110
|
reason?: string;
|
|
44
111
|
}
|
|
45
112
|
|
|
@@ -52,6 +119,47 @@ interface LegacyWorkflowState {
|
|
|
52
119
|
deactivatedAt?: string;
|
|
53
120
|
}
|
|
54
121
|
|
|
122
|
+
function isCloudTemplateSource(value: unknown): value is CloudTemplateSource {
|
|
123
|
+
if (!value || typeof value !== "object") return false;
|
|
124
|
+
const data = value as Record<string, unknown>;
|
|
125
|
+
return typeof data.id === "string"
|
|
126
|
+
&& typeof data.name === "string"
|
|
127
|
+
&& typeof data.createdAt === "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))));
|
|
161
|
+
}
|
|
162
|
+
|
|
55
163
|
function isCloudDetails(value: unknown): value is CloudWorkflowDetails {
|
|
56
164
|
if (!value || typeof value !== "object") return false;
|
|
57
165
|
const data = value as Record<string, unknown>;
|
|
@@ -61,7 +169,22 @@ function isCloudDetails(value: unknown): value is CloudWorkflowDetails {
|
|
|
61
169
|
&& typeof data.allowNonDeleteChanges === "boolean"
|
|
62
170
|
&& Array.isArray(data.failedApproaches)
|
|
63
171
|
&& Array.isArray(data.successfulSteps)
|
|
64
|
-
&&
|
|
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
|
+
})))
|
|
180
|
+
&& typeof data.terminalFailure === "boolean"
|
|
181
|
+
&& (data.artifactDirectory === undefined || typeof data.artifactDirectory === "string")
|
|
182
|
+
&& (data.templateGuidance === undefined || typeof data.templateGuidance === "string")
|
|
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));
|
|
65
188
|
}
|
|
66
189
|
|
|
67
190
|
export function decodeWorkflowState(value: unknown): WorkflowState | undefined {
|
|
@@ -75,6 +198,11 @@ export function decodeWorkflowState(value: unknown): WorkflowState | undefined {
|
|
|
75
198
|
|| typeof data.activatedAt !== "string"
|
|
76
199
|
|| typeof data.updatedAt !== "string") return undefined;
|
|
77
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
|
+
}
|
|
78
206
|
return data as unknown as WorkflowState;
|
|
79
207
|
}
|
|
80
208
|
|
|
@@ -121,6 +249,42 @@ export function createWorkflowState(
|
|
|
121
249
|
phase,
|
|
122
250
|
activatedAt: now,
|
|
123
251
|
updatedAt: now,
|
|
252
|
+
...(mode === "sdd" ? { sdd: { phase: "discovery", approvals: [] } } : {}),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function createCloudWorkflowState(
|
|
257
|
+
root: string,
|
|
258
|
+
vendor: CloudWorkflowDetails["vendor"],
|
|
259
|
+
deployCurrentProject: boolean,
|
|
260
|
+
request: string,
|
|
261
|
+
template?: { source: CloudTemplateSource; guidance: string },
|
|
262
|
+
artifactDirectory?: string,
|
|
263
|
+
): WorkflowState {
|
|
264
|
+
const now = new Date().toISOString();
|
|
265
|
+
return {
|
|
266
|
+
version: 2,
|
|
267
|
+
status: "active",
|
|
268
|
+
mode: "cloud",
|
|
269
|
+
root,
|
|
270
|
+
phase: "connected",
|
|
271
|
+
activatedAt: now,
|
|
272
|
+
updatedAt: now,
|
|
273
|
+
details: {
|
|
274
|
+
vendor,
|
|
275
|
+
deployCurrentProject,
|
|
276
|
+
request,
|
|
277
|
+
allowNonDeleteChanges: false,
|
|
278
|
+
failedApproaches: [],
|
|
279
|
+
successfulSteps: [],
|
|
280
|
+
resources: [],
|
|
281
|
+
terminalFailure: false,
|
|
282
|
+
...(artifactDirectory ? { artifactDirectory } : {}),
|
|
283
|
+
...(template ? {
|
|
284
|
+
templateGuidance: template.guidance,
|
|
285
|
+
sourceTemplate: template.source,
|
|
286
|
+
} : {}),
|
|
287
|
+
},
|
|
124
288
|
};
|
|
125
289
|
}
|
|
126
290
|
|
|
@@ -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
|
}
|
|
@@ -2,7 +2,7 @@ import { homedir, tmpdir } from "node:os";
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
findExternalPathReferences,
|
|
5
|
-
|
|
5
|
+
isPathAllowedWithoutApproval,
|
|
6
6
|
resolveToolPath,
|
|
7
7
|
type ExternalPathReference,
|
|
8
8
|
} from "../workflow-guard.ts";
|
|
@@ -21,7 +21,7 @@ export function evaluateToolPathAccess(
|
|
|
21
21
|
): ExternalPathReference[] {
|
|
22
22
|
if (FILE_PATH_TOOLS.has(request.toolName) && typeof request.input.path === "string") {
|
|
23
23
|
const resolved = resolveToolPath(root, request.input.path, homedir());
|
|
24
|
-
return
|
|
24
|
+
return isPathAllowedWithoutApproval(root, resolved)
|
|
25
25
|
? []
|
|
26
26
|
: [{ raw: request.input.path, resolved }];
|
|
27
27
|
}
|
|
@@ -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
|
|
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
|
@@ -100,11 +100,19 @@ Start one of the project workflows from the Pi input:
|
|
|
100
100
|
|
|
101
101
|
Both commands first confirm the current directory and lock project work to that
|
|
102
102
|
root for the session. In-root operations run normally; each tool call that names
|
|
103
|
-
an external path asks for separate approval.
|
|
104
|
-
|
|
103
|
+
an external path asks for separate approval. The non-storage sink `/dev/null` is
|
|
104
|
+
approval-free even when used by shell redirection. `/hwcode-vibe` uses short
|
|
105
|
+
iterative build-and-verify loops. `/hwcode-sdd` additionally requires the current directory
|
|
105
106
|
to be the Git repository root, inventories the codebase, resolves requirement
|
|
106
107
|
questions, and persists approved artifacts under
|
|
107
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.
|
|
108
116
|
|
|
109
117
|
### Cloud workflow
|
|
110
118
|
|
|
@@ -135,16 +143,38 @@ confirmation unless the user approves remaining non-delete changes for the
|
|
|
135
143
|
session. Resource deletion is always confirmed. After three genuinely distinct
|
|
136
144
|
technical approaches fail, the workflow stops and reports causes, progress,
|
|
137
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.
|
|
138
155
|
|
|
139
156
|
After at least one cloud command succeeds, run
|
|
140
157
|
`/hwcode-cloud-save-template [name]` to save the objective, validated execution
|
|
141
|
-
sequence, and optional lessons learned as a
|
|
142
|
-
|
|
143
|
-
|
|
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.
|
|
144
172
|
|
|
145
173
|
Use `/hwcode-cloud-template [additional instructions]` to select and start a
|
|
146
|
-
saved template
|
|
147
|
-
starts without an inline request.
|
|
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
|
|
148
178
|
Markdown/frontmatter format, but HWCode intentionally keeps them out of Pi
|
|
149
179
|
resource discovery so each saved template does not become another slash
|
|
150
180
|
command. Templates are extension-private resources and always run through the
|
|
@@ -154,14 +184,24 @@ Cloud approval guards.
|
|
|
154
184
|
## Internal architecture
|
|
155
185
|
|
|
156
186
|
Extensions under `.pi/extensions/` are Pi-facing adapters: they register events,
|
|
157
|
-
commands, tools, and UI.
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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.
|
|
161
195
|
- `workspace/` owns tool and command path-boundary decisions.
|
|
162
|
-
- `cloud/` owns provider adapters, isolated processes, and prompt templates.
|
|
163
196
|
- `context/` and `models/` own compaction and provider-configuration policy.
|
|
164
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
|
+
|
|
165
205
|
`settings.json` is layered as defaults → profile → project for settings such as
|
|
166
206
|
context and hidden commands. `welcome.json` uses a single replacing resource.
|
|
167
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
|
|
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.
|
|
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
|
|
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
|
}
|