@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.
Files changed (47) 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/workflow-guard.ts +8 -1
  19. package/.pi/lib/{cloud → workflows/cloud}/adapters.ts +7 -6
  20. package/.pi/lib/workflows/cloud/bundles.ts +358 -0
  21. package/.pi/lib/workflows/cloud/execution.ts +28 -0
  22. package/.pi/lib/{cloud → workflows/cloud}/process.ts +5 -3
  23. package/.pi/lib/workflows/cloud/remote/bootstrap.ts +23 -0
  24. package/.pi/lib/workflows/cloud/remote/connect.ts +83 -0
  25. package/.pi/lib/workflows/cloud/remote/host-key.ts +35 -0
  26. package/.pi/lib/workflows/cloud/remote/profiles.ts +100 -0
  27. package/.pi/lib/workflows/cloud/remote/ssh-transport.ts +104 -0
  28. package/.pi/lib/workflows/cloud/remote/workspace.ts +109 -0
  29. package/.pi/lib/workflows/cloud/template-save.ts +108 -0
  30. package/.pi/lib/workflows/cloud/templates.ts +256 -0
  31. package/.pi/lib/workflows/cloud/terraform/plan.ts +109 -0
  32. package/.pi/lib/workflows/cloud/terraform/policy.ts +36 -0
  33. package/.pi/lib/workflows/cloud/terraform/runner.ts +64 -0
  34. package/.pi/lib/{cloud-vault.ts → workflows/cloud/vault.ts} +26 -24
  35. package/.pi/lib/workflows/cloud/workspace.ts +37 -0
  36. package/.pi/lib/workflows/sdd.ts +11 -0
  37. package/.pi/lib/workflows/state.ts +165 -1
  38. package/.pi/lib/working-directory.ts +0 -58
  39. package/.pi/lib/workspace/access-policy.ts +2 -2
  40. package/.pi/skills/hwcode-cloud/SKILL.md +32 -1
  41. package/.pi/skills/hwcode-sdd/SKILL.md +2 -0
  42. package/README.md +52 -12
  43. package/bin/hwcode.js +2 -6
  44. package/package.json +8 -3
  45. package/.pi/extensions/cloud.ts +0 -587
  46. package/.pi/lib/cloud/templates.ts +0 -148
  47. /package/.pi/lib/{cloud-providers.ts → workflows/cloud/providers.ts} +0 -0
