halfcycle 0.3.8 → 0.3.9
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/.claude-plugin/plugin.json +37 -10
- package/README.md +2 -0
- package/bin/bin.bundle.mjs +20 -3
- package/dist/bin.js +579 -505
- package/dist/bin.js.map +3 -3
- package/dist/control-origin.d.ts +25 -7
- package/dist/control-origin.d.ts.map +1 -1
- package/dist/create-engagement.d.ts.map +1 -1
- package/dist/index.js +191 -126
- package/dist/index.js.map +3 -3
- package/dist/install.d.ts +1 -0
- package/dist/install.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -10,346 +10,6 @@ import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as rea
|
|
|
10
10
|
import { dirname as dirname2, join as join5, relative } from "node:path";
|
|
11
11
|
import { fileURLToPath } from "node:url";
|
|
12
12
|
|
|
13
|
-
// dist/engagement-credential.js
|
|
14
|
-
import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
15
|
-
import { platform as platform2 } from "node:os";
|
|
16
|
-
import { join as join2 } from "node:path";
|
|
17
|
-
|
|
18
|
-
// dist/account-credential.js
|
|
19
|
-
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
20
|
-
import { homedir, platform } from "node:os";
|
|
21
|
-
import { dirname, join } from "node:path";
|
|
22
|
-
function halfcycleHome(home) {
|
|
23
|
-
return join(home ?? homedir(), ".halfcycle");
|
|
24
|
-
}
|
|
25
|
-
function accountStorePath(home) {
|
|
26
|
-
return join(halfcycleHome(home), "account.json");
|
|
27
|
-
}
|
|
28
|
-
function normaliseOrigin(serviceUrl) {
|
|
29
|
-
return serviceUrl.trim().replace(/\/+$/, "").toLowerCase();
|
|
30
|
-
}
|
|
31
|
-
function readStore(home) {
|
|
32
|
-
let raw;
|
|
33
|
-
try {
|
|
34
|
-
raw = readFileSync(accountStorePath(home), "utf-8");
|
|
35
|
-
} catch {
|
|
36
|
-
return { version: 1, accounts: {} };
|
|
37
|
-
}
|
|
38
|
-
try {
|
|
39
|
-
const parsed = JSON.parse(raw);
|
|
40
|
-
if (!parsed || typeof parsed !== "object" || typeof parsed.accounts !== "object") {
|
|
41
|
-
return { version: 1, accounts: {} };
|
|
42
|
-
}
|
|
43
|
-
return { version: 1, accounts: parsed.accounts ?? {} };
|
|
44
|
-
} catch {
|
|
45
|
-
return { version: 1, accounts: {} };
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
function writeStore(store, home) {
|
|
49
|
-
const path = accountStorePath(home);
|
|
50
|
-
mkdirSync(dirname(path), { recursive: true, mode: 448 });
|
|
51
|
-
writeFileSync(path, `${JSON.stringify(store, null, 2)}
|
|
52
|
-
`, { mode: 384 });
|
|
53
|
-
if (platform() !== "win32") {
|
|
54
|
-
chmodSync(path, 384);
|
|
55
|
-
try {
|
|
56
|
-
chmodSync(dirname(path), 448);
|
|
57
|
-
} catch {
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
function readStoredCredential(serviceUrl, home) {
|
|
62
|
-
const entry = readStore(home).accounts[normaliseOrigin(serviceUrl)];
|
|
63
|
-
if (!entry || typeof entry.credential !== "string" || entry.credential.trim() === "") {
|
|
64
|
-
return void 0;
|
|
65
|
-
}
|
|
66
|
-
return entry;
|
|
67
|
-
}
|
|
68
|
-
function writeStoredCredential(serviceUrl, entry, home) {
|
|
69
|
-
const store = readStore(home);
|
|
70
|
-
store.accounts[normaliseOrigin(serviceUrl)] = {
|
|
71
|
-
accountId: entry.accountId,
|
|
72
|
-
credential: entry.credential,
|
|
73
|
-
expiresAt: entry.expiresAt,
|
|
74
|
-
signedInAt: entry.signedInAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
75
|
-
};
|
|
76
|
-
writeStore(store, home);
|
|
77
|
-
}
|
|
78
|
-
function forgetStoredCredential(serviceUrl, home) {
|
|
79
|
-
const store = readStore(home);
|
|
80
|
-
const key = normaliseOrigin(serviceUrl);
|
|
81
|
-
if (!(key in store.accounts))
|
|
82
|
-
return false;
|
|
83
|
-
delete store.accounts[key];
|
|
84
|
-
writeStore(store, home);
|
|
85
|
-
return true;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// dist/engagement-credential.js
|
|
89
|
-
var ENGAGEMENTS_DIR = "engagements";
|
|
90
|
-
var ENV_FILENAME = "env";
|
|
91
|
-
var PIN_ENGAGEMENT_ID_FIELD = "engagementId";
|
|
92
|
-
function engagementStateDir(engagementId, home) {
|
|
93
|
-
return join2(halfcycleHome(home), ENGAGEMENTS_DIR, engagementId);
|
|
94
|
-
}
|
|
95
|
-
function engagementEnvPath(engagementId, home) {
|
|
96
|
-
return join2(engagementStateDir(engagementId, home), ENV_FILENAME);
|
|
97
|
-
}
|
|
98
|
-
var GUARD_ENV_KEYS = [
|
|
99
|
-
"GUARD_SERVICE_URL",
|
|
100
|
-
"GUARD_SERVICE_TOKEN",
|
|
101
|
-
"GUARD_ENGAGEMENT_ID"
|
|
102
|
-
];
|
|
103
|
-
var ENGAGEMENT_ENV_KEYS = [
|
|
104
|
-
"HALFCYCLE_SERVICE_URL",
|
|
105
|
-
"HALFCYCLE_MCP_URL",
|
|
106
|
-
"HALFCYCLE_TOKEN",
|
|
107
|
-
"HALFCYCLE_ENGAGEMENT_ID",
|
|
108
|
-
...GUARD_ENV_KEYS,
|
|
109
|
-
"CONTROL_TELEMETRY_URL"
|
|
110
|
-
];
|
|
111
|
-
var ENV_HEADER = "# Halfcycle per-engagement credential \u2014 machine level, owner-only, never in a repository.";
|
|
112
|
-
function shq(value) {
|
|
113
|
-
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
114
|
-
}
|
|
115
|
-
function unquote(value) {
|
|
116
|
-
const v = value.trim();
|
|
117
|
-
if (v.length >= 2 && v.startsWith("'") && v.endsWith("'")) {
|
|
118
|
-
return v.slice(1, -1).split(`'\\''`).join(`'`);
|
|
119
|
-
}
|
|
120
|
-
if (v.length >= 2 && v.startsWith('"') && v.endsWith('"'))
|
|
121
|
-
return v.slice(1, -1);
|
|
122
|
-
return v;
|
|
123
|
-
}
|
|
124
|
-
function parseEnvText(raw) {
|
|
125
|
-
const out = {};
|
|
126
|
-
for (const line of raw.split("\n")) {
|
|
127
|
-
const eq = line.indexOf("=");
|
|
128
|
-
if (eq === -1)
|
|
129
|
-
continue;
|
|
130
|
-
const key = line.slice(0, eq).replace(/^\s*export\s+/, "").trim();
|
|
131
|
-
if (key === "" || key.startsWith("#"))
|
|
132
|
-
continue;
|
|
133
|
-
out[key] = unquote(line.slice(eq + 1));
|
|
134
|
-
}
|
|
135
|
-
return out;
|
|
136
|
-
}
|
|
137
|
-
function reconcileEnvText(existing, values, keys = ENGAGEMENT_ENV_KEYS) {
|
|
138
|
-
const desired = new Map(keys.map((k) => [k, values[k] ?? ""]));
|
|
139
|
-
const line = (k) => `${k}=${shq(desired.get(k) ?? "")}`;
|
|
140
|
-
if (existing === null) {
|
|
141
|
-
return `${ENV_HEADER}
|
|
142
|
-
` + keys.map(line).join("\n") + "\n";
|
|
143
|
-
}
|
|
144
|
-
const seen = /* @__PURE__ */ new Set();
|
|
145
|
-
const out = existing.split("\n").map((existingLine) => {
|
|
146
|
-
const eq = existingLine.indexOf("=");
|
|
147
|
-
if (eq === -1)
|
|
148
|
-
return existingLine;
|
|
149
|
-
const lhs = existingLine.slice(0, eq);
|
|
150
|
-
const exported = /^\s*export\s+/.exec(lhs);
|
|
151
|
-
const prefix = exported === null ? "" : exported[0];
|
|
152
|
-
const key = lhs.slice(prefix.length).trim();
|
|
153
|
-
if (desired.has(key)) {
|
|
154
|
-
seen.add(key);
|
|
155
|
-
return `${prefix}${line(key)}`;
|
|
156
|
-
}
|
|
157
|
-
return existingLine;
|
|
158
|
-
});
|
|
159
|
-
const missing = keys.filter((k) => !seen.has(k));
|
|
160
|
-
if (missing.length > 0) {
|
|
161
|
-
const trailingBlank = out.length > 0 && out[out.length - 1] === "";
|
|
162
|
-
const prefix = trailingBlank ? "" : existing.endsWith("\n") ? "" : "\n";
|
|
163
|
-
out.push(`${prefix}${ENV_HEADER}
|
|
164
|
-
` + missing.map(line).join("\n"));
|
|
165
|
-
}
|
|
166
|
-
let result = out.join("\n");
|
|
167
|
-
if (existing.endsWith("\n") && !result.endsWith("\n"))
|
|
168
|
-
result += "\n";
|
|
169
|
-
return result;
|
|
170
|
-
}
|
|
171
|
-
function readEngagementEnv(engagementId, home) {
|
|
172
|
-
try {
|
|
173
|
-
return parseEnvText(readFileSync2(engagementEnvPath(engagementId, home), "utf-8"));
|
|
174
|
-
} catch {
|
|
175
|
-
return null;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
function writeEngagementEnv(engagementId, values, home) {
|
|
179
|
-
const path = engagementEnvPath(engagementId, home);
|
|
180
|
-
const dir = engagementStateDir(engagementId, home);
|
|
181
|
-
mkdirSync2(dir, { recursive: true, mode: 448 });
|
|
182
|
-
let existing;
|
|
183
|
-
try {
|
|
184
|
-
existing = readFileSync2(path, "utf-8");
|
|
185
|
-
} catch {
|
|
186
|
-
existing = null;
|
|
187
|
-
}
|
|
188
|
-
const reconciled = reconcileEnvText(existing, values);
|
|
189
|
-
const outcome = existing === reconciled ? "skipped" : "written";
|
|
190
|
-
if (outcome === "written") {
|
|
191
|
-
writeFileSync2(path, reconciled, { mode: 384 });
|
|
192
|
-
}
|
|
193
|
-
applyOwnerOnly(path, dir);
|
|
194
|
-
return outcome;
|
|
195
|
-
}
|
|
196
|
-
function applyOwnerOnly(path, dir) {
|
|
197
|
-
if (platform2() === "win32")
|
|
198
|
-
return;
|
|
199
|
-
try {
|
|
200
|
-
chmodSync2(path, 384);
|
|
201
|
-
} catch {
|
|
202
|
-
}
|
|
203
|
-
try {
|
|
204
|
-
chmodSync2(dir, 448);
|
|
205
|
-
} catch {
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
function engagementResolutionShell() {
|
|
209
|
-
return `# --- Halfcycle credential resolution (generated; do not edit) ----------------
|
|
210
|
-
# THE CREDENTIAL IS NOT IN THIS REPOSITORY. It lives at
|
|
211
|
-
# $HOME/.halfcycle/${ENGAGEMENTS_DIR}/<engagement-id>/${ENV_FILENAME}
|
|
212
|
-
# written owner-only by \`npx halfcycle\`. What this repository holds is the
|
|
213
|
-
# engagement id, in the committed .halfcycle/bundle.json. So: read the id out of
|
|
214
|
-
# the pin, then name the file. There is no jq here and node is not dependable on a
|
|
215
|
-
# desktop session's PATH, so the id is cut out with sed \u2014 after flattening the
|
|
216
|
-
# file, because sed is line-oriented and the pin's line breaks are not a contract.
|
|
217
|
-
#
|
|
218
|
-
# Sets HALFCYCLE_ENV_FILE, HALFCYCLE_ENV_ENGAGEMENT and HALFCYCLE_ENV_PROBLEM in
|
|
219
|
-
# the CALLER's shell and returns a status. Call it as \`if halfcycle_env_file \u2026\`,
|
|
220
|
-
# never as \`$(halfcycle_env_file \u2026)\` \u2014 a command substitution is a subshell and
|
|
221
|
-
# would discard everything it set.
|
|
222
|
-
halfcycle_env_file() {
|
|
223
|
-
HALFCYCLE_ENV_FILE=""
|
|
224
|
-
HALFCYCLE_ENV_ENGAGEMENT=""
|
|
225
|
-
HALFCYCLE_ENV_PROBLEM=""
|
|
226
|
-
hc_pin="$1/.halfcycle/bundle.json"
|
|
227
|
-
if [ ! -f "$hc_pin" ]; then
|
|
228
|
-
HALFCYCLE_ENV_PROBLEM="no-pin"
|
|
229
|
-
return 1
|
|
230
|
-
fi
|
|
231
|
-
hc_id=$(tr -d '\\n' < "$hc_pin" | sed -n 's/.*"${PIN_ENGAGEMENT_ID_FIELD}"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p')
|
|
232
|
-
if [ -z "$hc_id" ]; then
|
|
233
|
-
HALFCYCLE_ENV_PROBLEM="no-id"
|
|
234
|
-
return 1
|
|
235
|
-
fi
|
|
236
|
-
HALFCYCLE_ENV_ENGAGEMENT="$hc_id"
|
|
237
|
-
if [ -z "\${HOME:-}" ]; then
|
|
238
|
-
HALFCYCLE_ENV_PROBLEM="no-home"
|
|
239
|
-
return 1
|
|
240
|
-
fi
|
|
241
|
-
hc_env="$HOME/.halfcycle/${ENGAGEMENTS_DIR}/$hc_id/${ENV_FILENAME}"
|
|
242
|
-
if [ ! -f "$hc_env" ]; then
|
|
243
|
-
HALFCYCLE_ENV_PROBLEM="no-credential"
|
|
244
|
-
return 1
|
|
245
|
-
fi
|
|
246
|
-
HALFCYCLE_ENV_FILE="$hc_env"
|
|
247
|
-
return 0
|
|
248
|
-
}
|
|
249
|
-
# --- end Halfcycle credential resolution ------------------------------------`;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// dist/identity.js
|
|
253
|
-
import { execFileSync } from "node:child_process";
|
|
254
|
-
import { existsSync, readFileSync as readFileSync3 } from "node:fs";
|
|
255
|
-
import { join as join3 } from "node:path";
|
|
256
|
-
import { randomUUID } from "node:crypto";
|
|
257
|
-
var PROJECT_IDENTITY_NOTE = 'projectId is NOT the engagement id \u2014 it identifies this repository/worktree to Halfcycle, not an engagement. The engagement id lives in .halfcycle/bundle.json, in its "engagementId" field.';
|
|
258
|
-
function git(targetRepoRoot, args2) {
|
|
259
|
-
try {
|
|
260
|
-
const out = execFileSync("git", ["-C", targetRepoRoot, ...args2], {
|
|
261
|
-
encoding: "utf-8",
|
|
262
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
263
|
-
});
|
|
264
|
-
const trimmed = out.trim();
|
|
265
|
-
return trimmed.length > 0 ? trimmed : null;
|
|
266
|
-
} catch {
|
|
267
|
-
return null;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
function readRootCommit(targetRepoRoot) {
|
|
271
|
-
const roots = git(targetRepoRoot, ["rev-list", "--max-parents=0", "HEAD"]);
|
|
272
|
-
if (!roots)
|
|
273
|
-
return null;
|
|
274
|
-
const lines = roots.split("\n").filter((l) => l.length > 0);
|
|
275
|
-
return lines.length > 0 ? lines[lines.length - 1] : null;
|
|
276
|
-
}
|
|
277
|
-
function readRemote(targetRepoRoot) {
|
|
278
|
-
return git(targetRepoRoot, ["remote", "get-url", "origin"]);
|
|
279
|
-
}
|
|
280
|
-
function mintOrReadIdentity(targetRepoRoot) {
|
|
281
|
-
const path = join3(targetRepoRoot, ".halfcycle", "project.json");
|
|
282
|
-
if (existsSync(path)) {
|
|
283
|
-
const onDisk = JSON.parse(readFileSync3(path, "utf-8"));
|
|
284
|
-
const identity2 = {
|
|
285
|
-
projectId: onDisk.projectId ?? "",
|
|
286
|
-
rootCommit: onDisk.rootCommit ?? null,
|
|
287
|
-
remote: onDisk.remote ?? null,
|
|
288
|
-
note: PROJECT_IDENTITY_NOTE
|
|
289
|
-
};
|
|
290
|
-
return { identity: identity2, minted: false };
|
|
291
|
-
}
|
|
292
|
-
const identity = {
|
|
293
|
-
projectId: randomUUID(),
|
|
294
|
-
rootCommit: readRootCommit(targetRepoRoot),
|
|
295
|
-
remote: readRemote(targetRepoRoot),
|
|
296
|
-
note: PROJECT_IDENTITY_NOTE
|
|
297
|
-
};
|
|
298
|
-
return { identity, minted: true };
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// dist/mcp-endpoint.js
|
|
302
|
-
var MCP_ENDPOINT_PATH = "/mcp";
|
|
303
|
-
function mcpEndpointUrl(origin) {
|
|
304
|
-
return `${origin.trim().replace(/\/+$/, "")}${MCP_ENDPOINT_PATH}`;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// dist/merge-settings.js
|
|
308
|
-
function isHalfcycleEntry(entry, halfcycleCommands) {
|
|
309
|
-
return entry.hooks.some((h) => halfcycleCommands.has(h.command));
|
|
310
|
-
}
|
|
311
|
-
function mergeSettings(existing, generated) {
|
|
312
|
-
const merged = { ...existing };
|
|
313
|
-
if (generated.$schema !== void 0) {
|
|
314
|
-
merged.$schema = generated.$schema;
|
|
315
|
-
}
|
|
316
|
-
const generatedHooks = generated.hooks ?? {};
|
|
317
|
-
const halfcycleCommands = /* @__PURE__ */ new Set();
|
|
318
|
-
for (const entries of Object.values(generatedHooks)) {
|
|
319
|
-
for (const entry of entries) {
|
|
320
|
-
for (const h of entry.hooks)
|
|
321
|
-
halfcycleCommands.add(h.command);
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
const existingHooks = existing.hooks ?? {};
|
|
325
|
-
const mergedHooks = {};
|
|
326
|
-
const events = /* @__PURE__ */ new Set([...Object.keys(existingHooks), ...Object.keys(generatedHooks)]);
|
|
327
|
-
for (const event of events) {
|
|
328
|
-
const developerEntries = (existingHooks[event] ?? []).filter((entry) => !isHalfcycleEntry(entry, halfcycleCommands));
|
|
329
|
-
const halfcycleEntries = generatedHooks[event] ?? [];
|
|
330
|
-
mergedHooks[event] = [...developerEntries, ...halfcycleEntries];
|
|
331
|
-
}
|
|
332
|
-
merged.hooks = mergedHooks;
|
|
333
|
-
merged.permissions = mergePermissions(existing.permissions, generated.permissions);
|
|
334
|
-
return merged;
|
|
335
|
-
}
|
|
336
|
-
function mergePermissions(existing, generated) {
|
|
337
|
-
if (existing === void 0 && generated === void 0)
|
|
338
|
-
return void 0;
|
|
339
|
-
const result = { ...existing ?? {} };
|
|
340
|
-
const generatedDeny = generated?.deny ?? [];
|
|
341
|
-
if (generatedDeny.length > 0) {
|
|
342
|
-
const theirs = existing?.deny ?? [];
|
|
343
|
-
const seen = new Set(theirs);
|
|
344
|
-
result.deny = [...theirs, ...generatedDeny.filter((p) => !seen.has(p))];
|
|
345
|
-
}
|
|
346
|
-
return result;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
// dist/scan.js
|
|
350
|
-
import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
|
|
351
|
-
import { join as join4 } from "node:path";
|
|
352
|
-
|
|
353
13
|
// ../events/dist/result.js
|
|
354
14
|
import { z as z2 } from "zod";
|
|
355
15
|
|
|
@@ -538,177 +198,572 @@ var acceptPhaseRequestSchema = z4.object({
|
|
|
538
198
|
acceptanceOutcome: acceptanceOutcomeSchema,
|
|
539
199
|
close: phaseCloseDecisionSchema.optional()
|
|
540
200
|
}).strict();
|
|
541
|
-
var coeFindingRequestSchema = z4.object({
|
|
542
|
-
phase: z4.string(),
|
|
543
|
-
description: z4.string(),
|
|
544
|
-
addRateTag: z4.enum(["seam-new", "seam-repeat", "model-limitation"]),
|
|
545
|
-
mappedPatternRef: z4.string().nullable()
|
|
201
|
+
var coeFindingRequestSchema = z4.object({
|
|
202
|
+
phase: z4.string(),
|
|
203
|
+
description: z4.string(),
|
|
204
|
+
addRateTag: z4.enum(["seam-new", "seam-repeat", "model-limitation"]),
|
|
205
|
+
mappedPatternRef: z4.string().nullable()
|
|
206
|
+
}).strict();
|
|
207
|
+
var coeFindingResponseSchema = z4.object({
|
|
208
|
+
findingId: z4.string(),
|
|
209
|
+
engagementId: z4.string(),
|
|
210
|
+
phase: z4.string(),
|
|
211
|
+
description: z4.string(),
|
|
212
|
+
addRateTag: z4.enum(["seam-new", "seam-repeat", "model-limitation"]),
|
|
213
|
+
mappedPatternRef: z4.string().nullable(),
|
|
214
|
+
createdAt: utcIso86012
|
|
215
|
+
}).strict();
|
|
216
|
+
var phaseEntryDecisionSchema = z4.discriminatedUnion("decision", [
|
|
217
|
+
z4.object({
|
|
218
|
+
decision: z4.literal("entry-condition-met"),
|
|
219
|
+
/** Who attests. */
|
|
220
|
+
actor: z4.string(),
|
|
221
|
+
/** What makes the entry condition hold, in the attester's own words. */
|
|
222
|
+
evidence: z4.string()
|
|
223
|
+
}).strict(),
|
|
224
|
+
z4.object({
|
|
225
|
+
decision: z4.literal("override"),
|
|
226
|
+
/** Who is opening it anyway. */
|
|
227
|
+
actor: z4.string(),
|
|
228
|
+
/** Why, in the operator's own words. */
|
|
229
|
+
reason: z4.string()
|
|
230
|
+
}).strict()
|
|
231
|
+
]);
|
|
232
|
+
var PHASE_ENTRY_DECISION_KINDS = [
|
|
233
|
+
"entry-condition-met",
|
|
234
|
+
"override"
|
|
235
|
+
];
|
|
236
|
+
var PHASE_CLOSE_DECISION_KINDS = ["phase-closed", "phase-close-override"];
|
|
237
|
+
var phaseDecisionKindSchema = z4.enum([
|
|
238
|
+
...PHASE_ENTRY_DECISION_KINDS,
|
|
239
|
+
...PHASE_CLOSE_DECISION_KINDS
|
|
240
|
+
]);
|
|
241
|
+
var openPhaseRequestSchema = z4.object({
|
|
242
|
+
phase: z4.string().nullable(),
|
|
243
|
+
entry: phaseEntryDecisionSchema
|
|
244
|
+
}).strict();
|
|
245
|
+
var phaseEntryDecisionRecordSchema = z4.object({
|
|
246
|
+
decisionId: z4.string(),
|
|
247
|
+
decision: z4.enum(PHASE_ENTRY_DECISION_KINDS),
|
|
248
|
+
actor: z4.string(),
|
|
249
|
+
/** The evidence (attestation) or the reason (override) — one basis per row. */
|
|
250
|
+
justification: z4.string(),
|
|
251
|
+
blockingFinding: z4.string().nullable(),
|
|
252
|
+
decidedAt: utcIso86012
|
|
253
|
+
}).strict();
|
|
254
|
+
var openPhaseResponseSchema = z4.object({
|
|
255
|
+
engagement: engagementStatusResponseSchema,
|
|
256
|
+
/** What is open now. `null` is project scope. */
|
|
257
|
+
phase: z4.string().nullable(),
|
|
258
|
+
/** What was open before this decision. `null` is project scope. */
|
|
259
|
+
previousPhase: z4.string().nullable(),
|
|
260
|
+
decision: phaseEntryDecisionRecordSchema
|
|
261
|
+
}).strict();
|
|
262
|
+
|
|
263
|
+
// ../events/dist/crew.js
|
|
264
|
+
import { z as z5 } from "zod";
|
|
265
|
+
var crewGroupSchema = z5.enum(["builder", "reviewer", "always-on"]);
|
|
266
|
+
var crewTierSchema = z5.enum(["deep", "standard", "fast"]);
|
|
267
|
+
var CREW_DISCIPLINE_MAX = 64;
|
|
268
|
+
var crewMemberSchema = z5.object({
|
|
269
|
+
// The agent's name. The same twenty-one on every project, so a name is a
|
|
270
|
+
// vocabulary a returning client already knows.
|
|
271
|
+
callsign: z5.string().min(1).max(32),
|
|
272
|
+
// Which of the three headings this member reads under.
|
|
273
|
+
group: crewGroupSchema,
|
|
274
|
+
// The kind of work this member takes. A task names a discipline, and only
|
|
275
|
+
// agents holding it can be dispatched to it.
|
|
276
|
+
discipline: z5.string().min(1).max(CREW_DISCIPLINE_MAX),
|
|
277
|
+
// How much thinking this member brings. There is no field naming the supplier
|
|
278
|
+
// behind it, on this shape or on any other.
|
|
279
|
+
tier: crewTierSchema
|
|
280
|
+
}).strict();
|
|
281
|
+
var CREW_ROSTER = [
|
|
282
|
+
// ── builders: code ──────────────────────────────────────────────────────────
|
|
283
|
+
{ callsign: "ADA", group: "builder", discipline: "implementation", tier: "deep" },
|
|
284
|
+
{ callsign: "OTTO", group: "builder", discipline: "implementation", tier: "deep" },
|
|
285
|
+
{ callsign: "MILO", group: "builder", discipline: "implementation", tier: "standard" },
|
|
286
|
+
{ callsign: "NOVA", group: "builder", discipline: "implementation", tier: "standard" },
|
|
287
|
+
{ callsign: "KAI", group: "builder", discipline: "implementation", tier: "standard" },
|
|
288
|
+
{ callsign: "HUGO", group: "builder", discipline: "infrastructure", tier: "standard" },
|
|
289
|
+
{ callsign: "WREN", group: "builder", discipline: "refactor", tier: "fast" },
|
|
290
|
+
{ callsign: "PIP", group: "builder", discipline: "refactor", tier: "fast" },
|
|
291
|
+
// ── builders: specs and docs ────────────────────────────────────────────────
|
|
292
|
+
{ callsign: "IRIS", group: "builder", discipline: "feature spec", tier: "deep" },
|
|
293
|
+
{ callsign: "JUNO", group: "builder", discipline: "feature spec", tier: "standard" },
|
|
294
|
+
{ callsign: "LEX", group: "builder", discipline: "context & docs", tier: "fast" },
|
|
295
|
+
// ── reviewers ───────────────────────────────────────────────────────────────
|
|
296
|
+
{ callsign: "VERA", group: "reviewer", discipline: "spec consistency", tier: "deep" },
|
|
297
|
+
{ callsign: "ODIN", group: "reviewer", discipline: "cross-spec audit", tier: "deep" },
|
|
298
|
+
{ callsign: "CASS", group: "reviewer", discipline: "code review", tier: "deep" },
|
|
299
|
+
{ callsign: "THEO", group: "reviewer", discipline: "code review", tier: "standard" },
|
|
300
|
+
{ callsign: "ECHO", group: "reviewer", discipline: "test quality", tier: "standard" },
|
|
301
|
+
{ callsign: "MAVIS", group: "reviewer", discipline: "suites & coverage", tier: "standard" },
|
|
302
|
+
{ callsign: "ZARA", group: "reviewer", discipline: "smoke & walk aid", tier: "standard" },
|
|
303
|
+
{ callsign: "NORA", group: "reviewer", discipline: "evidence & provenance", tier: "fast" },
|
|
304
|
+
// ── always on: continuous, takes no task ────────────────────────────────────
|
|
305
|
+
{ callsign: "ARGUS", group: "always-on", discipline: "guard \xB7 every diff", tier: "standard" },
|
|
306
|
+
{ callsign: "ATLAS", group: "always-on", discipline: "coordinator", tier: "deep" }
|
|
307
|
+
];
|
|
308
|
+
var CREW_ROSTER_SIZE = CREW_ROSTER.length;
|
|
309
|
+
var CREW_CALLSIGNS = CREW_ROSTER.map((member) => member.callsign);
|
|
310
|
+
|
|
311
|
+
// ../events/dist/device-auth.js
|
|
312
|
+
import { z as z6 } from "zod";
|
|
313
|
+
var utcIso86013 = z6.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/, "must be a UTC ISO-8601 timestamp ending in Z (INV-015)");
|
|
314
|
+
var DEVICE_AUTH_STATUS = {
|
|
315
|
+
/** The poll: minted, nobody has acted yet. Keep polling. 200. */
|
|
316
|
+
PENDING: "pending",
|
|
317
|
+
/** The poll: approved, and the credential is in this body. Once only. 200. */
|
|
318
|
+
APPROVED: "approved",
|
|
319
|
+
/** Confirm: the decision was recorded (approve or decline). 200. */
|
|
320
|
+
RECORDED: "recorded",
|
|
321
|
+
/** The human refused at the confirmation page. 400 on the poll. */
|
|
322
|
+
DECLINED: "declined",
|
|
323
|
+
/** The code's absolute expiry has passed. 400. */
|
|
324
|
+
EXPIRED: "expired",
|
|
325
|
+
/** Never minted, or already spent. 400. */
|
|
326
|
+
UNKNOWN: "unknown",
|
|
327
|
+
/** Confirm: neither `approve` nor `decline` was named. Nothing was bound. 400. */
|
|
328
|
+
INVALID_DECISION: "invalid-decision",
|
|
329
|
+
/** Confirm: an approval arrived with no signed-in identity. 400. */
|
|
330
|
+
IDENTITY_REQUIRED: "identity-required",
|
|
331
|
+
/** This deployment has no `SIGNIN_BASE_URL` / `DEVICE_AUTH_CONFIRM_SECRET`. 503. */
|
|
332
|
+
NOT_CONFIGURED: "not-configured",
|
|
333
|
+
/** Confirm: the shared confirmation secret was missing or wrong. 401. */
|
|
334
|
+
UNAUTHORIZED: "unauthorized",
|
|
335
|
+
/** Confirm: the code was already approved, declined or claimed. 409. */
|
|
336
|
+
ALREADY_DECIDED: "already-decided",
|
|
337
|
+
/**
|
|
338
|
+
* Confirm: an approval named a signed-in identity whose account has not accepted
|
|
339
|
+
* the terms of service and privacy policy currently in force (T-18, #588). 400.
|
|
340
|
+
*
|
|
341
|
+
* NOT BOUND TO THE DEVICE CODE. Unlike every other refusal in this const, this one
|
|
342
|
+
* says nothing about the code itself — the code is still `pending` after this
|
|
343
|
+
* response, exactly as it was before the request, so the SAME userCode can be
|
|
344
|
+
* confirmed again once the caller has recorded acceptance. `deviceConfirmRequestSchema`'s
|
|
345
|
+
* `acceptTerms` is how a second confirm says it did.
|
|
346
|
+
*/
|
|
347
|
+
TERMS_REQUIRED: "terms-required"
|
|
348
|
+
};
|
|
349
|
+
var DEVICE_POLL_REFUSALS = [
|
|
350
|
+
DEVICE_AUTH_STATUS.DECLINED,
|
|
351
|
+
DEVICE_AUTH_STATUS.EXPIRED,
|
|
352
|
+
DEVICE_AUTH_STATUS.UNKNOWN
|
|
353
|
+
];
|
|
354
|
+
var deviceAuthStartResponseSchema = z6.object({
|
|
355
|
+
deviceCode: z6.string().min(1),
|
|
356
|
+
userCode: z6.string().min(1),
|
|
357
|
+
verificationUrl: z6.string().min(1),
|
|
358
|
+
expiresAt: utcIso86013,
|
|
359
|
+
pollIntervalMs: z6.number().int().positive()
|
|
360
|
+
}).strict();
|
|
361
|
+
var devicePollPendingSchema = z6.object({
|
|
362
|
+
status: z6.literal(DEVICE_AUTH_STATUS.PENDING),
|
|
363
|
+
pollIntervalMs: z6.number().int().positive()
|
|
546
364
|
}).strict();
|
|
547
|
-
var
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
addRateTag: z4.enum(["seam-new", "seam-repeat", "model-limitation"]),
|
|
553
|
-
mappedPatternRef: z4.string().nullable(),
|
|
554
|
-
createdAt: utcIso86012
|
|
365
|
+
var devicePollApprovedSchema = z6.object({
|
|
366
|
+
status: z6.literal(DEVICE_AUTH_STATUS.APPROVED),
|
|
367
|
+
accountId: z6.string().min(1),
|
|
368
|
+
credential: z6.string().min(1),
|
|
369
|
+
expiresAt: utcIso86013
|
|
555
370
|
}).strict();
|
|
556
|
-
var
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
reason: z4.string()
|
|
570
|
-
}).strict()
|
|
571
|
-
]);
|
|
572
|
-
var openPhaseRequestSchema = z4.object({
|
|
573
|
-
phase: z4.string().nullable(),
|
|
574
|
-
entry: phaseEntryDecisionSchema
|
|
371
|
+
var deviceAuthRefusalSchema = z6.object({
|
|
372
|
+
status: z6.enum([
|
|
373
|
+
DEVICE_AUTH_STATUS.DECLINED,
|
|
374
|
+
DEVICE_AUTH_STATUS.EXPIRED,
|
|
375
|
+
DEVICE_AUTH_STATUS.UNKNOWN,
|
|
376
|
+
DEVICE_AUTH_STATUS.INVALID_DECISION,
|
|
377
|
+
DEVICE_AUTH_STATUS.IDENTITY_REQUIRED,
|
|
378
|
+
DEVICE_AUTH_STATUS.NOT_CONFIGURED,
|
|
379
|
+
DEVICE_AUTH_STATUS.UNAUTHORIZED,
|
|
380
|
+
DEVICE_AUTH_STATUS.ALREADY_DECIDED,
|
|
381
|
+
DEVICE_AUTH_STATUS.TERMS_REQUIRED
|
|
382
|
+
]),
|
|
383
|
+
message: z6.string().min(1)
|
|
575
384
|
}).strict();
|
|
576
|
-
var
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
385
|
+
var devicePollResponseSchema = z6.union([
|
|
386
|
+
devicePollPendingSchema,
|
|
387
|
+
devicePollApprovedSchema,
|
|
388
|
+
deviceAuthRefusalSchema
|
|
389
|
+
]);
|
|
390
|
+
var deviceConfirmRequestSchema = z6.object({
|
|
391
|
+
userCode: z6.string().min(1),
|
|
392
|
+
decision: z6.enum(["approve", "decline"]),
|
|
393
|
+
externalAuthId: z6.string().min(1).optional(),
|
|
394
|
+
email: z6.string().optional(),
|
|
395
|
+
acceptTerms: z6.boolean().optional()
|
|
584
396
|
}).strict();
|
|
585
|
-
var
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
phase: z4.string().nullable(),
|
|
589
|
-
/** What was open before this decision. `null` is project scope. */
|
|
590
|
-
previousPhase: z4.string().nullable(),
|
|
591
|
-
decision: phaseEntryDecisionRecordSchema
|
|
397
|
+
var deviceConfirmRecordedSchema = z6.object({
|
|
398
|
+
status: z6.literal(DEVICE_AUTH_STATUS.RECORDED),
|
|
399
|
+
accountId: z6.string().min(1).optional()
|
|
592
400
|
}).strict();
|
|
401
|
+
var deviceConfirmResponseSchema = z6.union([
|
|
402
|
+
deviceConfirmRecordedSchema,
|
|
403
|
+
deviceAuthRefusalSchema
|
|
404
|
+
]);
|
|
405
|
+
var LOOPBACK_REDIRECT_HOST = "127.0.0.1";
|
|
406
|
+
var LOOPBACK_PORT_MIN = 1024;
|
|
407
|
+
var LOOPBACK_PORT_MAX = 65535;
|
|
408
|
+
var LOOPBACK_CALLBACK_PATH = "/halfcycle-cli-callback";
|
|
409
|
+
var LOOPBACK_QUERY_PORT = "loopback_port";
|
|
410
|
+
var LOOPBACK_QUERY_STATE = "loopback_state";
|
|
411
|
+
var LOOPBACK_QUERY_RESULT = "result";
|
|
412
|
+
var LOOPBACK_RESULT = {
|
|
413
|
+
APPROVED: "approved",
|
|
414
|
+
DECLINED: "declined"
|
|
415
|
+
};
|
|
416
|
+
function loopbackVerificationUrl(verificationUrl, target) {
|
|
417
|
+
let url;
|
|
418
|
+
try {
|
|
419
|
+
url = new URL(verificationUrl);
|
|
420
|
+
} catch {
|
|
421
|
+
return verificationUrl;
|
|
422
|
+
}
|
|
423
|
+
url.searchParams.set(LOOPBACK_QUERY_PORT, String(target.port));
|
|
424
|
+
url.searchParams.set(LOOPBACK_QUERY_STATE, target.state);
|
|
425
|
+
return url.toString();
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// dist/engagement-credential.js
|
|
429
|
+
import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
430
|
+
import { platform as platform2 } from "node:os";
|
|
431
|
+
import { join as join2 } from "node:path";
|
|
432
|
+
|
|
433
|
+
// dist/account-credential.js
|
|
434
|
+
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
435
|
+
import { homedir, platform } from "node:os";
|
|
436
|
+
import { dirname, join } from "node:path";
|
|
437
|
+
function halfcycleHome(home) {
|
|
438
|
+
return join(home ?? homedir(), ".halfcycle");
|
|
439
|
+
}
|
|
440
|
+
function accountStorePath(home) {
|
|
441
|
+
return join(halfcycleHome(home), "account.json");
|
|
442
|
+
}
|
|
443
|
+
function normaliseOrigin(serviceUrl) {
|
|
444
|
+
return serviceUrl.trim().replace(/\/+$/, "").toLowerCase();
|
|
445
|
+
}
|
|
446
|
+
function readStore(home) {
|
|
447
|
+
let raw;
|
|
448
|
+
try {
|
|
449
|
+
raw = readFileSync(accountStorePath(home), "utf-8");
|
|
450
|
+
} catch {
|
|
451
|
+
return { version: 1, accounts: {} };
|
|
452
|
+
}
|
|
453
|
+
try {
|
|
454
|
+
const parsed = JSON.parse(raw);
|
|
455
|
+
if (!parsed || typeof parsed !== "object" || typeof parsed.accounts !== "object") {
|
|
456
|
+
return { version: 1, accounts: {} };
|
|
457
|
+
}
|
|
458
|
+
return { version: 1, accounts: parsed.accounts ?? {} };
|
|
459
|
+
} catch {
|
|
460
|
+
return { version: 1, accounts: {} };
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
function writeStore(store, home) {
|
|
464
|
+
const path = accountStorePath(home);
|
|
465
|
+
mkdirSync(dirname(path), { recursive: true, mode: 448 });
|
|
466
|
+
writeFileSync(path, `${JSON.stringify(store, null, 2)}
|
|
467
|
+
`, { mode: 384 });
|
|
468
|
+
if (platform() !== "win32") {
|
|
469
|
+
chmodSync(path, 384);
|
|
470
|
+
try {
|
|
471
|
+
chmodSync(dirname(path), 448);
|
|
472
|
+
} catch {
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
function readStoredCredential(serviceUrl, home) {
|
|
477
|
+
const entry = readStore(home).accounts[normaliseOrigin(serviceUrl)];
|
|
478
|
+
if (!entry || typeof entry.credential !== "string" || entry.credential.trim() === "") {
|
|
479
|
+
return void 0;
|
|
480
|
+
}
|
|
481
|
+
return entry;
|
|
482
|
+
}
|
|
483
|
+
function writeStoredCredential(serviceUrl, entry, home) {
|
|
484
|
+
const store = readStore(home);
|
|
485
|
+
store.accounts[normaliseOrigin(serviceUrl)] = {
|
|
486
|
+
accountId: entry.accountId,
|
|
487
|
+
credential: entry.credential,
|
|
488
|
+
expiresAt: entry.expiresAt,
|
|
489
|
+
signedInAt: entry.signedInAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
490
|
+
};
|
|
491
|
+
writeStore(store, home);
|
|
492
|
+
}
|
|
493
|
+
function forgetStoredCredential(serviceUrl, home) {
|
|
494
|
+
const store = readStore(home);
|
|
495
|
+
const key = normaliseOrigin(serviceUrl);
|
|
496
|
+
if (!(key in store.accounts))
|
|
497
|
+
return false;
|
|
498
|
+
delete store.accounts[key];
|
|
499
|
+
writeStore(store, home);
|
|
500
|
+
return true;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// dist/engagement-credential.js
|
|
504
|
+
var ENGAGEMENTS_DIR = "engagements";
|
|
505
|
+
var ENV_FILENAME = "env";
|
|
506
|
+
var PIN_ENGAGEMENT_ID_FIELD = "engagementId";
|
|
507
|
+
function engagementStateDir(engagementId, home) {
|
|
508
|
+
return join2(halfcycleHome(home), ENGAGEMENTS_DIR, engagementId);
|
|
509
|
+
}
|
|
510
|
+
function engagementEnvPath(engagementId, home) {
|
|
511
|
+
return join2(engagementStateDir(engagementId, home), ENV_FILENAME);
|
|
512
|
+
}
|
|
513
|
+
var GUARD_ENV_KEYS = [
|
|
514
|
+
"GUARD_SERVICE_URL",
|
|
515
|
+
"GUARD_SERVICE_TOKEN",
|
|
516
|
+
"GUARD_ENGAGEMENT_ID"
|
|
517
|
+
];
|
|
518
|
+
var ENGAGEMENT_ENV_KEYS = [
|
|
519
|
+
"HALFCYCLE_SERVICE_URL",
|
|
520
|
+
"HALFCYCLE_MCP_URL",
|
|
521
|
+
"HALFCYCLE_TOKEN",
|
|
522
|
+
"HALFCYCLE_ENGAGEMENT_ID",
|
|
523
|
+
...GUARD_ENV_KEYS,
|
|
524
|
+
"CONTROL_TELEMETRY_URL"
|
|
525
|
+
];
|
|
526
|
+
var ENV_HEADER = "# Halfcycle per-engagement credential \u2014 machine level, owner-only, never in a repository.";
|
|
527
|
+
function shq(value) {
|
|
528
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
529
|
+
}
|
|
530
|
+
function unquote(value) {
|
|
531
|
+
const v = value.trim();
|
|
532
|
+
if (v.length >= 2 && v.startsWith("'") && v.endsWith("'")) {
|
|
533
|
+
return v.slice(1, -1).split(`'\\''`).join(`'`);
|
|
534
|
+
}
|
|
535
|
+
if (v.length >= 2 && v.startsWith('"') && v.endsWith('"'))
|
|
536
|
+
return v.slice(1, -1);
|
|
537
|
+
return v;
|
|
538
|
+
}
|
|
539
|
+
function parseEnvText(raw) {
|
|
540
|
+
const out = {};
|
|
541
|
+
for (const line of raw.split("\n")) {
|
|
542
|
+
const eq = line.indexOf("=");
|
|
543
|
+
if (eq === -1)
|
|
544
|
+
continue;
|
|
545
|
+
const key = line.slice(0, eq).replace(/^\s*export\s+/, "").trim();
|
|
546
|
+
if (key === "" || key.startsWith("#"))
|
|
547
|
+
continue;
|
|
548
|
+
out[key] = unquote(line.slice(eq + 1));
|
|
549
|
+
}
|
|
550
|
+
return out;
|
|
551
|
+
}
|
|
552
|
+
function reconcileEnvText(existing, values, keys = ENGAGEMENT_ENV_KEYS) {
|
|
553
|
+
const desired = new Map(keys.map((k) => [k, values[k] ?? ""]));
|
|
554
|
+
const line = (k) => `${k}=${shq(desired.get(k) ?? "")}`;
|
|
555
|
+
if (existing === null) {
|
|
556
|
+
return `${ENV_HEADER}
|
|
557
|
+
` + keys.map(line).join("\n") + "\n";
|
|
558
|
+
}
|
|
559
|
+
const seen = /* @__PURE__ */ new Set();
|
|
560
|
+
const out = existing.split("\n").map((existingLine) => {
|
|
561
|
+
const eq = existingLine.indexOf("=");
|
|
562
|
+
if (eq === -1)
|
|
563
|
+
return existingLine;
|
|
564
|
+
const lhs = existingLine.slice(0, eq);
|
|
565
|
+
const exported = /^\s*export\s+/.exec(lhs);
|
|
566
|
+
const prefix = exported === null ? "" : exported[0];
|
|
567
|
+
const key = lhs.slice(prefix.length).trim();
|
|
568
|
+
if (desired.has(key)) {
|
|
569
|
+
seen.add(key);
|
|
570
|
+
return `${prefix}${line(key)}`;
|
|
571
|
+
}
|
|
572
|
+
return existingLine;
|
|
573
|
+
});
|
|
574
|
+
const missing = keys.filter((k) => !seen.has(k));
|
|
575
|
+
if (missing.length > 0) {
|
|
576
|
+
const trailingBlank = out.length > 0 && out[out.length - 1] === "";
|
|
577
|
+
const prefix = trailingBlank ? "" : existing.endsWith("\n") ? "" : "\n";
|
|
578
|
+
out.push(`${prefix}${ENV_HEADER}
|
|
579
|
+
` + missing.map(line).join("\n"));
|
|
580
|
+
}
|
|
581
|
+
let result = out.join("\n");
|
|
582
|
+
if (existing.endsWith("\n") && !result.endsWith("\n"))
|
|
583
|
+
result += "\n";
|
|
584
|
+
return result;
|
|
585
|
+
}
|
|
586
|
+
function readEngagementEnv(engagementId, home) {
|
|
587
|
+
try {
|
|
588
|
+
return parseEnvText(readFileSync2(engagementEnvPath(engagementId, home), "utf-8"));
|
|
589
|
+
} catch {
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
function writeEngagementEnv(engagementId, values, home) {
|
|
594
|
+
const path = engagementEnvPath(engagementId, home);
|
|
595
|
+
const dir = engagementStateDir(engagementId, home);
|
|
596
|
+
mkdirSync2(dir, { recursive: true, mode: 448 });
|
|
597
|
+
let existing;
|
|
598
|
+
try {
|
|
599
|
+
existing = readFileSync2(path, "utf-8");
|
|
600
|
+
} catch {
|
|
601
|
+
existing = null;
|
|
602
|
+
}
|
|
603
|
+
const reconciled = reconcileEnvText(existing, values);
|
|
604
|
+
const outcome = existing === reconciled ? "skipped" : "written";
|
|
605
|
+
if (outcome === "written") {
|
|
606
|
+
writeFileSync2(path, reconciled, { mode: 384 });
|
|
607
|
+
}
|
|
608
|
+
applyOwnerOnly(path, dir);
|
|
609
|
+
return outcome;
|
|
610
|
+
}
|
|
611
|
+
function applyOwnerOnly(path, dir) {
|
|
612
|
+
if (platform2() === "win32")
|
|
613
|
+
return;
|
|
614
|
+
try {
|
|
615
|
+
chmodSync2(path, 384);
|
|
616
|
+
} catch {
|
|
617
|
+
}
|
|
618
|
+
try {
|
|
619
|
+
chmodSync2(dir, 448);
|
|
620
|
+
} catch {
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
function engagementResolutionShell() {
|
|
624
|
+
return `# --- Halfcycle credential resolution (generated; do not edit) ----------------
|
|
625
|
+
# THE CREDENTIAL IS NOT IN THIS REPOSITORY. It lives at
|
|
626
|
+
# $HOME/.halfcycle/${ENGAGEMENTS_DIR}/<engagement-id>/${ENV_FILENAME}
|
|
627
|
+
# written owner-only by \`npx halfcycle\`. What this repository holds is the
|
|
628
|
+
# engagement id, in the committed .halfcycle/bundle.json. So: read the id out of
|
|
629
|
+
# the pin, then name the file. There is no jq here and node is not dependable on a
|
|
630
|
+
# desktop session's PATH, so the id is cut out with sed \u2014 after flattening the
|
|
631
|
+
# file, because sed is line-oriented and the pin's line breaks are not a contract.
|
|
632
|
+
#
|
|
633
|
+
# Sets HALFCYCLE_ENV_FILE, HALFCYCLE_ENV_ENGAGEMENT and HALFCYCLE_ENV_PROBLEM in
|
|
634
|
+
# the CALLER's shell and returns a status. Call it as \`if halfcycle_env_file \u2026\`,
|
|
635
|
+
# never as \`$(halfcycle_env_file \u2026)\` \u2014 a command substitution is a subshell and
|
|
636
|
+
# would discard everything it set.
|
|
637
|
+
halfcycle_env_file() {
|
|
638
|
+
HALFCYCLE_ENV_FILE=""
|
|
639
|
+
HALFCYCLE_ENV_ENGAGEMENT=""
|
|
640
|
+
HALFCYCLE_ENV_PROBLEM=""
|
|
641
|
+
hc_pin="$1/.halfcycle/bundle.json"
|
|
642
|
+
if [ ! -f "$hc_pin" ]; then
|
|
643
|
+
HALFCYCLE_ENV_PROBLEM="no-pin"
|
|
644
|
+
return 1
|
|
645
|
+
fi
|
|
646
|
+
hc_id=$(tr -d '\\n' < "$hc_pin" | sed -n 's/.*"${PIN_ENGAGEMENT_ID_FIELD}"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p')
|
|
647
|
+
if [ -z "$hc_id" ]; then
|
|
648
|
+
HALFCYCLE_ENV_PROBLEM="no-id"
|
|
649
|
+
return 1
|
|
650
|
+
fi
|
|
651
|
+
HALFCYCLE_ENV_ENGAGEMENT="$hc_id"
|
|
652
|
+
if [ -z "\${HOME:-}" ]; then
|
|
653
|
+
HALFCYCLE_ENV_PROBLEM="no-home"
|
|
654
|
+
return 1
|
|
655
|
+
fi
|
|
656
|
+
hc_env="$HOME/.halfcycle/${ENGAGEMENTS_DIR}/$hc_id/${ENV_FILENAME}"
|
|
657
|
+
if [ ! -f "$hc_env" ]; then
|
|
658
|
+
HALFCYCLE_ENV_PROBLEM="no-credential"
|
|
659
|
+
return 1
|
|
660
|
+
fi
|
|
661
|
+
HALFCYCLE_ENV_FILE="$hc_env"
|
|
662
|
+
return 0
|
|
663
|
+
}
|
|
664
|
+
# --- end Halfcycle credential resolution ------------------------------------`;
|
|
665
|
+
}
|
|
593
666
|
|
|
594
|
-
//
|
|
595
|
-
import {
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
APPROVED: "approved",
|
|
602
|
-
/** Confirm: the decision was recorded (approve or decline). 200. */
|
|
603
|
-
RECORDED: "recorded",
|
|
604
|
-
/** The human refused at the confirmation page. 400 on the poll. */
|
|
605
|
-
DECLINED: "declined",
|
|
606
|
-
/** The code's absolute expiry has passed. 400. */
|
|
607
|
-
EXPIRED: "expired",
|
|
608
|
-
/** Never minted, or already spent. 400. */
|
|
609
|
-
UNKNOWN: "unknown",
|
|
610
|
-
/** Confirm: neither `approve` nor `decline` was named. Nothing was bound. 400. */
|
|
611
|
-
INVALID_DECISION: "invalid-decision",
|
|
612
|
-
/** Confirm: an approval arrived with no signed-in identity. 400. */
|
|
613
|
-
IDENTITY_REQUIRED: "identity-required",
|
|
614
|
-
/** This deployment has no `SIGNIN_BASE_URL` / `DEVICE_AUTH_CONFIRM_SECRET`. 503. */
|
|
615
|
-
NOT_CONFIGURED: "not-configured",
|
|
616
|
-
/** Confirm: the shared confirmation secret was missing or wrong. 401. */
|
|
617
|
-
UNAUTHORIZED: "unauthorized",
|
|
618
|
-
/** Confirm: the code was already approved, declined or claimed. 409. */
|
|
619
|
-
ALREADY_DECIDED: "already-decided",
|
|
620
|
-
/**
|
|
621
|
-
* Confirm: an approval named a signed-in identity whose account has not accepted
|
|
622
|
-
* the terms of service and privacy policy currently in force (T-18, #588). 400.
|
|
623
|
-
*
|
|
624
|
-
* NOT BOUND TO THE DEVICE CODE. Unlike every other refusal in this const, this one
|
|
625
|
-
* says nothing about the code itself — the code is still `pending` after this
|
|
626
|
-
* response, exactly as it was before the request, so the SAME userCode can be
|
|
627
|
-
* confirmed again once the caller has recorded acceptance. `deviceConfirmRequestSchema`'s
|
|
628
|
-
* `acceptTerms` is how a second confirm says it did.
|
|
629
|
-
*/
|
|
630
|
-
TERMS_REQUIRED: "terms-required"
|
|
631
|
-
};
|
|
632
|
-
var DEVICE_POLL_REFUSALS = [
|
|
633
|
-
DEVICE_AUTH_STATUS.DECLINED,
|
|
634
|
-
DEVICE_AUTH_STATUS.EXPIRED,
|
|
635
|
-
DEVICE_AUTH_STATUS.UNKNOWN
|
|
636
|
-
];
|
|
637
|
-
var deviceAuthStartResponseSchema = z5.object({
|
|
638
|
-
deviceCode: z5.string().min(1),
|
|
639
|
-
userCode: z5.string().min(1),
|
|
640
|
-
verificationUrl: z5.string().min(1),
|
|
641
|
-
expiresAt: utcIso86013,
|
|
642
|
-
pollIntervalMs: z5.number().int().positive()
|
|
643
|
-
}).strict();
|
|
644
|
-
var devicePollPendingSchema = z5.object({
|
|
645
|
-
status: z5.literal(DEVICE_AUTH_STATUS.PENDING),
|
|
646
|
-
pollIntervalMs: z5.number().int().positive()
|
|
647
|
-
}).strict();
|
|
648
|
-
var devicePollApprovedSchema = z5.object({
|
|
649
|
-
status: z5.literal(DEVICE_AUTH_STATUS.APPROVED),
|
|
650
|
-
accountId: z5.string().min(1),
|
|
651
|
-
credential: z5.string().min(1),
|
|
652
|
-
expiresAt: utcIso86013
|
|
653
|
-
}).strict();
|
|
654
|
-
var deviceAuthRefusalSchema = z5.object({
|
|
655
|
-
status: z5.enum([
|
|
656
|
-
DEVICE_AUTH_STATUS.DECLINED,
|
|
657
|
-
DEVICE_AUTH_STATUS.EXPIRED,
|
|
658
|
-
DEVICE_AUTH_STATUS.UNKNOWN,
|
|
659
|
-
DEVICE_AUTH_STATUS.INVALID_DECISION,
|
|
660
|
-
DEVICE_AUTH_STATUS.IDENTITY_REQUIRED,
|
|
661
|
-
DEVICE_AUTH_STATUS.NOT_CONFIGURED,
|
|
662
|
-
DEVICE_AUTH_STATUS.UNAUTHORIZED,
|
|
663
|
-
DEVICE_AUTH_STATUS.ALREADY_DECIDED,
|
|
664
|
-
DEVICE_AUTH_STATUS.TERMS_REQUIRED
|
|
665
|
-
]),
|
|
666
|
-
message: z5.string().min(1)
|
|
667
|
-
}).strict();
|
|
668
|
-
var devicePollResponseSchema = z5.union([
|
|
669
|
-
devicePollPendingSchema,
|
|
670
|
-
devicePollApprovedSchema,
|
|
671
|
-
deviceAuthRefusalSchema
|
|
672
|
-
]);
|
|
673
|
-
var deviceConfirmRequestSchema = z5.object({
|
|
674
|
-
userCode: z5.string().min(1),
|
|
675
|
-
decision: z5.enum(["approve", "decline"]),
|
|
676
|
-
externalAuthId: z5.string().min(1).optional(),
|
|
677
|
-
email: z5.string().optional(),
|
|
678
|
-
acceptTerms: z5.boolean().optional()
|
|
679
|
-
}).strict();
|
|
680
|
-
var deviceConfirmRecordedSchema = z5.object({
|
|
681
|
-
status: z5.literal(DEVICE_AUTH_STATUS.RECORDED),
|
|
682
|
-
accountId: z5.string().min(1).optional()
|
|
683
|
-
}).strict();
|
|
684
|
-
var deviceConfirmResponseSchema = z5.union([
|
|
685
|
-
deviceConfirmRecordedSchema,
|
|
686
|
-
deviceAuthRefusalSchema
|
|
687
|
-
]);
|
|
688
|
-
var LOOPBACK_REDIRECT_HOST = "127.0.0.1";
|
|
689
|
-
var LOOPBACK_PORT_MIN = 1024;
|
|
690
|
-
var LOOPBACK_PORT_MAX = 65535;
|
|
691
|
-
var LOOPBACK_CALLBACK_PATH = "/halfcycle-cli-callback";
|
|
692
|
-
var LOOPBACK_QUERY_PORT = "loopback_port";
|
|
693
|
-
var LOOPBACK_QUERY_STATE = "loopback_state";
|
|
694
|
-
var LOOPBACK_QUERY_RESULT = "result";
|
|
695
|
-
var LOOPBACK_RESULT = {
|
|
696
|
-
APPROVED: "approved",
|
|
697
|
-
DECLINED: "declined"
|
|
698
|
-
};
|
|
699
|
-
function loopbackVerificationUrl(verificationUrl, target) {
|
|
700
|
-
let url;
|
|
667
|
+
// dist/identity.js
|
|
668
|
+
import { execFileSync } from "node:child_process";
|
|
669
|
+
import { existsSync, readFileSync as readFileSync3 } from "node:fs";
|
|
670
|
+
import { join as join3 } from "node:path";
|
|
671
|
+
import { randomUUID } from "node:crypto";
|
|
672
|
+
var PROJECT_IDENTITY_NOTE = 'projectId is NOT the engagement id \u2014 it identifies this repository/worktree to Halfcycle, not an engagement. The engagement id lives in .halfcycle/bundle.json, in its "engagementId" field.';
|
|
673
|
+
function git(targetRepoRoot, args2) {
|
|
701
674
|
try {
|
|
702
|
-
|
|
675
|
+
const out = execFileSync("git", ["-C", targetRepoRoot, ...args2], {
|
|
676
|
+
encoding: "utf-8",
|
|
677
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
678
|
+
});
|
|
679
|
+
const trimmed = out.trim();
|
|
680
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
703
681
|
} catch {
|
|
704
|
-
return
|
|
682
|
+
return null;
|
|
705
683
|
}
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
684
|
+
}
|
|
685
|
+
function readRootCommit(targetRepoRoot) {
|
|
686
|
+
const roots = git(targetRepoRoot, ["rev-list", "--max-parents=0", "HEAD"]);
|
|
687
|
+
if (!roots)
|
|
688
|
+
return null;
|
|
689
|
+
const lines = roots.split("\n").filter((l) => l.length > 0);
|
|
690
|
+
return lines.length > 0 ? lines[lines.length - 1] : null;
|
|
691
|
+
}
|
|
692
|
+
function readRemote(targetRepoRoot) {
|
|
693
|
+
return git(targetRepoRoot, ["remote", "get-url", "origin"]);
|
|
694
|
+
}
|
|
695
|
+
function mintOrReadIdentity(targetRepoRoot) {
|
|
696
|
+
const path = join3(targetRepoRoot, ".halfcycle", "project.json");
|
|
697
|
+
if (existsSync(path)) {
|
|
698
|
+
const onDisk = JSON.parse(readFileSync3(path, "utf-8"));
|
|
699
|
+
const identity2 = {
|
|
700
|
+
projectId: onDisk.projectId ?? "",
|
|
701
|
+
rootCommit: onDisk.rootCommit ?? null,
|
|
702
|
+
remote: onDisk.remote ?? null,
|
|
703
|
+
note: PROJECT_IDENTITY_NOTE
|
|
704
|
+
};
|
|
705
|
+
return { identity: identity2, minted: false };
|
|
706
|
+
}
|
|
707
|
+
const identity = {
|
|
708
|
+
projectId: randomUUID(),
|
|
709
|
+
rootCommit: readRootCommit(targetRepoRoot),
|
|
710
|
+
remote: readRemote(targetRepoRoot),
|
|
711
|
+
note: PROJECT_IDENTITY_NOTE
|
|
712
|
+
};
|
|
713
|
+
return { identity, minted: true };
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// dist/mcp-endpoint.js
|
|
717
|
+
var MCP_ENDPOINT_PATH = "/mcp";
|
|
718
|
+
function mcpEndpointUrl(origin) {
|
|
719
|
+
return `${origin.trim().replace(/\/+$/, "")}${MCP_ENDPOINT_PATH}`;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// dist/merge-settings.js
|
|
723
|
+
function isHalfcycleEntry(entry, halfcycleCommands) {
|
|
724
|
+
return entry.hooks.some((h) => halfcycleCommands.has(h.command));
|
|
725
|
+
}
|
|
726
|
+
function mergeSettings(existing, generated) {
|
|
727
|
+
const merged = { ...existing };
|
|
728
|
+
if (generated.$schema !== void 0) {
|
|
729
|
+
merged.$schema = generated.$schema;
|
|
730
|
+
}
|
|
731
|
+
const generatedHooks = generated.hooks ?? {};
|
|
732
|
+
const halfcycleCommands = /* @__PURE__ */ new Set();
|
|
733
|
+
for (const entries of Object.values(generatedHooks)) {
|
|
734
|
+
for (const entry of entries) {
|
|
735
|
+
for (const h of entry.hooks)
|
|
736
|
+
halfcycleCommands.add(h.command);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
const existingHooks = existing.hooks ?? {};
|
|
740
|
+
const mergedHooks = {};
|
|
741
|
+
const events = /* @__PURE__ */ new Set([...Object.keys(existingHooks), ...Object.keys(generatedHooks)]);
|
|
742
|
+
for (const event of events) {
|
|
743
|
+
const developerEntries = (existingHooks[event] ?? []).filter((entry) => !isHalfcycleEntry(entry, halfcycleCommands));
|
|
744
|
+
const halfcycleEntries = generatedHooks[event] ?? [];
|
|
745
|
+
mergedHooks[event] = [...developerEntries, ...halfcycleEntries];
|
|
746
|
+
}
|
|
747
|
+
merged.hooks = mergedHooks;
|
|
748
|
+
merged.permissions = mergePermissions(existing.permissions, generated.permissions);
|
|
749
|
+
return merged;
|
|
750
|
+
}
|
|
751
|
+
function mergePermissions(existing, generated) {
|
|
752
|
+
if (existing === void 0 && generated === void 0)
|
|
753
|
+
return void 0;
|
|
754
|
+
const result = { ...existing ?? {} };
|
|
755
|
+
const generatedDeny = generated?.deny ?? [];
|
|
756
|
+
if (generatedDeny.length > 0) {
|
|
757
|
+
const theirs = existing?.deny ?? [];
|
|
758
|
+
const seen = new Set(theirs);
|
|
759
|
+
result.deny = [...theirs, ...generatedDeny.filter((p) => !seen.has(p))];
|
|
760
|
+
}
|
|
761
|
+
return result;
|
|
709
762
|
}
|
|
710
763
|
|
|
711
764
|
// dist/scan.js
|
|
765
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
|
|
766
|
+
import { join as join4 } from "node:path";
|
|
712
767
|
var HALFCYCLE_STATE_FORMAT = "halfcycle-state-file/v1";
|
|
713
768
|
var HALFCYCLE_STATE_NOTE = [
|
|
714
769
|
"This file is a local cache written ONCE when Halfcycle was installed. It is not a",
|
|
@@ -1107,6 +1162,13 @@ var ENV_EXAMPLE_NAMES = [
|
|
|
1107
1162
|
"GUARD_SERVICE_URL",
|
|
1108
1163
|
"GUARD_SERVICE_TOKEN",
|
|
1109
1164
|
"GUARD_ENGAGEMENT_ID",
|
|
1165
|
+
// Declared for ONE reason only: it is written into the credential store, and the
|
|
1166
|
+
// INV-004 check below requires every written name to be named here. It is NOT
|
|
1167
|
+
// read from a client's environment — the generated CI stanza sets the three
|
|
1168
|
+
// GUARD_* names and not this one, and `packages/runner` never reads it at all.
|
|
1169
|
+
// It is how an install REMEMBERS the plane it was created against, which matters
|
|
1170
|
+
// only to our own development and CI planes (operator amendment, 2026-08-30);
|
|
1171
|
+
// nothing tells a client to set it, and the generated prose says so.
|
|
1110
1172
|
"HALFCYCLE_SERVICE_URL",
|
|
1111
1173
|
"HALFCYCLE_MCP_URL",
|
|
1112
1174
|
"HALFCYCLE_TOKEN",
|
|
@@ -1143,10 +1205,11 @@ GUARD_ENGAGEMENT_ID=<argType:runtime>
|
|
|
1143
1205
|
# nothing else here. The platform reports the second on create, so it is written
|
|
1144
1206
|
# for you and .mcp.json carries the composed address.
|
|
1145
1207
|
#
|
|
1146
|
-
# NEITHER IS
|
|
1147
|
-
#
|
|
1148
|
-
#
|
|
1149
|
-
#
|
|
1208
|
+
# NEITHER IS A SETTING. Both names are here because both are WRITTEN by the
|
|
1209
|
+
# installer into this engagement's credential, and every name it writes is declared
|
|
1210
|
+
# here \u2014 that is the rule this section exists for. Halfcycle's address ships in the
|
|
1211
|
+
# CLI and is the same one for everybody, so there is nothing to look up and nothing
|
|
1212
|
+
# to put in this file. Leave both lines exactly as they are.
|
|
1150
1213
|
HALFCYCLE_SERVICE_URL=<argType:runtime>
|
|
1151
1214
|
HALFCYCLE_MCP_URL=<argType:runtime>
|
|
1152
1215
|
HALFCYCLE_TOKEN=<argType:runtime>
|
|
@@ -1332,6 +1395,16 @@ function writeBundlePin(targetRepoRoot, version, engagementId, engagementType, w
|
|
|
1332
1395
|
const pinPath = join5(targetRepoRoot, ".halfcycle", "bundle.json");
|
|
1333
1396
|
writeAllowlisted(pinPath, targetRepoRoot, JSON.stringify(pin, null, 2) + "\n", writtenPaths);
|
|
1334
1397
|
}
|
|
1398
|
+
function writeCrewRoster(targetRepoRoot, report) {
|
|
1399
|
+
const doc = {
|
|
1400
|
+
$comment: "GENERATED by the Halfcycle installer \u2014 do not edit by hand. Re-run the installer to pick up a roster change.",
|
|
1401
|
+
crew: CREW_ROSTER
|
|
1402
|
+
};
|
|
1403
|
+
const rendered = `${JSON.stringify(doc, null, 2)}
|
|
1404
|
+
`;
|
|
1405
|
+
const crewPath = join5(targetRepoRoot, ".halfcycle", "crew.json");
|
|
1406
|
+
record(report, writeOwned(crewPath, targetRepoRoot, rendered), ".halfcycle/crew.json");
|
|
1407
|
+
}
|
|
1335
1408
|
function readBundlePin(targetRepoRoot) {
|
|
1336
1409
|
const pinPath = join5(targetRepoRoot, ".halfcycle", "bundle.json");
|
|
1337
1410
|
if (!existsSync3(pinPath))
|
|
@@ -1563,6 +1636,7 @@ async function install(options) {
|
|
|
1563
1636
|
break;
|
|
1564
1637
|
}
|
|
1565
1638
|
writeBundlePin(targetRepo, manifest.version, engagementId, engagementType, report.writtenPaths);
|
|
1639
|
+
writeCrewRoster(targetRepo, report);
|
|
1566
1640
|
const scanResult = runBootstrapScan(targetRepo);
|
|
1567
1641
|
return {
|
|
1568
1642
|
version: manifest.version,
|
|
@@ -1584,7 +1658,7 @@ function resolveControlOrigin(env = process.env) {
|
|
|
1584
1658
|
return { origin: DEFAULT_CONTROL_ORIGIN, source: "default" };
|
|
1585
1659
|
}
|
|
1586
1660
|
function controlOriginNote(resolved) {
|
|
1587
|
-
return resolved.source === "environment" ? `${resolved.origin} (from HALFCYCLE_SERVICE_URL)` : `${resolved.origin} (the Halfcycle plane
|
|
1661
|
+
return resolved.source === "environment" ? `${resolved.origin} (from HALFCYCLE_SERVICE_URL)` : `${resolved.origin} (the Halfcycle plane)`;
|
|
1588
1662
|
}
|
|
1589
1663
|
|
|
1590
1664
|
// dist/create-engagement.js
|
|
@@ -1644,7 +1718,7 @@ function planeRefusal(detail) {
|
|
|
1644
1718
|
error: typeof candidate.error === "string" ? candidate.error : void 0
|
|
1645
1719
|
};
|
|
1646
1720
|
}
|
|
1647
|
-
var CONTROL_ORIGIN_HINT = `
|
|
1721
|
+
var CONTROL_ORIGIN_HINT = `This CLI talks to ${DEFAULT_CONTROL_ORIGIN}; it must be the CONTROL origin \u2014 the one that serves /engagements and /board/enter-codes. The MCP server runs at a different address, and the platform supplies that one itself; you configure neither.`;
|
|
1648
1722
|
async function createEngagement(baseUrl, name, credential) {
|
|
1649
1723
|
return requestEngagementValues(`${baseUrl.replace(/\/+$/, "")}/engagements`, name ? { name } : {}, credential, { action: "Create-engagement", nothingHappened: "No engagement was created", route: "POST /engagements" });
|
|
1650
1724
|
}
|