halfcycle 0.3.8 → 0.3.10
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 +30 -6
- package/dist/bin.js +587 -535
- 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/engagement-credential.d.ts +26 -0
- package/dist/engagement-credential.d.ts.map +1 -1
- package/dist/index.js +203 -160
- 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/index.js
CHANGED
|
@@ -3,6 +3,173 @@ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as rea
|
|
|
3
3
|
import { dirname as dirname2, join as join5, relative } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
|
|
6
|
+
// ../events/dist/result.js
|
|
7
|
+
import { z as z2 } from "zod";
|
|
8
|
+
|
|
9
|
+
// ../core/dist/guard-record.js
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
var severitySchema = z.enum(["info", "warn", "block"]);
|
|
12
|
+
var channelSchema = z.enum(["stable", "candidate", "rented"]);
|
|
13
|
+
var matcherKindSchema = z.enum(["grep", "ast", "integrationTest", "llm"]);
|
|
14
|
+
var matcherConfigSchema = z.record(z.string(), z.unknown());
|
|
15
|
+
var grepMatcherSchema = z.object({ kind: z.literal("grep"), config: matcherConfigSchema }).strict();
|
|
16
|
+
var astMatcherSchema = z.object({ kind: z.literal("ast"), config: matcherConfigSchema }).strict();
|
|
17
|
+
var integrationTestMatcherSchema = z.object({ kind: z.literal("integrationTest"), config: matcherConfigSchema }).strict();
|
|
18
|
+
var llmMatcherSchema = z.object({ kind: z.literal("llm"), config: matcherConfigSchema }).strict();
|
|
19
|
+
var matcherSchema = z.discriminatedUnion("kind", [
|
|
20
|
+
grepMatcherSchema,
|
|
21
|
+
astMatcherSchema,
|
|
22
|
+
integrationTestMatcherSchema,
|
|
23
|
+
llmMatcherSchema
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
// ../events/dist/result.js
|
|
27
|
+
var firedGuardSchema = z2.object({
|
|
28
|
+
guardId: z2.string(),
|
|
29
|
+
patternRef: z2.string(),
|
|
30
|
+
severity: severitySchema,
|
|
31
|
+
explanation: z2.string(),
|
|
32
|
+
/**
|
|
33
|
+
* The repo-relative path of the file this firing was decided on. The matcher
|
|
34
|
+
* has the file live at the moment it decides to fire; this is the field that
|
|
35
|
+
* carries the answer out. It is the CLIENT's own coordinate — the wire type
|
|
36
|
+
* still has no field for a matcher body, a channel, a version, a scope or a
|
|
37
|
+
* guard's provenance, and physically cannot carry one.
|
|
38
|
+
*
|
|
39
|
+
* THE PRESENCE RULE, WHICH LIVES HERE AND NOT IN A CONSUMER'S PROSE: every
|
|
40
|
+
* firing produced from this contract version onward carries `path`, so on the
|
|
41
|
+
* WIRE, absence means exactly one thing — a response from a guard service
|
|
42
|
+
* older than this contract. It is declared optional because a client's guard
|
|
43
|
+
* runner is built from its own checkout and calls a service that may be older
|
|
44
|
+
* than it; a required field would make that pairing unparseable.
|
|
45
|
+
*
|
|
46
|
+
* The rule does NOT transfer to a store that persists a firing, where an
|
|
47
|
+
* absent path has a second legitimate meaning: a row written before the
|
|
48
|
+
* column existed. A persisted NULL is therefore not a writer bug.
|
|
49
|
+
*
|
|
50
|
+
* NO SHAPE CONSTRAINT. This value is whatever the changed tree called the
|
|
51
|
+
* file. A consumer that maps it onto a coordinate with its own path rule must
|
|
52
|
+
* safe-parse it and decide what to do with a refusal, rather than assume the
|
|
53
|
+
* two shapes agree.
|
|
54
|
+
*/
|
|
55
|
+
path: z2.string().optional()
|
|
56
|
+
}).strict();
|
|
57
|
+
var resultEnvelopeSchema = z2.object({
|
|
58
|
+
guardsFired: z2.array(firedGuardSchema),
|
|
59
|
+
severity: severitySchema.nullable(),
|
|
60
|
+
blocking: z2.boolean(),
|
|
61
|
+
explanation: z2.string()
|
|
62
|
+
}).strict();
|
|
63
|
+
var wireErrorSchema = z2.object({
|
|
64
|
+
statusCode: z2.number(),
|
|
65
|
+
error: z2.string(),
|
|
66
|
+
message: z2.string()
|
|
67
|
+
}).strict();
|
|
68
|
+
|
|
69
|
+
// ../events/dist/telemetry.js
|
|
70
|
+
import { z as z3 } from "zod";
|
|
71
|
+
var utcIso8601 = z3.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)");
|
|
72
|
+
var guardEvalOutcomeSchema = z3.enum([
|
|
73
|
+
"evaluated",
|
|
74
|
+
"infra-error",
|
|
75
|
+
"contract-error",
|
|
76
|
+
"unconfigured"
|
|
77
|
+
]);
|
|
78
|
+
var guardEvalRunSchema = z3.object({
|
|
79
|
+
// `runId`/`outcome` are the E2 run-observability additions, declared
|
|
80
|
+
// field-level-OPTIONAL as the FIRST (expand) step of an expand-contract
|
|
81
|
+
// migration. Every new record populated by the migrated writer (T-07)
|
|
82
|
+
// carries both; a legacy line predating the amendment carries neither and
|
|
83
|
+
// must still parse (the mixed-era guard-eval log the Build-Record reader
|
|
84
|
+
// assembles from, and the runner emit not yet migrated). So the presence
|
|
85
|
+
// refinement below is skipped for a legacy line (no `outcome`), and the
|
|
86
|
+
// per-writer guarantee lives with the writer + reader-normalisation (T-07),
|
|
87
|
+
// where the schema may then be tightened to required. runId is uuid-checked
|
|
88
|
+
// WHEN present; outcome is enum-checked when present.
|
|
89
|
+
runId: z3.string().uuid().optional(),
|
|
90
|
+
engagementId: z3.string().optional(),
|
|
91
|
+
runType: z3.enum(["hook", "ci"]),
|
|
92
|
+
occurredAt: utcIso8601,
|
|
93
|
+
outcome: guardEvalOutcomeSchema.optional(),
|
|
94
|
+
failureReason: z3.string().optional(),
|
|
95
|
+
phase: z3.string().optional(),
|
|
96
|
+
scope: z3.record(z3.string(), z3.unknown()),
|
|
97
|
+
cost: z3.record(z3.string(), z3.unknown()).optional(),
|
|
98
|
+
result: resultEnvelopeSchema.optional()
|
|
99
|
+
}).strict().superRefine((run, ctx) => {
|
|
100
|
+
if (run.outcome === void 0)
|
|
101
|
+
return;
|
|
102
|
+
if (run.outcome === "evaluated" && run.result === void 0) {
|
|
103
|
+
ctx.addIssue({
|
|
104
|
+
code: z3.ZodIssueCode.custom,
|
|
105
|
+
path: ["result"],
|
|
106
|
+
message: "result is required when outcome is 'evaluated'"
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
if (run.outcome !== "evaluated" && run.result !== void 0) {
|
|
110
|
+
ctx.addIssue({
|
|
111
|
+
code: z3.ZodIssueCode.custom,
|
|
112
|
+
path: ["result"],
|
|
113
|
+
message: "result is only present when outcome is 'evaluated'"
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
if (run.outcome !== "unconfigured" && run.engagementId === void 0) {
|
|
117
|
+
ctx.addIssue({
|
|
118
|
+
code: z3.ZodIssueCode.custom,
|
|
119
|
+
path: ["engagementId"],
|
|
120
|
+
message: "engagementId is required unless outcome is 'unconfigured'"
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// ../events/dist/crew.js
|
|
126
|
+
import { z as z4 } from "zod";
|
|
127
|
+
var crewGroupSchema = z4.enum(["builder", "reviewer", "always-on"]);
|
|
128
|
+
var crewTierSchema = z4.enum(["deep", "standard", "fast"]);
|
|
129
|
+
var CREW_DISCIPLINE_MAX = 64;
|
|
130
|
+
var crewMemberSchema = z4.object({
|
|
131
|
+
// The agent's name. The same twenty-one on every project, so a name is a
|
|
132
|
+
// vocabulary a returning client already knows.
|
|
133
|
+
callsign: z4.string().min(1).max(32),
|
|
134
|
+
// Which of the three headings this member reads under.
|
|
135
|
+
group: crewGroupSchema,
|
|
136
|
+
// The kind of work this member takes. A task names a discipline, and only
|
|
137
|
+
// agents holding it can be dispatched to it.
|
|
138
|
+
discipline: z4.string().min(1).max(CREW_DISCIPLINE_MAX),
|
|
139
|
+
// How much thinking this member brings. There is no field naming the supplier
|
|
140
|
+
// behind it, on this shape or on any other.
|
|
141
|
+
tier: crewTierSchema
|
|
142
|
+
}).strict();
|
|
143
|
+
var CREW_ROSTER = [
|
|
144
|
+
// ── builders: code ──────────────────────────────────────────────────────────
|
|
145
|
+
{ callsign: "ADA", group: "builder", discipline: "implementation", tier: "deep" },
|
|
146
|
+
{ callsign: "OTTO", group: "builder", discipline: "implementation", tier: "deep" },
|
|
147
|
+
{ callsign: "MILO", group: "builder", discipline: "implementation", tier: "standard" },
|
|
148
|
+
{ callsign: "NOVA", group: "builder", discipline: "implementation", tier: "standard" },
|
|
149
|
+
{ callsign: "KAI", group: "builder", discipline: "implementation", tier: "standard" },
|
|
150
|
+
{ callsign: "HUGO", group: "builder", discipline: "infrastructure", tier: "standard" },
|
|
151
|
+
{ callsign: "WREN", group: "builder", discipline: "refactor", tier: "fast" },
|
|
152
|
+
{ callsign: "PIP", group: "builder", discipline: "refactor", tier: "fast" },
|
|
153
|
+
// ── builders: specs and docs ────────────────────────────────────────────────
|
|
154
|
+
{ callsign: "IRIS", group: "builder", discipline: "feature spec", tier: "deep" },
|
|
155
|
+
{ callsign: "JUNO", group: "builder", discipline: "feature spec", tier: "standard" },
|
|
156
|
+
{ callsign: "LEX", group: "builder", discipline: "context & docs", tier: "fast" },
|
|
157
|
+
// ── reviewers ───────────────────────────────────────────────────────────────
|
|
158
|
+
{ callsign: "VERA", group: "reviewer", discipline: "spec consistency", tier: "deep" },
|
|
159
|
+
{ callsign: "ODIN", group: "reviewer", discipline: "cross-spec audit", tier: "deep" },
|
|
160
|
+
{ callsign: "CASS", group: "reviewer", discipline: "code review", tier: "deep" },
|
|
161
|
+
{ callsign: "THEO", group: "reviewer", discipline: "code review", tier: "standard" },
|
|
162
|
+
{ callsign: "ECHO", group: "reviewer", discipline: "test quality", tier: "standard" },
|
|
163
|
+
{ callsign: "MAVIS", group: "reviewer", discipline: "suites & coverage", tier: "standard" },
|
|
164
|
+
{ callsign: "ZARA", group: "reviewer", discipline: "smoke & walk aid", tier: "standard" },
|
|
165
|
+
{ callsign: "NORA", group: "reviewer", discipline: "evidence & provenance", tier: "fast" },
|
|
166
|
+
// ── always on: continuous, takes no task ────────────────────────────────────
|
|
167
|
+
{ callsign: "ARGUS", group: "always-on", discipline: "guard \xB7 every diff", tier: "standard" },
|
|
168
|
+
{ callsign: "ATLAS", group: "always-on", discipline: "coordinator", tier: "deep" }
|
|
169
|
+
];
|
|
170
|
+
var CREW_ROSTER_SIZE = CREW_ROSTER.length;
|
|
171
|
+
var CREW_CALLSIGNS = CREW_ROSTER.map((member) => member.callsign);
|
|
172
|
+
|
|
6
173
|
// dist/engagement-credential.js
|
|
7
174
|
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
8
175
|
import { platform as platform2 } from "node:os";
|
|
@@ -30,6 +197,7 @@ var GUARD_ENV_KEYS = [
|
|
|
30
197
|
"GUARD_SERVICE_TOKEN",
|
|
31
198
|
"GUARD_ENGAGEMENT_ID"
|
|
32
199
|
];
|
|
200
|
+
var GUARD_COVERAGE_REQUIRED_KEYS = GUARD_ENV_KEYS.filter((key) => key !== "GUARD_SERVICE_URL");
|
|
33
201
|
var ENGAGEMENT_ENV_KEYS = [
|
|
34
202
|
"HALFCYCLE_SERVICE_URL",
|
|
35
203
|
"HALFCYCLE_MCP_URL",
|
|
@@ -279,127 +447,6 @@ function mergePermissions(existing, generated) {
|
|
|
279
447
|
// dist/scan.js
|
|
280
448
|
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
|
|
281
449
|
import { join as join4 } from "node:path";
|
|
282
|
-
|
|
283
|
-
// ../events/dist/result.js
|
|
284
|
-
import { z as z2 } from "zod";
|
|
285
|
-
|
|
286
|
-
// ../core/dist/guard-record.js
|
|
287
|
-
import { z } from "zod";
|
|
288
|
-
var severitySchema = z.enum(["info", "warn", "block"]);
|
|
289
|
-
var channelSchema = z.enum(["stable", "candidate", "rented"]);
|
|
290
|
-
var matcherKindSchema = z.enum(["grep", "ast", "integrationTest", "llm"]);
|
|
291
|
-
var matcherConfigSchema = z.record(z.string(), z.unknown());
|
|
292
|
-
var grepMatcherSchema = z.object({ kind: z.literal("grep"), config: matcherConfigSchema }).strict();
|
|
293
|
-
var astMatcherSchema = z.object({ kind: z.literal("ast"), config: matcherConfigSchema }).strict();
|
|
294
|
-
var integrationTestMatcherSchema = z.object({ kind: z.literal("integrationTest"), config: matcherConfigSchema }).strict();
|
|
295
|
-
var llmMatcherSchema = z.object({ kind: z.literal("llm"), config: matcherConfigSchema }).strict();
|
|
296
|
-
var matcherSchema = z.discriminatedUnion("kind", [
|
|
297
|
-
grepMatcherSchema,
|
|
298
|
-
astMatcherSchema,
|
|
299
|
-
integrationTestMatcherSchema,
|
|
300
|
-
llmMatcherSchema
|
|
301
|
-
]);
|
|
302
|
-
|
|
303
|
-
// ../events/dist/result.js
|
|
304
|
-
var firedGuardSchema = z2.object({
|
|
305
|
-
guardId: z2.string(),
|
|
306
|
-
patternRef: z2.string(),
|
|
307
|
-
severity: severitySchema,
|
|
308
|
-
explanation: z2.string(),
|
|
309
|
-
/**
|
|
310
|
-
* The repo-relative path of the file this firing was decided on. The matcher
|
|
311
|
-
* has the file live at the moment it decides to fire; this is the field that
|
|
312
|
-
* carries the answer out. It is the CLIENT's own coordinate — the wire type
|
|
313
|
-
* still has no field for a matcher body, a channel, a version, a scope or a
|
|
314
|
-
* guard's provenance, and physically cannot carry one.
|
|
315
|
-
*
|
|
316
|
-
* THE PRESENCE RULE, WHICH LIVES HERE AND NOT IN A CONSUMER'S PROSE: every
|
|
317
|
-
* firing produced from this contract version onward carries `path`, so on the
|
|
318
|
-
* WIRE, absence means exactly one thing — a response from a guard service
|
|
319
|
-
* older than this contract. It is declared optional because a client's guard
|
|
320
|
-
* runner is built from its own checkout and calls a service that may be older
|
|
321
|
-
* than it; a required field would make that pairing unparseable.
|
|
322
|
-
*
|
|
323
|
-
* The rule does NOT transfer to a store that persists a firing, where an
|
|
324
|
-
* absent path has a second legitimate meaning: a row written before the
|
|
325
|
-
* column existed. A persisted NULL is therefore not a writer bug.
|
|
326
|
-
*
|
|
327
|
-
* NO SHAPE CONSTRAINT. This value is whatever the changed tree called the
|
|
328
|
-
* file. A consumer that maps it onto a coordinate with its own path rule must
|
|
329
|
-
* safe-parse it and decide what to do with a refusal, rather than assume the
|
|
330
|
-
* two shapes agree.
|
|
331
|
-
*/
|
|
332
|
-
path: z2.string().optional()
|
|
333
|
-
}).strict();
|
|
334
|
-
var resultEnvelopeSchema = z2.object({
|
|
335
|
-
guardsFired: z2.array(firedGuardSchema),
|
|
336
|
-
severity: severitySchema.nullable(),
|
|
337
|
-
blocking: z2.boolean(),
|
|
338
|
-
explanation: z2.string()
|
|
339
|
-
}).strict();
|
|
340
|
-
var wireErrorSchema = z2.object({
|
|
341
|
-
statusCode: z2.number(),
|
|
342
|
-
error: z2.string(),
|
|
343
|
-
message: z2.string()
|
|
344
|
-
}).strict();
|
|
345
|
-
|
|
346
|
-
// ../events/dist/telemetry.js
|
|
347
|
-
import { z as z3 } from "zod";
|
|
348
|
-
var utcIso8601 = z3.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)");
|
|
349
|
-
var guardEvalOutcomeSchema = z3.enum([
|
|
350
|
-
"evaluated",
|
|
351
|
-
"infra-error",
|
|
352
|
-
"contract-error",
|
|
353
|
-
"unconfigured"
|
|
354
|
-
]);
|
|
355
|
-
var guardEvalRunSchema = z3.object({
|
|
356
|
-
// `runId`/`outcome` are the E2 run-observability additions, declared
|
|
357
|
-
// field-level-OPTIONAL as the FIRST (expand) step of an expand-contract
|
|
358
|
-
// migration. Every new record populated by the migrated writer (T-07)
|
|
359
|
-
// carries both; a legacy line predating the amendment carries neither and
|
|
360
|
-
// must still parse (the mixed-era guard-eval log the Build-Record reader
|
|
361
|
-
// assembles from, and the runner emit not yet migrated). So the presence
|
|
362
|
-
// refinement below is skipped for a legacy line (no `outcome`), and the
|
|
363
|
-
// per-writer guarantee lives with the writer + reader-normalisation (T-07),
|
|
364
|
-
// where the schema may then be tightened to required. runId is uuid-checked
|
|
365
|
-
// WHEN present; outcome is enum-checked when present.
|
|
366
|
-
runId: z3.string().uuid().optional(),
|
|
367
|
-
engagementId: z3.string().optional(),
|
|
368
|
-
runType: z3.enum(["hook", "ci"]),
|
|
369
|
-
occurredAt: utcIso8601,
|
|
370
|
-
outcome: guardEvalOutcomeSchema.optional(),
|
|
371
|
-
failureReason: z3.string().optional(),
|
|
372
|
-
phase: z3.string().optional(),
|
|
373
|
-
scope: z3.record(z3.string(), z3.unknown()),
|
|
374
|
-
cost: z3.record(z3.string(), z3.unknown()).optional(),
|
|
375
|
-
result: resultEnvelopeSchema.optional()
|
|
376
|
-
}).strict().superRefine((run, ctx) => {
|
|
377
|
-
if (run.outcome === void 0)
|
|
378
|
-
return;
|
|
379
|
-
if (run.outcome === "evaluated" && run.result === void 0) {
|
|
380
|
-
ctx.addIssue({
|
|
381
|
-
code: z3.ZodIssueCode.custom,
|
|
382
|
-
path: ["result"],
|
|
383
|
-
message: "result is required when outcome is 'evaluated'"
|
|
384
|
-
});
|
|
385
|
-
}
|
|
386
|
-
if (run.outcome !== "evaluated" && run.result !== void 0) {
|
|
387
|
-
ctx.addIssue({
|
|
388
|
-
code: z3.ZodIssueCode.custom,
|
|
389
|
-
path: ["result"],
|
|
390
|
-
message: "result is only present when outcome is 'evaluated'"
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
|
-
if (run.outcome !== "unconfigured" && run.engagementId === void 0) {
|
|
394
|
-
ctx.addIssue({
|
|
395
|
-
code: z3.ZodIssueCode.custom,
|
|
396
|
-
path: ["engagementId"],
|
|
397
|
-
message: "engagementId is required unless outcome is 'unconfigured'"
|
|
398
|
-
});
|
|
399
|
-
}
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
// dist/scan.js
|
|
403
450
|
var HALFCYCLE_STATE_FORMAT = "halfcycle-state-file/v1";
|
|
404
451
|
var HALFCYCLE_STATE_NOTE = [
|
|
405
452
|
"This file is a local cache written ONCE when Halfcycle was installed. It is not a",
|
|
@@ -705,7 +752,7 @@ ${engagementResolutionShell()}
|
|
|
705
752
|
set +a
|
|
706
753
|
fi
|
|
707
754
|
hc_missing=""
|
|
708
|
-
for hc_key in ${
|
|
755
|
+
for hc_key in ${GUARD_COVERAGE_REQUIRED_KEYS.join(" ")}; do
|
|
709
756
|
eval "hc_value=\\\${$hc_key:-}"
|
|
710
757
|
if [ -z "$hc_value" ]; then hc_missing="$hc_missing $hc_key"; fi
|
|
711
758
|
done
|
|
@@ -798,7 +845,10 @@ function generateCiStanza() {
|
|
|
798
845
|
# node-version: '20'
|
|
799
846
|
# - name: Halfcycle guard CI check
|
|
800
847
|
# env:
|
|
801
|
-
#
|
|
848
|
+
# # TWO SECRETS, AND THEY ARE THE TWO THAT ARE YOURS: the engagement's
|
|
849
|
+
# # token and its id. Both were printed by the installer that generated
|
|
850
|
+
# # this file. There is no address to configure \u2014 the guard service this
|
|
851
|
+
# # job talks to ships inside the binary below.
|
|
802
852
|
# GUARD_SERVICE_TOKEN: \${{ secrets.GUARD_SERVICE_TOKEN }}
|
|
803
853
|
# GUARD_ENGAGEMENT_ID: \${{ secrets.GUARD_ENGAGEMENT_ID }}
|
|
804
854
|
# run: node ./.halfcycle/bin/bin.bundle.mjs ci
|
|
@@ -810,24 +860,21 @@ function generateCiStanza() {
|
|
|
810
860
|
`;
|
|
811
861
|
}
|
|
812
862
|
var ENV_EXAMPLE_NAMES = [
|
|
813
|
-
|
|
863
|
+
// The engagement's own secret, under the name the guard runner reads. A CI job
|
|
864
|
+
// has no per-user credential store, so this is one of the two it must supply.
|
|
814
865
|
"GUARD_SERVICE_TOKEN",
|
|
866
|
+
// Which engagement this repository is. Per-engagement by construction; the same
|
|
867
|
+
// CI job supplies it beside the token.
|
|
815
868
|
"GUARD_ENGAGEMENT_ID",
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
// `install-guard-config.test.ts`'s INV-004 check requires every written name to
|
|
821
|
-
// be named here too (`HALFCYCLE_ENGAGEMENT_ID` is the one deliberate exception,
|
|
822
|
-
// recorded in the spec). Optional at runtime — the runner's own telemetry
|
|
823
|
-
// emission is fail-open when it is absent — but a name that is written and never
|
|
824
|
-
// declared is exactly what this generated section exists to prevent.
|
|
825
|
-
"CONTROL_TELEMETRY_URL"
|
|
869
|
+
// The same credential under the name every other Halfcycle surface reads. Set in
|
|
870
|
+
// the environment it takes precedence over the stored credential, which is the
|
|
871
|
+
// escape hatch for running as a specific identity.
|
|
872
|
+
"HALFCYCLE_TOKEN"
|
|
826
873
|
];
|
|
827
874
|
function generateEnvExampleSection() {
|
|
828
875
|
return `
|
|
829
876
|
# ---------------------------------------------------------------------------
|
|
830
|
-
# Halfcycle
|
|
877
|
+
# Halfcycle (generated by the Halfcycle installer \u2014 names only, no values)
|
|
831
878
|
# ---------------------------------------------------------------------------
|
|
832
879
|
# YOU DO NOT NEED TO SET ANY OF THESE ON YOUR OWN MACHINE, and there is nowhere in
|
|
833
880
|
# this repository to put them. \`npx halfcycle\` writes this engagement's credential
|
|
@@ -835,33 +882,18 @@ function generateEnvExampleSection() {
|
|
|
835
882
|
# MCP headers helper read it from there. Nothing Halfcycle writes a secret into
|
|
836
883
|
# this tree, so there is no file here to leak, ignore or clean up.
|
|
837
884
|
#
|
|
838
|
-
# These
|
|
839
|
-
#
|
|
840
|
-
#
|
|
841
|
-
#
|
|
842
|
-
#
|
|
843
|
-
|
|
885
|
+
# These three are declared because they ARE read from the environment in one place:
|
|
886
|
+
# a CI job, which has no browser and no per-user store and supplies them as workflow
|
|
887
|
+
# secrets (see .halfcycle/ci-stanza.yml). All three are yours \u2014 a credential and the
|
|
888
|
+
# id of your engagement.
|
|
889
|
+
#
|
|
890
|
+
# THERE IS NO ADDRESS TO CONFIGURE ANYWHERE IN HALFCYCLE. Every service this
|
|
891
|
+
# product talks to has one address, the same for every user, and it ships in the
|
|
892
|
+
# tools you already have: the installer, the guard hook and the CI binary each know
|
|
893
|
+
# where to go. If something tells you to set a Halfcycle URL, it is out of date.
|
|
844
894
|
GUARD_SERVICE_TOKEN=<argType:runtime>
|
|
845
895
|
GUARD_ENGAGEMENT_ID=<argType:runtime>
|
|
846
|
-
# TWO ORIGINS, TWO SERVICES (T-22). HALFCYCLE_SERVICE_URL is the Halfcycle
|
|
847
|
-
# CONTROL plane \u2014 it serves engagement creation and the board's first-visit
|
|
848
|
-
# code. HALFCYCLE_MCP_URL is the method-delivery service, which serves /mcp and
|
|
849
|
-
# nothing else here. The platform reports the second on create, so it is written
|
|
850
|
-
# for you and .mcp.json carries the composed address.
|
|
851
|
-
#
|
|
852
|
-
# NEITHER IS SOMETHING YOU NEED TO SET (T-27). The installer talks to Halfcycle's
|
|
853
|
-
# own control plane by default; HALFCYCLE_SERVICE_URL OVERRIDES that address and
|
|
854
|
-
# exists for a self-hosted or development plane. It is not a prerequisite for
|
|
855
|
-
# \`npx halfcycle\`, and nothing in the product asks you to find its value.
|
|
856
|
-
HALFCYCLE_SERVICE_URL=<argType:runtime>
|
|
857
|
-
HALFCYCLE_MCP_URL=<argType:runtime>
|
|
858
896
|
HALFCYCLE_TOKEN=<argType:runtime>
|
|
859
|
-
# CONTROL_TELEMETRY_URL (T-10) \u2014 the platform reports this on create/join, same as
|
|
860
|
-
# HALFCYCLE_MCP_URL. OPTIONAL: absent, guard-eval telemetry is silently disabled
|
|
861
|
-
# and the guard loop itself is unaffected \u2014 this name is declared only because it
|
|
862
|
-
# is one this installer now writes to the machine-level store, same as every name
|
|
863
|
-
# above it.
|
|
864
|
-
CONTROL_TELEMETRY_URL=<argType:runtime>
|
|
865
897
|
`;
|
|
866
898
|
}
|
|
867
899
|
var MCP_REGISTRATION_REL = ".mcp.json";
|
|
@@ -1038,6 +1070,16 @@ function writeBundlePin(targetRepoRoot, version, engagementId, engagementType, w
|
|
|
1038
1070
|
const pinPath = join5(targetRepoRoot, ".halfcycle", "bundle.json");
|
|
1039
1071
|
writeAllowlisted(pinPath, targetRepoRoot, JSON.stringify(pin, null, 2) + "\n", writtenPaths);
|
|
1040
1072
|
}
|
|
1073
|
+
function writeCrewRoster(targetRepoRoot, report) {
|
|
1074
|
+
const doc = {
|
|
1075
|
+
$comment: "GENERATED by the Halfcycle installer \u2014 do not edit by hand. Re-run the installer to pick up a roster change.",
|
|
1076
|
+
crew: CREW_ROSTER
|
|
1077
|
+
};
|
|
1078
|
+
const rendered = `${JSON.stringify(doc, null, 2)}
|
|
1079
|
+
`;
|
|
1080
|
+
const crewPath = join5(targetRepoRoot, ".halfcycle", "crew.json");
|
|
1081
|
+
record(report, writeOwned(crewPath, targetRepoRoot, rendered), ".halfcycle/crew.json");
|
|
1082
|
+
}
|
|
1041
1083
|
function readBundlePin(targetRepoRoot) {
|
|
1042
1084
|
const pinPath = join5(targetRepoRoot, ".halfcycle", "bundle.json");
|
|
1043
1085
|
if (!existsSync3(pinPath))
|
|
@@ -1232,6 +1274,7 @@ async function install(options) {
|
|
|
1232
1274
|
break;
|
|
1233
1275
|
}
|
|
1234
1276
|
writeBundlePin(targetRepo, manifest.version, engagementId, engagementType, report.writtenPaths);
|
|
1277
|
+
writeCrewRoster(targetRepo, report);
|
|
1235
1278
|
const scanResult = runBootstrapScan(targetRepo);
|
|
1236
1279
|
return {
|
|
1237
1280
|
version: manifest.version,
|
|
@@ -1304,7 +1347,7 @@ function planeRefusal(detail) {
|
|
|
1304
1347
|
error: typeof candidate.error === "string" ? candidate.error : void 0
|
|
1305
1348
|
};
|
|
1306
1349
|
}
|
|
1307
|
-
var CONTROL_ORIGIN_HINT = `
|
|
1350
|
+
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.`;
|
|
1308
1351
|
async function createEngagement(baseUrl, name, credential) {
|
|
1309
1352
|
return requestEngagementValues(`${baseUrl.replace(/\/+$/, "")}/engagements`, name ? { name } : {}, credential, { action: "Create-engagement", nothingHappened: "No engagement was created", route: "POST /engagements" });
|
|
1310
1353
|
}
|