@@ -0,0 +1,300 @@
1
+ import type {
2
+ ExtensionAPI,
3
+ ExtensionCommandContext,
4
+ ExtensionContext,
5
+ } from "@earendil-works/pi-coding-agent";
6
+ import { Type } from "@earendil-works/pi-ai";
7
+ import { defineTool } from "@earendil-works/pi-coding-agent";
8
+ import { isAbsolute, relative, resolve } from "node:path";
9
+
10
+ import {
11
+ canonicalizeWorkspaceRoot,
12
+ } from "../../lib/workflow-guard.ts";
13
+ import { getWorkingDirectory } from "../../lib/working-directory.ts";
14
+ import {
15
+ WORKFLOW_EXTERNAL_AUDIT_TYPE,
16
+ WORKFLOW_STATE_TYPE,
17
+ activeWorkflow,
18
+ createWorkflowState,
19
+ updateWorkflowState,
20
+ workflowLabel,
21
+ type WorkflowMode,
22
+ type WorkflowState,
23
+ } from "../../lib/workflows/state.ts";
24
+ import { advanceSddProgress, SDD_PHASES } from "../../lib/workflows/sdd.ts";
25
+ import { evaluateToolPathAccess } from "../../lib/workspace/access-policy.ts";
26
+ import { PROJECT_SDD_SPECS_RELATIVE } from "../../lib/runtime/paths.ts";
27
+
28
+ interface GitState {
29
+ initialized: boolean;
30
+ dirty: boolean;
31
+ status: string;
32
+ }
33
+
34
+ function modeLabel(mode: WorkflowMode): string {
35
+ return workflowLabel(mode);
36
+ }
37
+
38
+ function notify(ctx: ExtensionContext, message: string, level: "info" | "warning" | "error" = "info"): void {
39
+ if (ctx.hasUI) ctx.ui.notify(message, level);
40
+ }
41
+
42
+ async function confirmWorkspace(mode: WorkflowMode, root: string, ctx: ExtensionCommandContext): Promise<boolean> {
43
+ if (!ctx.hasUI) return false;
44
+ return ctx.ui.confirm(
45
+ `Start ${modeLabel(mode)}?`,
46
+ [
47
+ `Project root: ${root}`,
48
+ "",
49
+ "All project work in this session will be limited to this root.",
50
+ "Paths inside it are allowed silently. Each external-path tool call requires separate approval.",
51
+ ].join("\n"),
52
+ );
53
+ }
54
+
55
+ async function inspectOrInitializeGit(
56
+ pi: ExtensionAPI,
57
+ root: string,
58
+ ctx: ExtensionCommandContext,
59
+ ): Promise<GitState | undefined> {
60
+ const gitVersion = await pi.exec("git", ["--version"], { cwd: root });
61
+ if (gitVersion.code !== 0) {
62
+ notify(ctx, "HWCode SDD requires Git, but the git executable is unavailable.", "error");
63
+ return undefined;
64
+ }
65
+
66
+ let initialized = false;
67
+ let topLevel = await pi.exec("git", ["rev-parse", "--show-toplevel"], { cwd: root });
68
+ if (topLevel.code !== 0) {
69
+ if (!ctx.hasUI || !(await ctx.ui.confirm(
70
+ "Initialize Git repository?",
71
+ `HWCode SDD requires the current directory to be a Git repository root.\n\nRun git init in:\n${root}`,
72
+ ))) return undefined;
73
+
74
+ const init = await pi.exec("git", ["init"], { cwd: root });
75
+ if (init.code !== 0) {
76
+ notify(ctx, `Git initialization failed: ${init.stderr.trim() || init.stdout.trim()}`, "error");
77
+ return undefined;
78
+ }
79
+ initialized = true;
80
+ topLevel = await pi.exec("git", ["rev-parse", "--show-toplevel"], { cwd: root });
81
+ }
82
+
83
+ if (topLevel.code !== 0) {
84
+ notify(ctx, "Unable to determine the Git repository root.", "error");
85
+ return undefined;
86
+ }
87
+
88
+ const gitRoot = canonicalizeWorkspaceRoot(topLevel.stdout.trim());
89
+ if (gitRoot !== root) {
90
+ notify(
91
+ ctx,
92
+ `HWCode SDD must start at the Git repository root. Restart it from: ${gitRoot}`,
93
+ "error",
94
+ );
95
+ return undefined;
96
+ }
97
+
98
+ const statusResult = await pi.exec(
99
+ "git",
100
+ ["status", "--porcelain=v1", "--untracked-files=all"],
101
+ { cwd: root },
102
+ );
103
+ if (statusResult.code !== 0) {
104
+ notify(ctx, `Unable to inspect Git status: ${statusResult.stderr.trim()}`, "error");
105
+ return undefined;
106
+ }
107
+
108
+ const status = statusResult.stdout.trim();
109
+ return { initialized, dirty: status.length > 0, status };
110
+ }
111
+
112
+ function activationPrompt(mode: WorkflowMode, root: string, git: GitState | undefined, request: string): string {
113
+ const context = [
114
+ `The ${modeLabel(mode)} workflow has been activated by its project command.`,
115
+ `Locked project root: ${root}`,
116
+ ];
117
+ if (git) {
118
+ context.push(
119
+ `Git repository: ${git.initialized ? "initialized now" : "already present"}; working tree: ${git.dirty ? "has changes" : "clean"}.`,
120
+ );
121
+ if (git.status) context.push(`Current porcelain status:\n${git.status}`);
122
+ }
123
+ if (request) context.push(`Initial user request:\n${request}`);
124
+ return `/skill:hwcode-${mode} ${context.join("\n\n")}`;
125
+ }
126
+
127
+ function workflowSystemPrompt(state: WorkflowState): string {
128
+ const common = [
129
+ `HWCODE WORKFLOW ACTIVE: ${state.mode.toUpperCase()}`,
130
+ `The sole project root for this session is ${state.root}.`,
131
+ "Keep all reads, writes, commands, generated files, and project work inside that root by default.",
132
+ "An external-path approval applies only to the exact tool call that requested it. Never evade the guard through indirection, symlinks, subprocesses, alternate tools, or encoded commands.",
133
+ "If external storage such as /tmp is genuinely useful, explain why and let the tool-call approval request obtain user consent.",
134
+ ];
135
+ if (state.mode === "sdd") {
136
+ common.push(
137
+ "Follow the SDD phase gates. Do not implement production behavior before requirements, design, test plan, and tasks are complete and explicitly approved.",
138
+ "Develop tests before implementation with a red-green-refactor loop. Return to specification when expected behavior or a test scenario is uncertain.",
139
+ "Never create a Git commit unless the user explicitly approves that commit.",
140
+ );
141
+ }
142
+ return common.join("\n");
143
+ }
144
+
145
+ export function registerVibeSddWorkflows(pi: ExtensionAPI) {
146
+ let activeState: WorkflowState | undefined;
147
+
148
+ async function activate(mode: WorkflowMode, args: string, ctx: ExtensionCommandContext): Promise<void> {
149
+ if (!ctx.isIdle()) {
150
+ notify(ctx, `Wait for the current response to finish before starting ${modeLabel(mode)}.`, "warning");
151
+ return;
152
+ }
153
+
154
+ const root = canonicalizeWorkspaceRoot(getWorkingDirectory(ctx.sessionManager));
155
+ const existing = activeWorkflow(ctx.sessionManager.getEntries());
156
+ if (existing) {
157
+ notify(
158
+ ctx,
159
+ `${workflowLabel(existing.mode)} is already active for ${existing.root}. Start a new session before activating another workflow.`,
160
+ "warning",
161
+ );
162
+ return;
163
+ }
164
+ if (!(await confirmWorkspace(mode, root, ctx))) {
165
+ notify(ctx, `${modeLabel(mode)} was not started.`, "warning");
166
+ return;
167
+ }
168
+
169
+ const git = mode === "sdd" ? await inspectOrInitializeGit(pi, root, ctx) : undefined;
170
+ if (mode === "sdd" && !git) return;
171
+
172
+ activeState = createWorkflowState(mode as "vibe" | "sdd", root);
173
+ pi.appendEntry<WorkflowState>(WORKFLOW_STATE_TYPE, activeState);
174
+ notify(ctx, `${modeLabel(mode)} activated. Project root locked to ${root}.`);
175
+ pi.sendUserMessage(activationPrompt(mode, root, git, args.trim()), { expandPromptTemplates: true });
176
+ }
177
+
178
+ pi.registerCommand("hwcode-vibe", {
179
+ description: "Start the directory-locked HWCode Vibe workflow",
180
+ handler: async (args, ctx) => activate("vibe", args, ctx),
181
+ });
182
+
183
+ pi.registerCommand("hwcode-sdd", {
184
+ description: "Start the Git-root-locked HWCode spec-driven workflow",
185
+ handler: async (args, ctx) => activate("sdd", args, ctx),
186
+ });
187
+
188
+ pi.registerTool(defineTool({
189
+ name: "hwcode_sdd_advance",
190
+ label: "HWCode SDD Phase Gate",
191
+ description: "Advance the active SDD workflow by exactly one explicitly approved phase.",
192
+ promptSnippet: "Use the SDD phase gate after presenting the completed artifact and receiving user approval.",
193
+ parameters: Type.Object({ nextPhase: Type.Union(SDD_PHASES.slice(1).map((phase) => Type.Literal(phase))), evidence: Type.String({ description: "Concise artifact and approval summary shown to the user" }) }),
194
+ executionMode: "sequential",
195
+ async execute(_id, params, _signal, _onUpdate, ctx) {
196
+ const state = activeWorkflow(ctx.sessionManager.getEntries());
197
+ if (!state || state.mode !== "sdd" || !state.sdd) return { content: [{ type: "text", text: "No active HWCode SDD workflow." }], isError: true, details: {} };
198
+ const evidence = params.evidence.replace(/[\r\n]+/gu, " ").trim().slice(0, 1_000);
199
+ let progress;
200
+ try { progress = advanceSddProgress(state.sdd, params.nextPhase, evidence); }
201
+ catch (error) { return { content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], isError: true, details: {} }; }
202
+ if (!ctx.hasUI || !await ctx.ui.confirm(`批准进入 SDD ${params.nextPhase} 阶段?`, evidence)) return { content: [{ type: "text", text: "SDD phase advancement was not approved." }], isError: true, details: {} };
203
+ const updated = updateWorkflowState(state, {
204
+ phase: params.nextPhase,
205
+ sdd: progress,
206
+ });
207
+ activeState = updated;
208
+ pi.appendEntry<WorkflowState>(WORKFLOW_STATE_TYPE, updated);
209
+ return { content: [{ type: "text", text: `SDD advanced to ${params.nextPhase}.` }], details: { phase: params.nextPhase } };
210
+ },
211
+ }));
212
+
213
+ pi.on("session_start", async (_event, ctx) => {
214
+ const restored = activeWorkflow(ctx.sessionManager.getEntries());
215
+ if (!restored) {
216
+ activeState = undefined;
217
+ return;
218
+ }
219
+
220
+ const currentRoot = canonicalizeWorkspaceRoot(getWorkingDirectory(ctx.sessionManager));
221
+ if (currentRoot !== restored.root) {
222
+ activeState = undefined;
223
+ pi.appendEntry<WorkflowState>(WORKFLOW_STATE_TYPE, updateWorkflowState(restored, {
224
+ status: "failed",
225
+ phase: "invalid-root",
226
+ reason: "Stored workflow root no longer matches the current execution directory.",
227
+ }));
228
+ notify(ctx, "Stored HWCode workflow disabled because the current directory changed.", "warning");
229
+ return;
230
+ }
231
+
232
+ activeState = restored;
233
+ notify(ctx, `${modeLabel(restored.mode)} restored. Root: ${restored.root}`);
234
+ });
235
+
236
+ pi.on("before_agent_start", async (event, ctx) => {
237
+ activeState = activeWorkflow(ctx.sessionManager.getEntries());
238
+ if (!activeState) return undefined;
239
+ const sddPhase = activeState.mode === "sdd" && activeState.sdd ? `\nCurrent enforced SDD phase: ${activeState.sdd.phase}. Use hwcode_sdd_advance for the next user-approved transition; never skip a phase.` : "";
240
+ return { systemPrompt: `${event.systemPrompt}\n\n${workflowSystemPrompt(activeState)}${sddPhase}` };
241
+ });
242
+
243
+ pi.on("tool_call", async (event, ctx) => {
244
+ activeState = activeWorkflow(ctx.sessionManager.getEntries());
245
+ if (!activeState) return undefined;
246
+
247
+ const input = event.input as Record<string, unknown>;
248
+ if (activeState.mode === "sdd" && activeState.sdd && ["write", "edit"].includes(event.toolName) && typeof input.path === "string") {
249
+ const path = resolve(activeState.root, input.path);
250
+ const projectRelative = relative(activeState.root, path).split("\\").join("/");
251
+ const isSpecArtifact = projectRelative.startsWith(`${PROJECT_SDD_SPECS_RELATIVE}/`);
252
+ const isTestArtifact = /(?:^|\/)(?:test|tests|__tests__)\/|(?:\.test|\.spec)\.[^/]+$/u.test(projectRelative);
253
+ const phase = activeState.sdd.phase;
254
+ const allowed = ["implementation", "verification"].includes(phase)
255
+ || isSpecArtifact
256
+ || phase === "tests" && isTestArtifact;
257
+ if (!allowed || isAbsolute(projectRelative) || projectRelative.startsWith("..")) {
258
+ return { block: true, reason: `SDD phase ${phase} does not allow production-file changes. Write approved spec artifacts under ${PROJECT_SDD_SPECS_RELATIVE}; test files become writable in the tests phase.` };
259
+ }
260
+ }
261
+ const external = evaluateToolPathAccess({ toolName: event.toolName, input }, activeState.root);
262
+
263
+ if (external.length === 0) return undefined;
264
+ const details = external.map((entry) => `• ${entry.raw} → ${entry.resolved}`).join("\n");
265
+ if (!ctx.hasUI) {
266
+ return {
267
+ block: true,
268
+ reason: `External path requires interactive one-call approval:\n${details}`,
269
+ };
270
+ }
271
+
272
+ const allowed = await ctx.ui.confirm(
273
+ "Allow external path for this call?",
274
+ [
275
+ `${modeLabel(activeState.mode)} is locked to ${activeState.root}.`,
276
+ "This tool call references paths outside that root:",
277
+ "",
278
+ details,
279
+ "",
280
+ "Approval applies only to this tool call.",
281
+ ].join("\n"),
282
+ );
283
+ if (!allowed) {
284
+ return { block: true, reason: `User denied external path access:\n${details}` };
285
+ }
286
+
287
+ pi.appendEntry(WORKFLOW_EXTERNAL_AUDIT_TYPE, {
288
+ mode: activeState.mode,
289
+ root: activeState.root,
290
+ toolName: event.toolName,
291
+ external,
292
+ approvedAt: new Date().toISOString(),
293
+ });
294
+ return undefined;
295
+ });
296
+
297
+ pi.on("session_shutdown", () => {
298
+ activeState = undefined;
299
+ });
300
+ }
@@ -1,256 +1,9 @@
1
- import type {
2
- ExtensionAPI,
3
- ExtensionCommandContext,
4
- ExtensionContext,
5
- } from "@earendil-works/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
6
2
 
