@stackwright-pro/raft 1.0.0-alpha.13 → 1.0.0-alpha.132
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/LICENSE +21 -0
- package/README.md +23 -1
- package/dist/index.js +763 -97
- package/dist/index.js.map +1 -1
- package/package.json +14 -9
package/dist/index.js
CHANGED
|
@@ -1,44 +1,82 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
-
var __create = Object.create;
|
|
4
|
-
var __defProp = Object.defineProperty;
|
|
5
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
-
var __copyProps = (to, from, except, desc) => {
|
|
10
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
-
for (let key of __getOwnPropNames(from))
|
|
12
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
-
}
|
|
15
|
-
return to;
|
|
16
|
-
};
|
|
17
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
-
mod
|
|
24
|
-
));
|
|
25
3
|
|
|
26
4
|
// src/index.ts
|
|
27
|
-
var
|
|
28
|
-
var
|
|
29
|
-
var
|
|
5
|
+
var import_fs3 = require("fs");
|
|
6
|
+
var import_path3 = require("path");
|
|
7
|
+
var import_child_process3 = require("child_process");
|
|
30
8
|
var import_integrity = require("@stackwright-pro/mcp/integrity");
|
|
9
|
+
var import_type_schemas = require("@stackwright-pro/mcp/type-schemas");
|
|
31
10
|
|
|
32
11
|
// src/lib.ts
|
|
33
12
|
var import_child_process = require("child_process");
|
|
34
13
|
var import_fs = require("fs");
|
|
35
14
|
var import_path = require("path");
|
|
36
15
|
var import_os = require("os");
|
|
16
|
+
var CODE_PUPPY_ENV_ALLOWLIST = /* @__PURE__ */ new Set([
|
|
17
|
+
// Shell / execution environment
|
|
18
|
+
"PATH",
|
|
19
|
+
"HOME",
|
|
20
|
+
"USER",
|
|
21
|
+
"USERNAME",
|
|
22
|
+
"SHELL",
|
|
23
|
+
"TMPDIR",
|
|
24
|
+
"TMP",
|
|
25
|
+
"TEMP",
|
|
26
|
+
// Terminal
|
|
27
|
+
"TERM",
|
|
28
|
+
"COLORTERM",
|
|
29
|
+
"COLUMNS",
|
|
30
|
+
"LINES",
|
|
31
|
+
"NO_COLOR",
|
|
32
|
+
"FORCE_COLOR",
|
|
33
|
+
// Language / locale
|
|
34
|
+
"LANG",
|
|
35
|
+
"LC_ALL",
|
|
36
|
+
"LC_CTYPE",
|
|
37
|
+
"LC_MESSAGES",
|
|
38
|
+
// Network proxy — needed for outbound LLM API calls behind corporate proxies
|
|
39
|
+
"HTTP_PROXY",
|
|
40
|
+
"HTTPS_PROXY",
|
|
41
|
+
"NO_PROXY",
|
|
42
|
+
"http_proxy",
|
|
43
|
+
"https_proxy",
|
|
44
|
+
"no_proxy",
|
|
45
|
+
// LLM API keys — the only credentials code-puppy should have
|
|
46
|
+
"ANTHROPIC_API_KEY",
|
|
47
|
+
"ANTHROPIC_BASE_URL",
|
|
48
|
+
"OPENAI_API_KEY",
|
|
49
|
+
// Node runtime
|
|
50
|
+
"NODE_ENV",
|
|
51
|
+
// Stackwright-specific
|
|
52
|
+
"STACKWRIGHT_PROJECT_ROOT",
|
|
53
|
+
"STACKWRIGHT_SKIP_PREFLIGHT",
|
|
54
|
+
"STACKWRIGHT_CODE_PUPPY_PATH",
|
|
55
|
+
// Telemetry vars (swp-bhw1): NDJSON path is force-injected by the launcher (see index.ts),
|
|
56
|
+
// but listing here allows user override via shell env. DISABLED/DEBUG are opt-in toggles.
|
|
57
|
+
"STACKWRIGHT_TELEMETRY_NDJSON",
|
|
58
|
+
"STACKWRIGHT_TELEMETRY_NDJSON_SUBAGENT_RESPONSES",
|
|
59
|
+
"STACKWRIGHT_TELEMETRY_DISABLED",
|
|
60
|
+
"STACKWRIGHT_TELEMETRY_DEBUG"
|
|
61
|
+
]);
|
|
62
|
+
var CODE_PUPPY_ENV_PREFIXES = ["PYTHON", "LC_"];
|
|
63
|
+
var MIN_SUPPORTED_CODE_PUPPY_VERSION = "0.0.575";
|
|
37
64
|
function parseArgs(argv) {
|
|
38
65
|
const args = {
|
|
39
66
|
projectRoot: process.cwd(),
|
|
40
67
|
verbose: false,
|
|
41
|
-
help: false
|
|
68
|
+
help: false,
|
|
69
|
+
passEnvKeys: [],
|
|
70
|
+
nonInteractive: false,
|
|
71
|
+
devOnly: false,
|
|
72
|
+
useCasePath: null,
|
|
73
|
+
skipQa: false,
|
|
74
|
+
qaOnly: false,
|
|
75
|
+
qaDevServerTimeoutMs: 6e4,
|
|
76
|
+
parallelPhases: false,
|
|
77
|
+
dataflowScheduling: false,
|
|
78
|
+
subagentResponses: false,
|
|
79
|
+
maxConcurrentApiInvocations: 3
|
|
42
80
|
};
|
|
43
81
|
const raw = argv.slice(2);
|
|
44
82
|
for (let i = 0; i < raw.length; i++) {
|
|
@@ -54,7 +92,58 @@ function parseArgs(argv) {
|
|
|
54
92
|
case "-h":
|
|
55
93
|
args.help = true;
|
|
56
94
|
break;
|
|
95
|
+
case "--pass-env":
|
|
96
|
+
args.passEnvKeys.push(raw[++i] ?? die("--pass-env requires a KEY argument"));
|
|
97
|
+
break;
|
|
98
|
+
case "--non-interactive":
|
|
99
|
+
args.nonInteractive = true;
|
|
100
|
+
break;
|
|
101
|
+
case "--dev-only":
|
|
102
|
+
args.devOnly = true;
|
|
103
|
+
break;
|
|
104
|
+
case "--use-case":
|
|
105
|
+
args.useCasePath = raw[++i] ?? die("--use-case requires a file path argument");
|
|
106
|
+
break;
|
|
107
|
+
case "--skip-qa":
|
|
108
|
+
args.skipQa = true;
|
|
109
|
+
break;
|
|
110
|
+
case "--qa-only":
|
|
111
|
+
args.qaOnly = true;
|
|
112
|
+
break;
|
|
113
|
+
case "--parallel-phases":
|
|
114
|
+
args.parallelPhases = true;
|
|
115
|
+
break;
|
|
116
|
+
case "--dataflow-scheduling":
|
|
117
|
+
args.dataflowScheduling = true;
|
|
118
|
+
break;
|
|
119
|
+
case "--subagent-responses":
|
|
120
|
+
args.subagentResponses = true;
|
|
121
|
+
break;
|
|
122
|
+
case "--max-concurrent-api": {
|
|
123
|
+
const val = parseInt(raw[++i] ?? "", 10);
|
|
124
|
+
if (isNaN(val) || val < 1) {
|
|
125
|
+
die("--max-concurrent-api requires a positive integer");
|
|
126
|
+
}
|
|
127
|
+
args.maxConcurrentApiInvocations = val;
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
57
130
|
default:
|
|
131
|
+
if (token.startsWith("--qa-dev-server-timeout-ms=")) {
|
|
132
|
+
const val = parseInt(token.split("=")[1] ?? "", 10);
|
|
133
|
+
if (isNaN(val) || val <= 0) {
|
|
134
|
+
die("--qa-dev-server-timeout-ms requires a positive integer (ms)");
|
|
135
|
+
}
|
|
136
|
+
args.qaDevServerTimeoutMs = val;
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
if (token.startsWith("--max-concurrent-api=")) {
|
|
140
|
+
const val = parseInt(token.split("=")[1] ?? "", 10);
|
|
141
|
+
if (isNaN(val) || val < 1) {
|
|
142
|
+
die("--max-concurrent-api requires a positive integer");
|
|
143
|
+
}
|
|
144
|
+
args.maxConcurrentApiInvocations = val;
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
58
147
|
die(`Unknown option: ${token}
|
|
59
148
|
Run with --help for usage.`);
|
|
60
149
|
}
|
|
@@ -69,9 +158,20 @@ function printHelp() {
|
|
|
69
158
|
Usage: launch-raft [options]
|
|
70
159
|
|
|
71
160
|
Options:
|
|
72
|
-
--project-root <path>
|
|
73
|
-
--verbose
|
|
74
|
-
--
|
|
161
|
+
--project-root <path> Project root directory (default: cwd)
|
|
162
|
+
--verbose Enable verbose logging
|
|
163
|
+
--pass-env <KEY> Forward an additional env var to code-puppy (repeatable)
|
|
164
|
+
--non-interactive Skip all TUI prompts, use sane defaults for questions
|
|
165
|
+
--dev-only Mock auth only \u2014 roles derived from use case, no real providers
|
|
166
|
+
--use-case <file> Seed "what to build" from a file (plain text or markdown)
|
|
167
|
+
--skip-qa Skip the visual QA gate (useful for CI or fast iterations)
|
|
168
|
+
--qa-only Skip the otter pipeline; spawn dev server and run QA directly
|
|
169
|
+
--qa-dev-server-timeout-ms=<n> Dev server startup timeout in ms for QA gate (default: 60000)
|
|
170
|
+
--parallel-phases Enable parallel specialist invocation within dependency waves (~40% wall-time reduction). Default: off (serial).
|
|
171
|
+
--dataflow-scheduling Dissolve wave barriers: fire phases as deps satisfy, not when the whole wave clears. Default: off (wave-parallel mode). (swp-ioc7)
|
|
172
|
+
--subagent-responses Capture full sub-agent response text in raft-puppy NDJSON telemetry (agent_response events). Verbose \u2014 can produce large logs. Default: off.
|
|
173
|
+
--max-concurrent-api <N> Max concurrent api-otter invocations during the api phase fan-out (default: 3). Raise to 8 for 8-spec projects when rate limits allow. (swp-3l9j)
|
|
174
|
+
--help, -h Show this help
|
|
75
175
|
|
|
76
176
|
Prerequisites:
|
|
77
177
|
pip install stackwright-puppy # provides raft-puppy + code-puppy alias
|
|
@@ -91,6 +191,33 @@ function verbose(message, isVerbose) {
|
|
|
91
191
|
console.log(` ${message}`);
|
|
92
192
|
}
|
|
93
193
|
}
|
|
194
|
+
function buildCodePuppyEnv(extraVars = {}, passEnvKeys = []) {
|
|
195
|
+
const env = {};
|
|
196
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
197
|
+
if (value === void 0) continue;
|
|
198
|
+
const allowed = CODE_PUPPY_ENV_ALLOWLIST.has(key) || CODE_PUPPY_ENV_PREFIXES.some((prefix) => key.startsWith(prefix)) || passEnvKeys.includes(key);
|
|
199
|
+
if (allowed) {
|
|
200
|
+
env[key] = value;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
for (const [key, value] of Object.entries(extraVars)) {
|
|
204
|
+
env[key] = value;
|
|
205
|
+
}
|
|
206
|
+
return env;
|
|
207
|
+
}
|
|
208
|
+
function buildLauncherEnvOverrides(projectRoot, options = {}) {
|
|
209
|
+
const overrides = {
|
|
210
|
+
STACKWRIGHT_PROJECT_ROOT: projectRoot,
|
|
211
|
+
// swp-bhw1: route raft-puppy telemetry to a SEPARATE file from Pro telemetry.
|
|
212
|
+
// Two-file separation paves the way for the eventual message-bus architecture
|
|
213
|
+
// (distributed deployments, restart, replay). User can override via shell env.
|
|
214
|
+
STACKWRIGHT_TELEMETRY_NDJSON: (0, import_path.join)(projectRoot, ".stackwright", "raft-puppy-events.ndjson")
|
|
215
|
+
};
|
|
216
|
+
if (options.subagentResponses) {
|
|
217
|
+
overrides["STACKWRIGHT_TELEMETRY_NDJSON_SUBAGENT_RESPONSES"] = "1";
|
|
218
|
+
}
|
|
219
|
+
return overrides;
|
|
220
|
+
}
|
|
94
221
|
var LOCK_FILE = ".stackwright/.lock";
|
|
95
222
|
function acquireLock(projectRoot) {
|
|
96
223
|
const lockPath = (0, import_path.join)(projectRoot, LOCK_FILE);
|
|
@@ -137,6 +264,26 @@ function releaseLock(projectRoot) {
|
|
|
137
264
|
} catch {
|
|
138
265
|
}
|
|
139
266
|
}
|
|
267
|
+
function migrateWorkspaceDir(projectRoot) {
|
|
268
|
+
const oldDir = (0, import_path.join)(projectRoot, ".code-puppy");
|
|
269
|
+
const newDir = (0, import_path.join)(projectRoot, ".code_puppy");
|
|
270
|
+
const oldExists = (0, import_fs.existsSync)(oldDir);
|
|
271
|
+
const newExists = (0, import_fs.existsSync)(newDir);
|
|
272
|
+
if (!oldExists) return;
|
|
273
|
+
if ((0, import_fs.lstatSync)(oldDir).isSymbolicLink()) {
|
|
274
|
+
die(".code-puppy/ is a symlink \u2014 refusing to migrate. Check for tampering.");
|
|
275
|
+
}
|
|
276
|
+
if (newExists && (0, import_fs.lstatSync)(newDir).isSymbolicLink()) {
|
|
277
|
+
die(".code_puppy/ is a symlink \u2014 refusing to migrate. Check for tampering.");
|
|
278
|
+
}
|
|
279
|
+
if (!newExists) {
|
|
280
|
+
(0, import_fs.renameSync)(oldDir, newDir);
|
|
281
|
+
log("Migrated .code-puppy/ \u2192 .code_puppy/ (underscore convention)");
|
|
282
|
+
} else {
|
|
283
|
+
(0, import_fs.rmSync)(oldDir, { recursive: true });
|
|
284
|
+
log("Removed stale .code-puppy/ (underscore version already exists)");
|
|
285
|
+
}
|
|
286
|
+
}
|
|
140
287
|
function writeInitContext(projectRoot) {
|
|
141
288
|
if (!acquireLock(projectRoot)) {
|
|
142
289
|
let existingPid = "unknown";
|
|
@@ -144,7 +291,8 @@ function writeInitContext(projectRoot) {
|
|
|
144
291
|
const lockPath = (0, import_path.join)(projectRoot, LOCK_FILE);
|
|
145
292
|
if ((0, import_fs.existsSync)(lockPath)) {
|
|
146
293
|
const lockData = JSON.parse((0, import_fs.readFileSync)(lockPath, "utf8"));
|
|
147
|
-
|
|
294
|
+
const rawPid = lockData["pid"];
|
|
295
|
+
existingPid = typeof rawPid === "string" || typeof rawPid === "number" ? rawPid : "unknown";
|
|
148
296
|
}
|
|
149
297
|
} catch {
|
|
150
298
|
}
|
|
@@ -232,18 +380,42 @@ process.on("SIGTERM", () => {
|
|
|
232
380
|
}
|
|
233
381
|
process.exit(0);
|
|
234
382
|
});
|
|
235
|
-
function
|
|
383
|
+
function getTrustedBinaryDirs() {
|
|
384
|
+
return [
|
|
385
|
+
(0, import_path.join)((0, import_os.homedir)(), ".local", "bin"),
|
|
386
|
+
// pip user install, uvx, pipx (Linux/macOS)
|
|
387
|
+
"/opt/homebrew/bin",
|
|
388
|
+
// brew Apple Silicon
|
|
389
|
+
"/usr/local/bin",
|
|
390
|
+
// brew Intel, pip system install
|
|
391
|
+
"/usr/bin"
|
|
392
|
+
// system package managers (apt, dnf)
|
|
393
|
+
];
|
|
394
|
+
}
|
|
395
|
+
function findCodePuppy(trustedDirsOverride) {
|
|
236
396
|
let candidate = null;
|
|
237
397
|
const envPath = process.env["STACKWRIGHT_CODE_PUPPY_PATH"];
|
|
238
398
|
if (envPath) {
|
|
239
399
|
candidate = envPath;
|
|
240
400
|
}
|
|
401
|
+
if (!candidate) {
|
|
402
|
+
const trustedDirs = trustedDirsOverride ?? getTrustedBinaryDirs();
|
|
403
|
+
outer: for (const bin of ["raft-puppy", "code-puppy"]) {
|
|
404
|
+
for (const dir of trustedDirs) {
|
|
405
|
+
const p = (0, import_path.join)(dir, bin);
|
|
406
|
+
if ((0, import_fs.existsSync)(p)) {
|
|
407
|
+
candidate = p;
|
|
408
|
+
break outer;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
241
413
|
if (!candidate) {
|
|
242
414
|
for (const bin of ["raft-puppy", "code-puppy"]) {
|
|
243
|
-
|
|
244
|
-
|
|
415
|
+
const whichResult = (0, import_child_process.spawnSync)("which", [bin], { encoding: "utf-8" });
|
|
416
|
+
if (whichResult.status === 0 && whichResult.stdout) {
|
|
417
|
+
candidate = whichResult.stdout.trim();
|
|
245
418
|
if (candidate) break;
|
|
246
|
-
} catch {
|
|
247
419
|
}
|
|
248
420
|
}
|
|
249
421
|
}
|
|
@@ -267,39 +439,116 @@ function findCodePuppy() {
|
|
|
267
439
|
if (!(0, import_fs.existsSync)(realBinary)) {
|
|
268
440
|
die(`raft-puppy symlink at ${resolved} points to a missing file: ${realBinary}`);
|
|
269
441
|
}
|
|
270
|
-
|
|
442
|
+
let fd;
|
|
443
|
+
try {
|
|
444
|
+
fd = (0, import_fs.openSync)(realBinary, "r");
|
|
445
|
+
} catch (err) {
|
|
446
|
+
die(
|
|
447
|
+
`Cannot open raft-puppy at ${realBinary} for validation: ${String(err)}. Try reinstalling stackwright-puppy or set STACKWRIGHT_CODE_PUPPY_PATH.`
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
const stat = (0, import_fs.fstatSync)(fd);
|
|
271
451
|
if (stat.mode & 18) {
|
|
452
|
+
(0, import_fs.closeSync)(fd);
|
|
272
453
|
die(
|
|
273
454
|
`raft-puppy at ${realBinary} is group- or world-writable (mode: ${(stat.mode & 511).toString(8)}) \u2014 refusing to exec.`
|
|
274
455
|
);
|
|
275
456
|
}
|
|
276
457
|
if (stat.mode & 3072) {
|
|
458
|
+
(0, import_fs.closeSync)(fd);
|
|
277
459
|
die(`raft-puppy at ${realBinary} has setuid/setgid bits \u2014 refusing to exec.`);
|
|
278
460
|
}
|
|
279
|
-
return realBinary;
|
|
461
|
+
return { path: realBinary, fd };
|
|
462
|
+
}
|
|
463
|
+
function parseSemver(version) {
|
|
464
|
+
const parts = version.split("-")[0].split(".").map(Number);
|
|
465
|
+
return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];
|
|
466
|
+
}
|
|
467
|
+
function semverGte(a, b) {
|
|
468
|
+
const [aMaj, aMin, aPatch] = parseSemver(a);
|
|
469
|
+
const [bMaj, bMin, bPatch] = parseSemver(b);
|
|
470
|
+
if (aMaj !== bMaj) return aMaj > bMaj;
|
|
471
|
+
if (aMin !== bMin) return aMin > bMin;
|
|
472
|
+
return aPatch >= bPatch;
|
|
473
|
+
}
|
|
474
|
+
function validateBinaryVersion(binaryPath) {
|
|
475
|
+
if (process.env["STACKWRIGHT_SKIP_PREFLIGHT"] === "true") {
|
|
476
|
+
log("Pre-flight checks skipped via STACKWRIGHT_SKIP_PREFLIGHT");
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
const versionStartTime = Date.now();
|
|
480
|
+
const versionResult = (0, import_child_process.spawnSync)(binaryPath, ["--version"], {
|
|
481
|
+
encoding: "utf-8",
|
|
482
|
+
// 15s: uv-installed Python tools cold-start (venv activation + interpreter
|
|
483
|
+
// startup) can take 5-10s on first invocation; subsequent runs are ms.
|
|
484
|
+
timeout: 15e3,
|
|
485
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
486
|
+
});
|
|
487
|
+
const versionElapsed = Date.now() - versionStartTime;
|
|
488
|
+
if (versionResult.error !== void 0 || versionResult.status !== 0) {
|
|
489
|
+
const errorDetail = versionResult.error ? `${versionResult.error.code ?? "unknown"}: ${versionResult.error.message}` : `exit status ${String(versionResult.status)}`;
|
|
490
|
+
const stdoutSnip = (versionResult.stdout ?? "").trim() || "<empty>";
|
|
491
|
+
const stderrSnip = (versionResult.stderr ?? "").trim() || "<empty>";
|
|
492
|
+
die(
|
|
493
|
+
`Could not determine raft-puppy / code-puppy version.
|
|
494
|
+
Binary: ${binaryPath}
|
|
495
|
+
Failure: ${errorDetail}
|
|
496
|
+
Elapsed: ${String(versionElapsed)}ms (timeout: 15000ms)
|
|
497
|
+
Stdout: ${stdoutSnip}
|
|
498
|
+
Stderr: ${stderrSnip}
|
|
499
|
+
Required: ${MIN_SUPPORTED_CODE_PUPPY_VERSION}
|
|
500
|
+
|
|
501
|
+
Run: pip install --upgrade stackwright-puppy
|
|
502
|
+
Or: pip install --upgrade code-puppy
|
|
503
|
+
|
|
504
|
+
If the binary works manually but fails here, set
|
|
505
|
+
STACKWRIGHT_SKIP_PREFLIGHT=true to bypass this check.`
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
const versionStdout = versionResult.stdout.trim();
|
|
509
|
+
const versionStderr = versionResult.stderr.trim();
|
|
510
|
+
const versionOutput = versionStdout || versionStderr;
|
|
511
|
+
const match = /(\d+\.\d+\.\d+(?:-[^\s]+)?)/.exec(versionOutput);
|
|
512
|
+
if (!match || !match[1]) {
|
|
513
|
+
die(
|
|
514
|
+
`Could not parse version from raft-puppy / code-puppy output.
|
|
515
|
+
Binary: ${binaryPath}
|
|
516
|
+
Stdout: ${JSON.stringify(versionStdout) || "<empty>"}
|
|
517
|
+
Stderr: ${JSON.stringify(versionStderr) || "<empty>"}
|
|
518
|
+
Required: ${MIN_SUPPORTED_CODE_PUPPY_VERSION}
|
|
519
|
+
|
|
520
|
+
Run: pip install --upgrade stackwright-puppy
|
|
521
|
+
Or: pip install --upgrade code-puppy`
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
const installedVersion = match[1];
|
|
525
|
+
const isDevBuild = installedVersion === "0.0.0" || /^0\.0\.0-.+$/.test(installedVersion);
|
|
526
|
+
if (isDevBuild) {
|
|
527
|
+
console.warn(
|
|
528
|
+
`\u26A0\uFE0F Dev build detected (${installedVersion}) \u2014 skipping minimum version check.
|
|
529
|
+
Minimum required for production: ${MIN_SUPPORTED_CODE_PUPPY_VERSION}
|
|
530
|
+
To suppress this warning in CI: set STACKWRIGHT_SKIP_PREFLIGHT=true`
|
|
531
|
+
);
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
if (!semverGte(installedVersion, MIN_SUPPORTED_CODE_PUPPY_VERSION)) {
|
|
535
|
+
die(
|
|
536
|
+
`raft-puppy / code-puppy ${installedVersion} is below the minimum required version.
|
|
537
|
+
Installed: ${installedVersion}
|
|
538
|
+
Minimum required: ${MIN_SUPPORTED_CODE_PUPPY_VERSION}
|
|
539
|
+
|
|
540
|
+
Run: pip install --upgrade stackwright-puppy
|
|
541
|
+
Or: pip install --upgrade code-puppy`
|
|
542
|
+
);
|
|
543
|
+
}
|
|
280
544
|
}
|
|
281
545
|
function ensureMcpConfig(projectRoot) {
|
|
282
|
-
const
|
|
283
|
-
const
|
|
284
|
-
const configPath = (0, import_path.join)(configDir, "mcp_servers.json");
|
|
546
|
+
const workspaceDir = (0, import_path.join)(projectRoot, ".code_puppy");
|
|
547
|
+
const configPath = (0, import_path.join)(workspaceDir, "mcp_servers.json");
|
|
285
548
|
if ((0, import_fs.existsSync)(configPath) && (0, import_fs.lstatSync)(configPath).isSymbolicLink()) {
|
|
286
|
-
die("
|
|
287
|
-
}
|
|
288
|
-
(0, import_fs.mkdirSync)(configDir, { recursive: true });
|
|
289
|
-
const localServerPath = (0, import_path.join)(
|
|
290
|
-
projectRoot,
|
|
291
|
-
"node_modules",
|
|
292
|
-
"@stackwright-pro",
|
|
293
|
-
"mcp",
|
|
294
|
-
"dist",
|
|
295
|
-
"server.js"
|
|
296
|
-
);
|
|
297
|
-
let raftOwnServerPath = null;
|
|
298
|
-
try {
|
|
299
|
-
raftOwnServerPath = require.resolve("@stackwright-pro/mcp");
|
|
300
|
-
} catch {
|
|
549
|
+
die(".code_puppy/mcp_servers.json is a symlink \u2014 refusing to write. Check for tampering.");
|
|
301
550
|
}
|
|
302
|
-
|
|
551
|
+
(0, import_fs.mkdirSync)(workspaceDir, { recursive: true });
|
|
303
552
|
let existing = {};
|
|
304
553
|
if ((0, import_fs.existsSync)(configPath)) {
|
|
305
554
|
try {
|
|
@@ -308,28 +557,59 @@ function ensureMcpConfig(projectRoot) {
|
|
|
308
557
|
} catch {
|
|
309
558
|
}
|
|
310
559
|
}
|
|
560
|
+
const shimName = (0, import_os.platform)() === "win32" ? "stackwright-pro-mcp.cmd" : "stackwright-pro-mcp";
|
|
561
|
+
const shimPath = (0, import_path.join)(projectRoot, "node_modules", ".bin", shimName);
|
|
562
|
+
if (!(0, import_fs.existsSync)(shimPath)) {
|
|
563
|
+
die(
|
|
564
|
+
`MCP server shim not found: ${shimPath}
|
|
565
|
+
Run \`pnpm install\` (or your package manager equivalent) before \`raft\`.`
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
const serverConfig = {
|
|
569
|
+
type: "stdio",
|
|
570
|
+
command: shimPath,
|
|
571
|
+
args: [],
|
|
572
|
+
enabled: true,
|
|
573
|
+
cwd: projectRoot
|
|
574
|
+
};
|
|
311
575
|
const merged = {
|
|
312
576
|
...existing,
|
|
313
577
|
mcp_servers: {
|
|
314
578
|
...existing.mcp_servers ?? {},
|
|
315
|
-
"stackwright-pro": serverConfig
|
|
579
|
+
"stackwright-pro-mcp": serverConfig
|
|
316
580
|
}
|
|
317
581
|
};
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
582
|
+
const tmpPath = `${configPath}.tmp`;
|
|
583
|
+
(0, import_fs.writeFileSync)(tmpPath, JSON.stringify(merged, null, 2), "utf-8");
|
|
584
|
+
(0, import_fs.renameSync)(tmpPath, configPath);
|
|
585
|
+
log("MCP server registered \u2192 .code_puppy/mcp_servers.json");
|
|
586
|
+
}
|
|
587
|
+
function ensureWorkspaceConfig(projectRoot) {
|
|
588
|
+
const workspaceDir = (0, import_path.join)(projectRoot, ".code_puppy");
|
|
589
|
+
const configPath = (0, import_path.join)(workspaceDir, "config.json");
|
|
590
|
+
if ((0, import_fs.existsSync)(workspaceDir) && (0, import_fs.lstatSync)(workspaceDir).isSymbolicLink()) {
|
|
591
|
+
die(".code_puppy/ is a symlink \u2014 refusing to write. Check for tampering.");
|
|
592
|
+
}
|
|
593
|
+
(0, import_fs.mkdirSync)(workspaceDir, { recursive: true });
|
|
594
|
+
if ((0, import_fs.existsSync)(configPath) && (0, import_fs.lstatSync)(configPath).isSymbolicLink()) {
|
|
595
|
+
die(".code_puppy/config.json is a symlink \u2014 refusing to write. Check for tampering.");
|
|
596
|
+
}
|
|
597
|
+
let existing = {};
|
|
598
|
+
if ((0, import_fs.existsSync)(configPath)) {
|
|
599
|
+
try {
|
|
600
|
+
existing = JSON.parse((0, import_fs.readFileSync)(configPath, "utf-8"));
|
|
601
|
+
} catch {
|
|
327
602
|
}
|
|
328
603
|
}
|
|
604
|
+
const merged = {
|
|
605
|
+
...existing,
|
|
606
|
+
$schema: "https://stackwright.dev/schemas/workspace-config/v2.json",
|
|
607
|
+
profile: "strict-local"
|
|
608
|
+
};
|
|
329
609
|
const tmpPath = `${configPath}.tmp`;
|
|
330
610
|
(0, import_fs.writeFileSync)(tmpPath, JSON.stringify(merged, null, 2), "utf-8");
|
|
331
611
|
(0, import_fs.renameSync)(tmpPath, configPath);
|
|
332
|
-
log("
|
|
612
|
+
log("Workspace config written \u2192 .code_puppy/config.json (profile: strict-local)");
|
|
333
613
|
}
|
|
334
614
|
function printResumeStatus(projectRoot) {
|
|
335
615
|
const pipelineStatePath = (0, import_path.join)(projectRoot, ".stackwright", "pipeline-state.json");
|
|
@@ -366,6 +646,17 @@ function printResumeStatus(projectRoot) {
|
|
|
366
646
|
} catch {
|
|
367
647
|
}
|
|
368
648
|
}
|
|
649
|
+
function isPipelineResumable(projectRoot) {
|
|
650
|
+
const pipelineStatePath = (0, import_path.join)(projectRoot, ".stackwright", "pipeline-state.json");
|
|
651
|
+
try {
|
|
652
|
+
const raw = (0, import_fs.readFileSync)(pipelineStatePath, "utf-8");
|
|
653
|
+
const state = JSON.parse(raw);
|
|
654
|
+
const status = state["status"];
|
|
655
|
+
return status === "questions" || status === "execution";
|
|
656
|
+
} catch {
|
|
657
|
+
return false;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
369
660
|
function resolveOtterDir(projectRoot) {
|
|
370
661
|
const candidates = [
|
|
371
662
|
(0, import_path.join)(projectRoot, "node_modules", "@stackwright-pro", "otters", "src"),
|
|
@@ -378,25 +669,326 @@ function resolveOtterDir(projectRoot) {
|
|
|
378
669
|
}
|
|
379
670
|
return null;
|
|
380
671
|
}
|
|
672
|
+
function syncAgents(projectRoot, isVerbose = false) {
|
|
673
|
+
const agentsDir = (0, import_path.join)(projectRoot, ".code_puppy", "agents");
|
|
674
|
+
const otterDir = resolveOtterDir(projectRoot);
|
|
675
|
+
if (!otterDir) {
|
|
676
|
+
verbose("No otters directory found \u2014 skipping agent sync", isVerbose);
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
if ((0, import_fs.existsSync)(agentsDir) && (0, import_fs.lstatSync)(agentsDir).isSymbolicLink()) {
|
|
680
|
+
die(".code_puppy/agents is a symlink \u2014 refusing to write. Check for tampering.");
|
|
681
|
+
}
|
|
682
|
+
(0, import_fs.mkdirSync)(agentsDir, { recursive: true });
|
|
683
|
+
let synced = 0;
|
|
684
|
+
let skipped = 0;
|
|
685
|
+
try {
|
|
686
|
+
const files = (0, import_fs.readdirSync)(otterDir);
|
|
687
|
+
for (const file of files) {
|
|
688
|
+
if (!file.endsWith("-otter.json")) continue;
|
|
689
|
+
const src = (0, import_path.join)(otterDir, file);
|
|
690
|
+
const dest = (0, import_path.join)(agentsDir, file);
|
|
691
|
+
if ((0, import_fs.existsSync)(dest) && (0, import_fs.lstatSync)(dest).isSymbolicLink()) {
|
|
692
|
+
verbose(`Skipping ${file} \u2014 dest is a symlink`, isVerbose);
|
|
693
|
+
skipped++;
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
696
|
+
const tmp = `${dest}.tmp`;
|
|
697
|
+
const content = (0, import_fs.readFileSync)(src);
|
|
698
|
+
(0, import_fs.writeFileSync)(tmp, content);
|
|
699
|
+
(0, import_fs.renameSync)(tmp, dest);
|
|
700
|
+
verbose(`Synced: ${file}`, isVerbose);
|
|
701
|
+
synced++;
|
|
702
|
+
}
|
|
703
|
+
} catch (err) {
|
|
704
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
705
|
+
console.warn(`\u26A0\uFE0F Agent sync partial failure: ${msg}`);
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
if (synced > 0) {
|
|
709
|
+
log(
|
|
710
|
+
`Agents synced \u2192 .code_puppy/agents/ (${synced} otters${skipped > 0 ? `, ${skipped} skipped` : ""})`
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
function buildSpawnConfig(platform2, executable, binaryFd) {
|
|
715
|
+
const PINNED_CHILD_FD = 3;
|
|
716
|
+
if (platform2 === "linux") {
|
|
717
|
+
return {
|
|
718
|
+
spawnTarget: `/proc/self/fd/${PINNED_CHILD_FD}`,
|
|
719
|
+
stdioConfig: ["inherit", "inherit", "inherit", binaryFd]
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
return {
|
|
723
|
+
spawnTarget: executable,
|
|
724
|
+
stdioConfig: "inherit"
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// src/dev-server.ts
|
|
729
|
+
var import_child_process2 = require("child_process");
|
|
730
|
+
var import_fs2 = require("fs");
|
|
731
|
+
var import_path2 = require("path");
|
|
732
|
+
var DevServerStartupError = class extends Error {
|
|
733
|
+
constructor(message, logTail, elapsedMs) {
|
|
734
|
+
super(message);
|
|
735
|
+
this.logTail = logTail;
|
|
736
|
+
this.elapsedMs = elapsedMs;
|
|
737
|
+
this.name = "DevServerStartupError";
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
function detectPrismConfig(projectRoot, defaultPrismUrl = "http://localhost:4010") {
|
|
741
|
+
try {
|
|
742
|
+
const pkgPath = (0, import_path2.join)(projectRoot, "package.json");
|
|
743
|
+
const raw = (0, import_fs2.readFileSync)(pkgPath, "utf-8");
|
|
744
|
+
const pkg = JSON.parse(raw);
|
|
745
|
+
const scripts = pkg["scripts"];
|
|
746
|
+
const devScript = typeof scripts?.["dev"] === "string" ? scripts["dev"] : "";
|
|
747
|
+
const hasConcurrently = devScript.includes("concurrently");
|
|
748
|
+
const hasPrism = /prism/i.test(devScript);
|
|
749
|
+
return { hasPrism: hasConcurrently && hasPrism, prismUrl: defaultPrismUrl };
|
|
750
|
+
} catch {
|
|
751
|
+
return { hasPrism: false, prismUrl: defaultPrismUrl };
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
function tailLog(logPath, maxLines) {
|
|
755
|
+
try {
|
|
756
|
+
const content = (0, import_fs2.readFileSync)(logPath, "utf-8");
|
|
757
|
+
const lines = content.split("\n").filter((l) => l.length > 0);
|
|
758
|
+
return lines.slice(-maxLines);
|
|
759
|
+
} catch {
|
|
760
|
+
return [];
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
function sleep(ms) {
|
|
764
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
765
|
+
}
|
|
766
|
+
async function singleFetch(url, timeoutMs) {
|
|
767
|
+
const controller = new AbortController();
|
|
768
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
769
|
+
try {
|
|
770
|
+
const res = await globalThis.fetch(url, { signal: controller.signal });
|
|
771
|
+
clearTimeout(timer);
|
|
772
|
+
return res.status >= 200 && res.status < 300;
|
|
773
|
+
} catch {
|
|
774
|
+
clearTimeout(timer);
|
|
775
|
+
return false;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
async function pollUntilAllReady(urls, timeoutMs, pollIntervalMs, perRequestTimeoutMs) {
|
|
779
|
+
const deadline = Date.now() + timeoutMs;
|
|
780
|
+
while (Date.now() < deadline) {
|
|
781
|
+
const remaining = deadline - Date.now();
|
|
782
|
+
const perReq = Math.min(perRequestTimeoutMs, remaining);
|
|
783
|
+
const results = await Promise.all(urls.map((url) => singleFetch(url, perReq)));
|
|
784
|
+
if (results.every(Boolean)) return true;
|
|
785
|
+
const sleepMs = Math.min(pollIntervalMs, deadline - Date.now());
|
|
786
|
+
if (sleepMs <= 0) break;
|
|
787
|
+
await sleep(sleepMs);
|
|
788
|
+
}
|
|
789
|
+
return false;
|
|
790
|
+
}
|
|
791
|
+
async function killChildProcess(child, pid) {
|
|
792
|
+
return new Promise((resolve3) => {
|
|
793
|
+
let settled = false;
|
|
794
|
+
const settle = () => {
|
|
795
|
+
if (!settled) {
|
|
796
|
+
settled = true;
|
|
797
|
+
clearTimeout(escalateTimer);
|
|
798
|
+
resolve3();
|
|
799
|
+
}
|
|
800
|
+
};
|
|
801
|
+
child.once("exit", settle);
|
|
802
|
+
const escalateTimer = setTimeout(() => {
|
|
803
|
+
try {
|
|
804
|
+
process.kill(pid, "SIGKILL");
|
|
805
|
+
} catch {
|
|
806
|
+
}
|
|
807
|
+
settle();
|
|
808
|
+
}, 5e3);
|
|
809
|
+
try {
|
|
810
|
+
process.kill(pid, "SIGTERM");
|
|
811
|
+
} catch {
|
|
812
|
+
settle();
|
|
813
|
+
}
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
async function withDevServer(projectRoot, options, body) {
|
|
817
|
+
const {
|
|
818
|
+
timeoutMs = 6e4,
|
|
819
|
+
baseUrl: baseUrlOpt,
|
|
820
|
+
prismUrl: prismUrlOpt,
|
|
821
|
+
detectPrism: shouldDetectPrism = true
|
|
822
|
+
} = options;
|
|
823
|
+
const baseUrl = baseUrlOpt ?? "http://localhost:3000";
|
|
824
|
+
let prismUrl;
|
|
825
|
+
if (prismUrlOpt !== void 0) {
|
|
826
|
+
prismUrl = prismUrlOpt;
|
|
827
|
+
} else if (shouldDetectPrism) {
|
|
828
|
+
const cfg = detectPrismConfig(projectRoot);
|
|
829
|
+
prismUrl = cfg.hasPrism ? cfg.prismUrl : void 0;
|
|
830
|
+
}
|
|
831
|
+
const stackwrightDir = (0, import_path2.join)(projectRoot, ".stackwright");
|
|
832
|
+
(0, import_fs2.mkdirSync)(stackwrightDir, { recursive: true });
|
|
833
|
+
const logPath = (0, import_path2.join)(stackwrightDir, "dev-server.log");
|
|
834
|
+
const logFd = (0, import_fs2.openSync)(logPath, "w");
|
|
835
|
+
const readyFilePath = (0, import_path2.join)(stackwrightDir, "dev-server-ready.json");
|
|
836
|
+
const startedAt = Date.now();
|
|
837
|
+
let child;
|
|
838
|
+
let pid;
|
|
839
|
+
let spawnErr;
|
|
840
|
+
try {
|
|
841
|
+
child = (0, import_child_process2.spawn)("pnpm", ["dev"], {
|
|
842
|
+
cwd: projectRoot,
|
|
843
|
+
detached: false,
|
|
844
|
+
stdio: ["ignore", logFd, logFd]
|
|
845
|
+
});
|
|
846
|
+
pid = child.pid;
|
|
847
|
+
child.on("error", (err) => {
|
|
848
|
+
spawnErr = err;
|
|
849
|
+
});
|
|
850
|
+
if (pid === void 0) {
|
|
851
|
+
throw new DevServerStartupError(
|
|
852
|
+
"pnpm dev failed to spawn (no PID)",
|
|
853
|
+
tailLog(logPath, 50),
|
|
854
|
+
Date.now() - startedAt
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
const urlsToProbe = [baseUrl, ...prismUrl ? [prismUrl] : []];
|
|
858
|
+
const ready = await pollUntilAllReady(urlsToProbe, timeoutMs, 2e3, 1e3);
|
|
859
|
+
if (!ready) {
|
|
860
|
+
const elapsedMs = Date.now() - startedAt;
|
|
861
|
+
const tail = tailLog(logPath, 50);
|
|
862
|
+
const spawnErrMsg = spawnErr ? ` Spawn error: ${spawnErr.message}.` : "";
|
|
863
|
+
throw new DevServerStartupError(
|
|
864
|
+
`Dev server did not become ready within ${timeoutMs}ms.${spawnErrMsg}`,
|
|
865
|
+
tail,
|
|
866
|
+
elapsedMs
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
const info = {
|
|
870
|
+
pid,
|
|
871
|
+
baseUrl,
|
|
872
|
+
...prismUrl ? { prismUrl } : {},
|
|
873
|
+
startedAt,
|
|
874
|
+
logPath
|
|
875
|
+
};
|
|
876
|
+
(0, import_fs2.writeFileSync)(
|
|
877
|
+
readyFilePath,
|
|
878
|
+
JSON.stringify({ ...info, readyAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2) + "\n"
|
|
879
|
+
);
|
|
880
|
+
return await body(info);
|
|
881
|
+
} finally {
|
|
882
|
+
if (child && pid !== void 0) {
|
|
883
|
+
await killChildProcess(child, pid);
|
|
884
|
+
}
|
|
885
|
+
try {
|
|
886
|
+
(0, import_fs2.unlinkSync)(readyFilePath);
|
|
887
|
+
} catch {
|
|
888
|
+
}
|
|
889
|
+
try {
|
|
890
|
+
(0, import_fs2.closeSync)(logFd);
|
|
891
|
+
} catch {
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
381
895
|
|
|
382
896
|
// src/index.ts
|
|
383
|
-
|
|
897
|
+
var MAX_RETRIES = 3;
|
|
898
|
+
var BACKOFF_INITIAL_MS = 5e3;
|
|
899
|
+
var BACKOFF_MAX_MS = 6e4;
|
|
900
|
+
function backoffMs(attempt) {
|
|
901
|
+
return Math.min(BACKOFF_INITIAL_MS * Math.pow(2, attempt), BACKOFF_MAX_MS);
|
|
902
|
+
}
|
|
903
|
+
async function main() {
|
|
384
904
|
const args = parseArgs(process.argv);
|
|
385
905
|
if (args.help) {
|
|
386
906
|
printHelp();
|
|
387
907
|
process.exit(0);
|
|
388
908
|
}
|
|
389
|
-
const projectRoot = (0,
|
|
390
|
-
if (!(0,
|
|
909
|
+
const projectRoot = (0, import_path3.resolve)(args.projectRoot);
|
|
910
|
+
if (!(0, import_fs3.existsSync)(projectRoot)) {
|
|
391
911
|
die(`Project root does not exist: ${projectRoot}`);
|
|
392
912
|
}
|
|
393
|
-
if (!(0,
|
|
913
|
+
if (!(0, import_fs3.existsSync)((0, import_path3.join)(projectRoot, "package.json"))) {
|
|
394
914
|
die("No package.json found. Run npx @stackwright-pro/launch-stackwright-pro first.");
|
|
395
915
|
}
|
|
396
916
|
log("Launching Pro Otter Raft...");
|
|
397
917
|
writeInitContext(projectRoot);
|
|
398
918
|
verbose("Init context written", args.verbose);
|
|
919
|
+
try {
|
|
920
|
+
const stackwrightDir = (0, import_path3.join)(projectRoot, ".stackwright");
|
|
921
|
+
(0, import_fs3.mkdirSync)(stackwrightDir, { recursive: true });
|
|
922
|
+
const schemaSummary = (0, import_type_schemas.buildTypeSchemaSummary)();
|
|
923
|
+
(0, import_fs3.writeFileSync)(
|
|
924
|
+
(0, import_path3.join)(stackwrightDir, "type-schemas.json"),
|
|
925
|
+
JSON.stringify(schemaSummary, null, 2) + "\n"
|
|
926
|
+
);
|
|
927
|
+
verbose("Type schemas sink written", args.verbose);
|
|
928
|
+
} catch (err) {
|
|
929
|
+
console.warn("\u26A0\uFE0F Could not write type-schemas.json:", String(err));
|
|
930
|
+
}
|
|
931
|
+
{
|
|
932
|
+
const stackwrightDir = (0, import_path3.join)(projectRoot, ".stackwright");
|
|
933
|
+
const initContextPath = (0, import_path3.join)(stackwrightDir, "init-context.json");
|
|
934
|
+
const initRaw = (0, import_fs3.readFileSync)(initContextPath, "utf-8");
|
|
935
|
+
const initCtx = JSON.parse(initRaw);
|
|
936
|
+
if (args.nonInteractive) initCtx["nonInteractive"] = true;
|
|
937
|
+
if (args.devOnly) initCtx["devOnly"] = true;
|
|
938
|
+
if (args.parallelPhases) initCtx["parallelPhases"] = true;
|
|
939
|
+
if (args.dataflowScheduling) initCtx["dataflowScheduling"] = true;
|
|
940
|
+
initCtx["maxConcurrentApiInvocations"] = args.maxConcurrentApiInvocations;
|
|
941
|
+
initCtx["skipQa"] = args.skipQa;
|
|
942
|
+
initCtx["qaOnly"] = args.qaOnly;
|
|
943
|
+
initCtx["devServer"] = {
|
|
944
|
+
managed: true,
|
|
945
|
+
baseUrl: "http://localhost:3000",
|
|
946
|
+
timeoutMs: args.qaDevServerTimeoutMs
|
|
947
|
+
};
|
|
948
|
+
(0, import_fs3.writeFileSync)(initContextPath, JSON.stringify(initCtx, null, 2) + "\n", "utf-8");
|
|
949
|
+
verbose("Raft flags written to init-context", args.verbose);
|
|
950
|
+
}
|
|
951
|
+
{
|
|
952
|
+
const buildContextPath = (0, import_path3.join)(projectRoot, ".stackwright", "build-context.json");
|
|
953
|
+
const MAX_USE_CASE_BYTES = 1 * 1024 * 1024;
|
|
954
|
+
const WARN_USE_CASE_BYTES = 200 * 1024;
|
|
955
|
+
if (args.useCasePath) {
|
|
956
|
+
const resolvedPath = (0, import_path3.resolve)(args.useCasePath);
|
|
957
|
+
if (!(0, import_fs3.existsSync)(resolvedPath)) {
|
|
958
|
+
die(`Use-case file not found: ${resolvedPath}`);
|
|
959
|
+
}
|
|
960
|
+
const content = (0, import_fs3.readFileSync)(resolvedPath, "utf-8");
|
|
961
|
+
if (content.length > MAX_USE_CASE_BYTES) {
|
|
962
|
+
die(
|
|
963
|
+
`Use-case file exceeds ${(MAX_USE_CASE_BYTES / 1024 / 1024).toFixed(0)}MB (got ${(content.length / 1024).toFixed(0)}KB). Trim the file or split into sections.`
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
if (content.length > WARN_USE_CASE_BYTES) {
|
|
967
|
+
console.warn(
|
|
968
|
+
`\u26A0\uFE0F Large use-case file (${(content.length / 1024).toFixed(0)}KB) \u2014 specialist otters will receive the full context via build prompts.`
|
|
969
|
+
);
|
|
970
|
+
}
|
|
971
|
+
const payload = {
|
|
972
|
+
version: "1.0",
|
|
973
|
+
savedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
974
|
+
buildContext: content
|
|
975
|
+
};
|
|
976
|
+
(0, import_fs3.writeFileSync)(buildContextPath, JSON.stringify(payload, null, 2) + "\n", "utf-8");
|
|
977
|
+
log(`Build context seeded from ${args.useCasePath}`);
|
|
978
|
+
} else if (args.nonInteractive && !(0, import_fs3.existsSync)(buildContextPath)) {
|
|
979
|
+
const payload = {
|
|
980
|
+
version: "1.0",
|
|
981
|
+
savedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
982
|
+
buildContext: "Build a sample application from the available API specs and data sources with default settings. Use all available endpoints and standard page layouts."
|
|
983
|
+
};
|
|
984
|
+
(0, import_fs3.writeFileSync)(buildContextPath, JSON.stringify(payload, null, 2) + "\n", "utf-8");
|
|
985
|
+
log("Build context seeded with generic defaults (--non-interactive, no --use-case)");
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
migrateWorkspaceDir(projectRoot);
|
|
989
|
+
ensureWorkspaceConfig(projectRoot);
|
|
399
990
|
ensureMcpConfig(projectRoot);
|
|
991
|
+
syncAgents(projectRoot, args.verbose);
|
|
400
992
|
const otterDir = resolveOtterDir(projectRoot);
|
|
401
993
|
if (!otterDir) {
|
|
402
994
|
die(
|
|
@@ -417,36 +1009,110 @@ function main() {
|
|
|
417
1009
|
log(`\u2705 All ${result.verified.length} otters verified`);
|
|
418
1010
|
}
|
|
419
1011
|
printResumeStatus(projectRoot);
|
|
420
|
-
const executable = findCodePuppy();
|
|
1012
|
+
const { path: executable, fd: initialFd } = findCodePuppy();
|
|
421
1013
|
verbose(`Resolved raft-puppy / code-puppy: ${executable}`, args.verbose);
|
|
1014
|
+
try {
|
|
1015
|
+
(0, import_fs3.closeSync)(initialFd);
|
|
1016
|
+
} catch {
|
|
1017
|
+
}
|
|
1018
|
+
validateBinaryVersion(executable);
|
|
1019
|
+
if (args.qaOnly) {
|
|
1020
|
+
log("--qa-only mode: skipping otter pipeline, spawning dev server for visual QA...");
|
|
1021
|
+
await withDevServer(projectRoot, { timeoutMs: args.qaDevServerTimeoutMs }, async (info) => {
|
|
1022
|
+
log("--qa-only: dev server ready \u2705");
|
|
1023
|
+
log(` pid: ${info.pid}`);
|
|
1024
|
+
log(` baseUrl: ${info.baseUrl}`);
|
|
1025
|
+
if (info.prismUrl) log(` prismUrl: ${info.prismUrl}`);
|
|
1026
|
+
log(` logPath: ${info.logPath}`);
|
|
1027
|
+
log("\u23F8 QA otter invocation not yet implemented (blocked on swp-wde8 + swp-kjkg)");
|
|
1028
|
+
});
|
|
1029
|
+
process.exit(0);
|
|
1030
|
+
}
|
|
422
1031
|
log("Spawning raft-puppy / code-puppy raft session...");
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
1032
|
+
let retryCount = 0;
|
|
1033
|
+
let userInterrupted = false;
|
|
1034
|
+
function spawnSession(isResume) {
|
|
1035
|
+
const { path: resolvedBinary, fd: binaryFd } = findCodePuppy();
|
|
1036
|
+
if (process.platform !== "linux") {
|
|
1037
|
+
verbose(
|
|
1038
|
+
`macOS/other: spawning via path (residual TOCTOU \u2014 /proc unavailable). See GH#128 for threat model documentation.`,
|
|
1039
|
+
args.verbose
|
|
1040
|
+
);
|
|
431
1041
|
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
1042
|
+
const prompt = isResume ? "Resume" : "Begin";
|
|
1043
|
+
const spawnArgs = [prompt, "--interactive", "--agent", "stackwright-pro-foreman-otter"];
|
|
1044
|
+
const { spawnTarget, stdioConfig } = buildSpawnConfig(
|
|
1045
|
+
process.platform,
|
|
1046
|
+
resolvedBinary,
|
|
1047
|
+
binaryFd
|
|
1048
|
+
);
|
|
1049
|
+
verbose(
|
|
1050
|
+
`cmd: ${spawnTarget} ${spawnArgs.join(" ")}${spawnTarget !== resolvedBinary ? ` (real: ${resolvedBinary})` : ""}${isResume ? ` [retry ${retryCount}/${MAX_RETRIES}]` : ""}`,
|
|
1051
|
+
args.verbose
|
|
1052
|
+
);
|
|
1053
|
+
const child = (0, import_child_process3.spawn)(spawnTarget, spawnArgs, {
|
|
1054
|
+
stdio: stdioConfig,
|
|
1055
|
+
cwd: projectRoot,
|
|
1056
|
+
env: buildCodePuppyEnv(
|
|
1057
|
+
buildLauncherEnvOverrides(projectRoot, { subagentResponses: args.subagentResponses }),
|
|
1058
|
+
args.passEnvKeys
|
|
1059
|
+
)
|
|
1060
|
+
});
|
|
1061
|
+
try {
|
|
1062
|
+
(0, import_fs3.closeSync)(binaryFd);
|
|
1063
|
+
} catch {
|
|
448
1064
|
}
|
|
449
|
-
|
|
1065
|
+
const forward = (signal) => {
|
|
1066
|
+
if (child.pid) child.kill(signal);
|
|
1067
|
+
};
|
|
1068
|
+
const onSigint = () => {
|
|
1069
|
+
userInterrupted = true;
|
|
1070
|
+
forward("SIGINT");
|
|
1071
|
+
};
|
|
1072
|
+
const onSigterm = () => {
|
|
1073
|
+
userInterrupted = true;
|
|
1074
|
+
forward("SIGTERM");
|
|
1075
|
+
};
|
|
1076
|
+
process.on("SIGINT", onSigint);
|
|
1077
|
+
process.on("SIGTERM", onSigterm);
|
|
1078
|
+
child.on("error", (err) => die(`Failed to spawn raft-puppy / code-puppy: ${err.message}`));
|
|
1079
|
+
child.on("close", (code, signal) => {
|
|
1080
|
+
process.off("SIGINT", onSigint);
|
|
1081
|
+
process.off("SIGTERM", onSigterm);
|
|
1082
|
+
if (signal) {
|
|
1083
|
+
process.kill(process.pid, signal);
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
if (code === 0 || userInterrupted) {
|
|
1087
|
+
process.exit(code ?? 0);
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
if (retryCount < MAX_RETRIES && isPipelineResumable(projectRoot)) {
|
|
1091
|
+
retryCount++;
|
|
1092
|
+
const delay = backoffMs(retryCount - 1);
|
|
1093
|
+
log(
|
|
1094
|
+
`\u26A0\uFE0F Raft session exited unexpectedly (code ${code ?? "?"}). Pipeline is mid-run \u2014 retrying in ${delay / 1e3}s\u2026 (attempt ${retryCount}/${MAX_RETRIES})`
|
|
1095
|
+
);
|
|
1096
|
+
setTimeout(() => spawnSession(true), delay);
|
|
1097
|
+
} else {
|
|
1098
|
+
if (retryCount >= MAX_RETRIES) {
|
|
1099
|
+
log(`\u274C Max retries (${MAX_RETRIES}) reached \u2014 giving up. Check logs above for errors.`);
|
|
1100
|
+
}
|
|
1101
|
+
process.exit(code ?? 1);
|
|
1102
|
+
}
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
if (args.skipQa) {
|
|
1106
|
+
log("\u26A0\uFE0F Visual QA skipped by --skip-qa flag");
|
|
1107
|
+
} else {
|
|
1108
|
+
log(
|
|
1109
|
+
"\u23F8 Visual QA stub \u2014 dev server lifecycle is wired, QA otter invocation lands in swp-wde8/swp-kjkg"
|
|
1110
|
+
);
|
|
1111
|
+
}
|
|
1112
|
+
spawnSession(false);
|
|
450
1113
|
}
|
|
451
|
-
main()
|
|
1114
|
+
main().catch((err) => {
|
|
1115
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1116
|
+
die(`Unexpected fatal error: ${msg}`);
|
|
1117
|
+
});
|
|
452
1118
|
//# sourceMappingURL=index.js.map
|