7
- import {
8
- canonicalizeWorkspaceRoot,
9
- } from "../lib/workflow-guard.ts";
10
- import { getWorkingDirectory } from "../lib/working-directory.ts";
11
- import {
12
- WORKFLOW_EXTERNAL_AUDIT_TYPE,
13
- WORKFLOW_STATE_TYPE,
14
- activeWorkflow,
15
- createWorkflowState,
16
- updateWorkflowState,
17
- workflowLabel,
18
- type WorkflowMode,
19
- type WorkflowState,
20
- } from "../lib/workflows/state.ts";
21
- import { evaluateToolPathAccess } from "../lib/workspace/access-policy.ts";
3
+ import { registerCloudWorkflow } from "./workflows/cloud/index.ts";
4
+ import { registerVibeSddWorkflows } from "./workflows/vibe-sdd.ts";
22
5
 
23
- interface GitState {
24
- initialized: boolean;
25
- dirty: boolean;
26
- status: string;
27
- }
28
-
29
- function modeLabel(mode: WorkflowMode): string {
30
- return workflowLabel(mode);
31
- }
32
-
33
- function notify(ctx: ExtensionContext, message: string, level: "info" | "warning" | "error" = "info"): void {
34
- if (ctx.hasUI) ctx.ui.notify(message, level);
35
- }
36
-
37
- async function confirmWorkspace(mode: WorkflowMode, root: string, ctx: ExtensionCommandContext): Promise<boolean> {
38
- if (!ctx.hasUI) return false;
39
- return ctx.ui.confirm(
40
- `Start ${modeLabel(mode)}?`,
41
- [
42
- `Project root: ${root}`,
43
- "",
44
- "All project work in this session will be limited to this root.",
45
- "Paths inside it are allowed silently. Each external-path tool call requires separate approval.",
46
- ].join("\n"),
47
- );
48
- }
49
-
50
- async function inspectOrInitializeGit(
51
- pi: ExtensionAPI,
52
- root: string,
53
- ctx: ExtensionCommandContext,
54
- ): Promise<GitState | undefined> {
55
- const gitVersion = await pi.exec("git", ["--version"], { cwd: root });
56
- if (gitVersion.code !== 0) {
57
- notify(ctx, "HWCode SDD requires Git, but the git executable is unavailable.", "error");
58
- return undefined;
59
- }
60
-
61
- let initialized = false;
62
- let topLevel = await pi.exec("git", ["rev-parse", "--show-toplevel"], { cwd: root });
63
- if (topLevel.code !== 0) {
64
- if (!ctx.hasUI || !(await ctx.ui.confirm(
65
- "Initialize Git repository?",
66
- `HWCode SDD requires the current directory to be a Git repository root.\n\nRun git init in:\n${root}`,
67
- ))) return undefined;
68
-
69
- const init = await pi.exec("git", ["init"], { cwd: root });
70
- if (init.code !== 0) {
71
- notify(ctx, `Git initialization failed: ${init.stderr.trim() || init.stdout.trim()}`, "error");
72
- return undefined;
73
- }
74
- initialized = true;
75
- topLevel = await pi.exec("git", ["rev-parse", "--show-toplevel"], { cwd: root });
76
- }
77
-
78
- if (topLevel.code !== 0) {
79
- notify(ctx, "Unable to determine the Git repository root.", "error");
80
- return undefined;
81
- }
82
-
83
- const gitRoot = canonicalizeWorkspaceRoot(topLevel.stdout.trim());
84
- if (gitRoot !== root) {
85
- notify(
86
- ctx,
87
- `HWCode SDD must start at the Git repository root. Restart it from: ${gitRoot}`,
88
- "error",
89
- );
90
- return undefined;
91
- }
92
-
93
- const statusResult = await pi.exec(
94
- "git",
95
- ["status", "--porcelain=v1", "--untracked-files=all"],
96
- { cwd: root },
97
- );
98
- if (statusResult.code !== 0) {
99
- notify(ctx, `Unable to inspect Git status: ${statusResult.stderr.trim()}`, "error");
100
- return undefined;
101
- }
102
-
103
- const status = statusResult.stdout.trim();
104
- return { initialized, dirty: status.length > 0, status };
105
- }
106
-
107
- function activationPrompt(mode: WorkflowMode, root: string, git: GitState | undefined, request: string): string {
108
- const context = [
109
- `The ${modeLabel(mode)} workflow has been activated by its project command.`,
110
- `Locked project root: ${root}`,
111
- ];
112
- if (git) {
113
- context.push(
114
- `Git repository: ${git.initialized ? "initialized now" : "already present"}; working tree: ${git.dirty ? "has changes" : "clean"}.`,
115
- );
116
- if (git.status) context.push(`Current porcelain status:\n${git.status}`);
117
- }
118
- if (request) context.push(`Initial user request:\n${request}`);
119
- return `/skill:hwcode-${mode} ${context.join("\n\n")}`;
120
- }
121
-
122
- function workflowSystemPrompt(state: WorkflowState): string {
123
- const common = [
124
- `HWCODE WORKFLOW ACTIVE: ${state.mode.toUpperCase()}`,
125
- `The sole project root for this session is ${state.root}.`,
126
- "Keep all reads, writes, commands, generated files, and project work inside that root by default.",
127
- "An external-path approval applies only to the exact tool call that requested it. Never evade the guard through indirection, symlinks, subprocesses, alternate tools, or encoded commands.",
128
- "If external storage such as /tmp is genuinely useful, explain why and let the tool-call approval request obtain user consent.",
129
- ];
130
- if (state.mode === "sdd") {
131
- common.push(
132
- "Follow the SDD phase gates. Do not implement production behavior before requirements, design, test plan, and tasks are complete and explicitly approved.",
133
- "Develop tests before implementation with a red-green-refactor loop. Return to specification when expected behavior or a test scenario is uncertain.",
134
- "Never create a Git commit unless the user explicitly approves that commit.",
135
- );
136
- }
137
- return common.join("\n");
138
- }
139
-
140
- export default function (pi: ExtensionAPI) {
141
- let activeState: WorkflowState | undefined;
142
-
143
- async function activate(mode: WorkflowMode, args: string, ctx: ExtensionCommandContext): Promise<void> {
144
- if (!ctx.isIdle()) {
145
- notify(ctx, `Wait for the current response to finish before starting ${modeLabel(mode)}.`, "warning");
146
- return;
147
- }
148
-
149
- const root = canonicalizeWorkspaceRoot(getWorkingDirectory(ctx.sessionManager));
150
- const existing = activeWorkflow(ctx.sessionManager.getEntries());
151
- if (existing) {
152
- notify(
153
- ctx,
154
- `${workflowLabel(existing.mode)} is already active for ${existing.root}. Start a new session before activating another workflow.`,
155
- "warning",
156
- );
157
- return;
158
- }
159
- if (!(await confirmWorkspace(mode, root, ctx))) {
160
- notify(ctx, `${modeLabel(mode)} was not started.`, "warning");
161
- return;
162
- }
163
-
164
- const git = mode === "sdd" ? await inspectOrInitializeGit(pi, root, ctx) : undefined;
165
- if (mode === "sdd" && !git) return;
166
-
167
- activeState = createWorkflowState(mode as "vibe" | "sdd", root);
168
- pi.appendEntry<WorkflowState>(WORKFLOW_STATE_TYPE, activeState);
169
- notify(ctx, `${modeLabel(mode)} activated. Project root locked to ${root}.`);
170
- pi.sendUserMessage(activationPrompt(mode, root, git, args.trim()), { expandPromptTemplates: true });
171
- }
172
-
173
- pi.registerCommand("hwcode-vibe", {
174
- description: "Start the directory-locked HWCode Vibe workflow",
175
- handler: async (args, ctx) => activate("vibe", args, ctx),
176
- });
177
-
178
- pi.registerCommand("hwcode-sdd", {
179
- description: "Start the Git-root-locked HWCode spec-driven workflow",
180
- handler: async (args, ctx) => activate("sdd", args, ctx),
181
- });
182
-
183
- pi.on("session_start", async (_event, ctx) => {
184
- const restored = activeWorkflow(ctx.sessionManager.getEntries());
185
- if (!restored) {
186
- activeState = undefined;
187
- return;
188
- }
189
-
190
- const currentRoot = canonicalizeWorkspaceRoot(getWorkingDirectory(ctx.sessionManager));
191
- if (currentRoot !== restored.root) {
192
- activeState = undefined;
193
- pi.appendEntry<WorkflowState>(WORKFLOW_STATE_TYPE, updateWorkflowState(restored, {
194
- status: "failed",
195
- phase: "invalid-root",
196
- reason: "Stored workflow root no longer matches the current execution directory.",
197
- }));
198
- notify(ctx, "Stored HWCode workflow disabled because the current directory changed.", "warning");
199
- return;
200
- }
201
-
202
- activeState = restored;
203
- notify(ctx, `${modeLabel(restored.mode)} restored. Root: ${restored.root}`);
204
- });
205
-
206
- pi.on("before_agent_start", async (event, ctx) => {
207
- activeState = activeWorkflow(ctx.sessionManager.getEntries());
208
- if (!activeState) return undefined;
209
- return { systemPrompt: `${event.systemPrompt}\n\n${workflowSystemPrompt(activeState)}` };
210
- });
211
-
212
- pi.on("tool_call", async (event, ctx) => {
213
- activeState = activeWorkflow(ctx.sessionManager.getEntries());
214
- if (!activeState) return undefined;
215
-
216
- const input = event.input as Record<string, unknown>;
217
- const external = evaluateToolPathAccess({ toolName: event.toolName, input }, activeState.root);
218
-
219
- if (external.length === 0) return undefined;
220
- const details = external.map((entry) => `• ${entry.raw} → ${entry.resolved}`).join("\n");
221
- if (!ctx.hasUI) {
222
- return {
223
- block: true,
224
- reason: `External path requires interactive one-call approval:\n${details}`,
225
- };
226
- }
227
-
228
- const allowed = await ctx.ui.confirm(
229
- "Allow external path for this call?",
230
- [
231
- `${modeLabel(activeState.mode)} is locked to ${activeState.root}.`,
232
- "This tool call references paths outside that root:",
233
- "",
234
- details,
235
- "",
236
- "Approval applies only to this tool call.",
237
- ].join("\n"),
238
- );
239
- if (!allowed) {
240
- return { block: true, reason: `User denied external path access:\n${details}` };
241
- }
242
-
243
- pi.appendEntry(WORKFLOW_EXTERNAL_AUDIT_TYPE, {
244
- mode: activeState.mode,
245
- root: activeState.root,
246
- toolName: event.toolName,
247
- external,
248
- approvedAt: new Date().toISOString(),
249
- });
250
- return undefined;
251
- });
252
-
253
- pi.on("session_shutdown", () => {
254
- activeState = undefined;
255
- });
6
+ export default function registerWorkflows(pi: ExtensionAPI): void {
7
+ registerVibeSddWorkflows(pi);
8
+ registerCloudWorkflow(pi);
256
9
  }
@@ -1,5 +1,7 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
- import { join, resolve } from "node:path";
2
+ import { resolve } from "node:path";
3
+
4
+ import { profileResourcePath } from "./paths.ts";
3
5
 
4
6
  export type JsonObject = Record<string, unknown>;
5
7
 
@@ -23,12 +25,6 @@ export function deepMerge<T extends JsonObject>(base: T, override: JsonObject):
23
25
  return merged as T;
24
26
  }
25
27
 
26
- export function profileResourcePath(fileName: string): string | undefined {
27
- return process.env.HWCODE_PROFILE_DIR
28
- ? join(process.env.HWCODE_PROFILE_DIR, fileName)
29
- : undefined;
30
- }
31
-
32
28
  export function configLocations(cwd: string, fileName: string): ConfigLocations {
33
29
  return {
34
30
  profile: profileResourcePath(fileName),
@@ -0,0 +1,20 @@
1
+ /** Internal product defaults and safety limits. User-overridable settings stay in settings.json. */
2
+ export const CLOUD_RUNTIME_DEFAULTS = Object.freeze({
3
+ workflow: Object.freeze({ maxFailedApproaches: 3, maxSuccessfulSteps: 100 }),
4
+ process: Object.freeze({
5
+ commandTimeoutMs: 120_000,
6
+ providerValidationTimeoutMs: 30_000,
7
+ forceKillGraceMs: 2_000,
8
+ maxOutputBytes: 50_000,
9
+ }),
10
+ runner: Object.freeze({
11
+ defaultUser: "ubuntu",
12
+ defaultPort: 22,
13
+ keyCandidates: Object.freeze(["id_ed25519", "id_rsa"]),
14
+ hostKeyScanTimeoutMs: 15_000,
15
+ connectTimeoutSeconds: 15,
16
+ commandTimeoutMs: 120_000,
17
+ }),
18
+ templates: Object.freeze({ slugMaxLength: 48, nameMaxLength: 80 }),
19
+ terraform: Object.freeze({ executable: "terraform", planFile: "hwcode.tfplan" }),
20
+ });
@@ -0,0 +1,68 @@
1
+ import { homedir } from "node:os";
2
+ import { join, posix, resolve } from "node:path";
3
+
4
+ export const HWCODE_DATA_DIRECTORY = ".hwcode";
5
+ export const PROJECT_SDD_SPECS_RELATIVE = `${HWCODE_DATA_DIRECTORY}/specs`;
6
+ export const PROJECT_CLOUD_RUNS_RELATIVE = `${HWCODE_DATA_DIRECTORY}/cloud/runs`;
7
+
8
+ export interface ProjectRuntimePaths {
9
+ root: string;
10
+ hwcode: string;
11
+ specs: string;
12
+ cloud: string;
13
+ cloudRuns: string;
14
+ }
15
+
16
+ export interface UserRuntimePaths {
17
+ root: string;
18
+ cloud: string;
19
+ cloudVault: string;
20
+ cloudKnownHosts: string;
21
+ cloudPromptTemplates: string;
22
+ cloudDeploymentTemplates: string;
23
+ }
24
+
25
+ export function projectRuntimePaths(projectRoot: string): ProjectRuntimePaths {
26
+ const root = resolve(projectRoot);
27
+ const hwcode = join(root, HWCODE_DATA_DIRECTORY);
28
+ const cloud = join(hwcode, "cloud");
29
+ return {
30
+ root,
31
+ hwcode,
32
+ specs: join(hwcode, "specs"),
33
+ cloud,
34
+ cloudRuns: join(cloud, "runs"),
35
+ };
36
+ }
37
+
38
+ export function userRuntimePaths(home = homedir()): UserRuntimePaths {
39
+ const root = join(home, HWCODE_DATA_DIRECTORY);
40
+ const cloud = join(root, "cloud");
41
+ return {
42
+ root,
43
+ 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"),
48
+ };
49
+ }
50
+
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
+ export function remoteRunnerPaths(user: string): { root: string; runs: string } {
60
+ const root = posix.join("/home", user, HWCODE_DATA_DIRECTORY);
61
+ return { root, runs: posix.join(root, "runs") };
62
+ }
63
+
64
+ export function profileResourcePath(fileName: string): string | undefined {
65
+ return process.env.HWCODE_PROFILE_DIR
66
+ ? join(process.env.HWCODE_PROFILE_DIR, fileName)
67
+ : undefined;
68
+ }