halfcycle 0.3.7 → 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 +114 -18
- package/dist/bin.js +748 -552
- package/dist/bin.js.map +3 -3
- package/dist/close-phase.d.ts.map +1 -1
- package/dist/control-origin.d.ts +102 -0
- package/dist/control-origin.d.ts.map +1 -0
- package/dist/create-engagement.d.ts +3 -1
- package/dist/create-engagement.d.ts.map +1 -1
- package/dist/index.js +275 -102
- 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 +5 -1
package/dist/bin.js
CHANGED
|
@@ -10,6 +10,421 @@ 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
|
+
// ../events/dist/result.js
|
|
14
|
+
import { z as z2 } from "zod";
|
|
15
|
+
|
|
16
|
+
// ../core/dist/guard-record.js
|
|
17
|
+
import { z } from "zod";
|
|
18
|
+
var severitySchema = z.enum(["info", "warn", "block"]);
|
|
19
|
+
var channelSchema = z.enum(["stable", "candidate", "rented"]);
|
|
20
|
+
var matcherKindSchema = z.enum(["grep", "ast", "integrationTest", "llm"]);
|
|
21
|
+
var matcherConfigSchema = z.record(z.string(), z.unknown());
|
|
22
|
+
var grepMatcherSchema = z.object({ kind: z.literal("grep"), config: matcherConfigSchema }).strict();
|
|
23
|
+
var astMatcherSchema = z.object({ kind: z.literal("ast"), config: matcherConfigSchema }).strict();
|
|
24
|
+
var integrationTestMatcherSchema = z.object({ kind: z.literal("integrationTest"), config: matcherConfigSchema }).strict();
|
|
25
|
+
var llmMatcherSchema = z.object({ kind: z.literal("llm"), config: matcherConfigSchema }).strict();
|
|
26
|
+
var matcherSchema = z.discriminatedUnion("kind", [
|
|
27
|
+
grepMatcherSchema,
|
|
28
|
+
astMatcherSchema,
|
|
29
|
+
integrationTestMatcherSchema,
|
|
30
|
+
llmMatcherSchema
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
// ../events/dist/result.js
|
|
34
|
+
var firedGuardSchema = z2.object({
|
|
35
|
+
guardId: z2.string(),
|
|
36
|
+
patternRef: z2.string(),
|
|
37
|
+
severity: severitySchema,
|
|
38
|
+
explanation: z2.string(),
|
|
39
|
+
/**
|
|
40
|
+
* The repo-relative path of the file this firing was decided on. The matcher
|
|
41
|
+
* has the file live at the moment it decides to fire; this is the field that
|
|
42
|
+
* carries the answer out. It is the CLIENT's own coordinate — the wire type
|
|
43
|
+
* still has no field for a matcher body, a channel, a version, a scope or a
|
|
44
|
+
* guard's provenance, and physically cannot carry one.
|
|
45
|
+
*
|
|
46
|
+
* THE PRESENCE RULE, WHICH LIVES HERE AND NOT IN A CONSUMER'S PROSE: every
|
|
47
|
+
* firing produced from this contract version onward carries `path`, so on the
|
|
48
|
+
* WIRE, absence means exactly one thing — a response from a guard service
|
|
49
|
+
* older than this contract. It is declared optional because a client's guard
|
|
50
|
+
* runner is built from its own checkout and calls a service that may be older
|
|
51
|
+
* than it; a required field would make that pairing unparseable.
|
|
52
|
+
*
|
|
53
|
+
* The rule does NOT transfer to a store that persists a firing, where an
|
|
54
|
+
* absent path has a second legitimate meaning: a row written before the
|
|
55
|
+
* column existed. A persisted NULL is therefore not a writer bug.
|
|
56
|
+
*
|
|
57
|
+
* NO SHAPE CONSTRAINT. This value is whatever the changed tree called the
|
|
58
|
+
* file. A consumer that maps it onto a coordinate with its own path rule must
|
|
59
|
+
* safe-parse it and decide what to do with a refusal, rather than assume the
|
|
60
|
+
* two shapes agree.
|
|
61
|
+
*/
|
|
62
|
+
path: z2.string().optional()
|
|
63
|
+
}).strict();
|
|
64
|
+
var resultEnvelopeSchema = z2.object({
|
|
65
|
+
guardsFired: z2.array(firedGuardSchema),
|
|
66
|
+
severity: severitySchema.nullable(),
|
|
67
|
+
blocking: z2.boolean(),
|
|
68
|
+
explanation: z2.string()
|
|
69
|
+
}).strict();
|
|
70
|
+
var wireErrorSchema = z2.object({
|
|
71
|
+
statusCode: z2.number(),
|
|
72
|
+
error: z2.string(),
|
|
73
|
+
message: z2.string()
|
|
74
|
+
}).strict();
|
|
75
|
+
|
|
76
|
+
// ../events/dist/telemetry.js
|
|
77
|
+
import { z as z3 } from "zod";
|
|
78
|
+
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)");
|
|
79
|
+
var guardEvalOutcomeSchema = z3.enum([
|
|
80
|
+
"evaluated",
|
|
81
|
+
"infra-error",
|
|
82
|
+
"contract-error",
|
|
83
|
+
"unconfigured"
|
|
84
|
+
]);
|
|
85
|
+
var guardEvalRunSchema = z3.object({
|
|
86
|
+
// `runId`/`outcome` are the E2 run-observability additions, declared
|
|
87
|
+
// field-level-OPTIONAL as the FIRST (expand) step of an expand-contract
|
|
88
|
+
// migration. Every new record populated by the migrated writer (T-07)
|
|
89
|
+
// carries both; a legacy line predating the amendment carries neither and
|
|
90
|
+
// must still parse (the mixed-era guard-eval log the Build-Record reader
|
|
91
|
+
// assembles from, and the runner emit not yet migrated). So the presence
|
|
92
|
+
// refinement below is skipped for a legacy line (no `outcome`), and the
|
|
93
|
+
// per-writer guarantee lives with the writer + reader-normalisation (T-07),
|
|
94
|
+
// where the schema may then be tightened to required. runId is uuid-checked
|
|
95
|
+
// WHEN present; outcome is enum-checked when present.
|
|
96
|
+
runId: z3.string().uuid().optional(),
|
|
97
|
+
engagementId: z3.string().optional(),
|
|
98
|
+
runType: z3.enum(["hook", "ci"]),
|
|
99
|
+
occurredAt: utcIso8601,
|
|
100
|
+
outcome: guardEvalOutcomeSchema.optional(),
|
|
101
|
+
failureReason: z3.string().optional(),
|
|
102
|
+
phase: z3.string().optional(),
|
|
103
|
+
scope: z3.record(z3.string(), z3.unknown()),
|
|
104
|
+
cost: z3.record(z3.string(), z3.unknown()).optional(),
|
|
105
|
+
result: resultEnvelopeSchema.optional()
|
|
106
|
+
}).strict().superRefine((run, ctx) => {
|
|
107
|
+
if (run.outcome === void 0)
|
|
108
|
+
return;
|
|
109
|
+
if (run.outcome === "evaluated" && run.result === void 0) {
|
|
110
|
+
ctx.addIssue({
|
|
111
|
+
code: z3.ZodIssueCode.custom,
|
|
112
|
+
path: ["result"],
|
|
113
|
+
message: "result is required when outcome is 'evaluated'"
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
if (run.outcome !== "evaluated" && run.result !== void 0) {
|
|
117
|
+
ctx.addIssue({
|
|
118
|
+
code: z3.ZodIssueCode.custom,
|
|
119
|
+
path: ["result"],
|
|
120
|
+
message: "result is only present when outcome is 'evaluated'"
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
if (run.outcome !== "unconfigured" && run.engagementId === void 0) {
|
|
124
|
+
ctx.addIssue({
|
|
125
|
+
code: z3.ZodIssueCode.custom,
|
|
126
|
+
path: ["engagementId"],
|
|
127
|
+
message: "engagementId is required unless outcome is 'unconfigured'"
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// ../events/dist/engagement-lifecycle.js
|
|
133
|
+
import { z as z4 } from "zod";
|
|
134
|
+
var utcIso86012 = z4.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)");
|
|
135
|
+
var engagementTypeSchema = z4.enum(["client"]);
|
|
136
|
+
var lifecycleStatusSchema = z4.enum(["active", "delivering", "closed"]);
|
|
137
|
+
var createEngagementRequestSchema = z4.object({
|
|
138
|
+
name: z4.string(),
|
|
139
|
+
region: z4.string(),
|
|
140
|
+
engagementType: engagementTypeSchema
|
|
141
|
+
}).strict();
|
|
142
|
+
var createEngagementResponseSchema = z4.object({
|
|
143
|
+
engagementId: z4.string(),
|
|
144
|
+
name: z4.string(),
|
|
145
|
+
region: z4.string(),
|
|
146
|
+
status: lifecycleStatusSchema,
|
|
147
|
+
createdAt: utcIso86012,
|
|
148
|
+
sessionToken: z4.string()
|
|
149
|
+
}).strict();
|
|
150
|
+
var engagementStatusResponseSchema = z4.object({
|
|
151
|
+
engagementId: z4.string(),
|
|
152
|
+
name: z4.string(),
|
|
153
|
+
region: z4.string(),
|
|
154
|
+
status: lifecycleStatusSchema,
|
|
155
|
+
installLinked: z4.boolean(),
|
|
156
|
+
createdAt: utcIso86012,
|
|
157
|
+
updatedAt: utcIso86012
|
|
158
|
+
}).strict();
|
|
159
|
+
var linkInstallationRequestSchema = z4.object({
|
|
160
|
+
installationId: z4.string(),
|
|
161
|
+
engagementId: z4.string()
|
|
162
|
+
}).strict();
|
|
163
|
+
var startBuildRequestSchema = z4.object({
|
|
164
|
+
engagementId: z4.string()
|
|
165
|
+
}).strict();
|
|
166
|
+
var startBuildResponseSchema = z4.object({
|
|
167
|
+
engagementId: z4.string(),
|
|
168
|
+
status: lifecycleStatusSchema,
|
|
169
|
+
enqueuedAt: utcIso86012
|
|
170
|
+
}).strict();
|
|
171
|
+
var acceptanceOutcomeSchema = z4.object({
|
|
172
|
+
verdict: z4.enum(["clean", "defects"]),
|
|
173
|
+
findings: z4.number().int().nonnegative(),
|
|
174
|
+
findingsDetail: z4.array(z4.object({ summary: z4.string() }).strict())
|
|
175
|
+
}).strict();
|
|
176
|
+
var PHASE_CLOSE_REASON_MAX = 1900;
|
|
177
|
+
var phaseCloseDecisionSchema = z4.discriminatedUnion("decision", [
|
|
178
|
+
z4.object({
|
|
179
|
+
decision: z4.literal("done-when-met"),
|
|
180
|
+
/** Who is closing the phase. */
|
|
181
|
+
actor: z4.string()
|
|
182
|
+
}).strict(),
|
|
183
|
+
z4.object({
|
|
184
|
+
decision: z4.literal("override"),
|
|
185
|
+
/** Who is closing it anyway. */
|
|
186
|
+
actor: z4.string(),
|
|
187
|
+
/**
|
|
188
|
+
* Why, in their own words. Bounded: the reason is kept on the phase's
|
|
189
|
+
* permanent record of its own close, and that record refuses anything
|
|
190
|
+
* longer — so an over-long reason is refused here, before the close is
|
|
191
|
+
* attempted, rather than failing the close itself.
|
|
192
|
+
*/
|
|
193
|
+
reason: z4.string().max(PHASE_CLOSE_REASON_MAX)
|
|
194
|
+
}).strict()
|
|
195
|
+
]);
|
|
196
|
+
var acceptPhaseRequestSchema = z4.object({
|
|
197
|
+
phase: z4.string(),
|
|
198
|
+
acceptanceOutcome: acceptanceOutcomeSchema,
|
|
199
|
+
close: phaseCloseDecisionSchema.optional()
|
|
200
|
+
}).strict();
|
|
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()
|
|
364
|
+
}).strict();
|
|
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
|
|
370
|
+
}).strict();
|
|
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)
|
|
384
|
+
}).strict();
|
|
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()
|
|
396
|
+
}).strict();
|
|
397
|
+
var deviceConfirmRecordedSchema = z6.object({
|
|
398
|
+
status: z6.literal(DEVICE_AUTH_STATUS.RECORDED),
|
|
399
|
+
accountId: z6.string().min(1).optional()
|
|
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
|
+
|
|
13
428
|
// dist/engagement-credential.js
|
|
14
429
|
import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
15
430
|
import { platform as platform2 } from "node:os";
|
|
@@ -165,507 +580,190 @@ function reconcileEnvText(existing, values, keys = ENGAGEMENT_ENV_KEYS) {
|
|
|
165
580
|
}
|
|
166
581
|
let result = out.join("\n");
|
|
167
582
|
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];
|
|
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;
|
|
331
591
|
}
|
|
332
|
-
merged.hooks = mergedHooks;
|
|
333
|
-
merged.permissions = mergePermissions(existing.permissions, generated.permissions);
|
|
334
|
-
return merged;
|
|
335
592
|
}
|
|
336
|
-
function
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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;
|
|
345
602
|
}
|
|
346
|
-
|
|
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;
|
|
347
610
|
}
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
// ../events/dist/result.js
|
|
354
|
-
import { z as z2 } from "zod";
|
|
355
|
-
|
|
356
|
-
// ../core/dist/guard-record.js
|
|
357
|
-
import { z } from "zod";
|
|
358
|
-
var severitySchema = z.enum(["info", "warn", "block"]);
|
|
359
|
-
var channelSchema = z.enum(["stable", "candidate", "rented"]);
|
|
360
|
-
var matcherKindSchema = z.enum(["grep", "ast", "integrationTest", "llm"]);
|
|
361
|
-
var matcherConfigSchema = z.record(z.string(), z.unknown());
|
|
362
|
-
var grepMatcherSchema = z.object({ kind: z.literal("grep"), config: matcherConfigSchema }).strict();
|
|
363
|
-
var astMatcherSchema = z.object({ kind: z.literal("ast"), config: matcherConfigSchema }).strict();
|
|
364
|
-
var integrationTestMatcherSchema = z.object({ kind: z.literal("integrationTest"), config: matcherConfigSchema }).strict();
|
|
365
|
-
var llmMatcherSchema = z.object({ kind: z.literal("llm"), config: matcherConfigSchema }).strict();
|
|
366
|
-
var matcherSchema = z.discriminatedUnion("kind", [
|
|
367
|
-
grepMatcherSchema,
|
|
368
|
-
astMatcherSchema,
|
|
369
|
-
integrationTestMatcherSchema,
|
|
370
|
-
llmMatcherSchema
|
|
371
|
-
]);
|
|
372
|
-
|
|
373
|
-
// ../events/dist/result.js
|
|
374
|
-
var firedGuardSchema = z2.object({
|
|
375
|
-
guardId: z2.string(),
|
|
376
|
-
patternRef: z2.string(),
|
|
377
|
-
severity: severitySchema,
|
|
378
|
-
explanation: z2.string()
|
|
379
|
-
}).strict();
|
|
380
|
-
var resultEnvelopeSchema = z2.object({
|
|
381
|
-
guardsFired: z2.array(firedGuardSchema),
|
|
382
|
-
severity: severitySchema.nullable(),
|
|
383
|
-
blocking: z2.boolean(),
|
|
384
|
-
explanation: z2.string()
|
|
385
|
-
}).strict();
|
|
386
|
-
var wireErrorSchema = z2.object({
|
|
387
|
-
statusCode: z2.number(),
|
|
388
|
-
error: z2.string(),
|
|
389
|
-
message: z2.string()
|
|
390
|
-
}).strict();
|
|
391
|
-
|
|
392
|
-
// ../events/dist/telemetry.js
|
|
393
|
-
import { z as z3 } from "zod";
|
|
394
|
-
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)");
|
|
395
|
-
var guardEvalOutcomeSchema = z3.enum([
|
|
396
|
-
"evaluated",
|
|
397
|
-
"infra-error",
|
|
398
|
-
"contract-error",
|
|
399
|
-
"unconfigured"
|
|
400
|
-
]);
|
|
401
|
-
var guardEvalRunSchema = z3.object({
|
|
402
|
-
// `runId`/`outcome` are the E2 run-observability additions, declared
|
|
403
|
-
// field-level-OPTIONAL as the FIRST (expand) step of an expand-contract
|
|
404
|
-
// migration. Every new record populated by the migrated writer (T-07)
|
|
405
|
-
// carries both; a legacy line predating the amendment carries neither and
|
|
406
|
-
// must still parse (the mixed-era guard-eval log the Build-Record reader
|
|
407
|
-
// assembles from, and the runner emit not yet migrated). So the presence
|
|
408
|
-
// refinement below is skipped for a legacy line (no `outcome`), and the
|
|
409
|
-
// per-writer guarantee lives with the writer + reader-normalisation (T-07),
|
|
410
|
-
// where the schema may then be tightened to required. runId is uuid-checked
|
|
411
|
-
// WHEN present; outcome is enum-checked when present.
|
|
412
|
-
runId: z3.string().uuid().optional(),
|
|
413
|
-
engagementId: z3.string().optional(),
|
|
414
|
-
runType: z3.enum(["hook", "ci"]),
|
|
415
|
-
occurredAt: utcIso8601,
|
|
416
|
-
outcome: guardEvalOutcomeSchema.optional(),
|
|
417
|
-
failureReason: z3.string().optional(),
|
|
418
|
-
phase: z3.string().optional(),
|
|
419
|
-
scope: z3.record(z3.string(), z3.unknown()),
|
|
420
|
-
cost: z3.record(z3.string(), z3.unknown()).optional(),
|
|
421
|
-
result: resultEnvelopeSchema.optional()
|
|
422
|
-
}).strict().superRefine((run, ctx) => {
|
|
423
|
-
if (run.outcome === void 0)
|
|
611
|
+
function applyOwnerOnly(path, dir) {
|
|
612
|
+
if (platform2() === "win32")
|
|
424
613
|
return;
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
path: ["result"],
|
|
429
|
-
message: "result is required when outcome is 'evaluated'"
|
|
430
|
-
});
|
|
431
|
-
}
|
|
432
|
-
if (run.outcome !== "evaluated" && run.result !== void 0) {
|
|
433
|
-
ctx.addIssue({
|
|
434
|
-
code: z3.ZodIssueCode.custom,
|
|
435
|
-
path: ["result"],
|
|
436
|
-
message: "result is only present when outcome is 'evaluated'"
|
|
437
|
-
});
|
|
614
|
+
try {
|
|
615
|
+
chmodSync2(path, 384);
|
|
616
|
+
} catch {
|
|
438
617
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
path: ["engagementId"],
|
|
443
|
-
message: "engagementId is required unless outcome is 'unconfigured'"
|
|
444
|
-
});
|
|
618
|
+
try {
|
|
619
|
+
chmodSync2(dir, 448);
|
|
620
|
+
} catch {
|
|
445
621
|
}
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
findingsDetail: z4.array(z4.object({ summary: z4.string() }).strict())
|
|
491
|
-
}).strict();
|
|
492
|
-
var phaseCloseDecisionSchema = z4.discriminatedUnion("decision", [
|
|
493
|
-
z4.object({
|
|
494
|
-
decision: z4.literal("done-when-met"),
|
|
495
|
-
/** Who is closing the phase. */
|
|
496
|
-
actor: z4.string()
|
|
497
|
-
}).strict(),
|
|
498
|
-
z4.object({
|
|
499
|
-
decision: z4.literal("override"),
|
|
500
|
-
/** Who is closing it anyway. */
|
|
501
|
-
actor: z4.string(),
|
|
502
|
-
/** Why, in their own words. */
|
|
503
|
-
reason: z4.string()
|
|
504
|
-
}).strict()
|
|
505
|
-
]);
|
|
506
|
-
var acceptPhaseRequestSchema = z4.object({
|
|
507
|
-
phase: z4.string(),
|
|
508
|
-
acceptanceOutcome: acceptanceOutcomeSchema,
|
|
509
|
-
close: phaseCloseDecisionSchema.optional()
|
|
510
|
-
}).strict();
|
|
511
|
-
var coeFindingRequestSchema = z4.object({
|
|
512
|
-
phase: z4.string(),
|
|
513
|
-
description: z4.string(),
|
|
514
|
-
addRateTag: z4.enum(["seam-new", "seam-repeat", "model-limitation"]),
|
|
515
|
-
mappedPatternRef: z4.string().nullable()
|
|
516
|
-
}).strict();
|
|
517
|
-
var coeFindingResponseSchema = z4.object({
|
|
518
|
-
findingId: z4.string(),
|
|
519
|
-
engagementId: z4.string(),
|
|
520
|
-
phase: z4.string(),
|
|
521
|
-
description: z4.string(),
|
|
522
|
-
addRateTag: z4.enum(["seam-new", "seam-repeat", "model-limitation"]),
|
|
523
|
-
mappedPatternRef: z4.string().nullable(),
|
|
524
|
-
createdAt: utcIso86012
|
|
525
|
-
}).strict();
|
|
526
|
-
var phaseEntryDecisionSchema = z4.discriminatedUnion("decision", [
|
|
527
|
-
z4.object({
|
|
528
|
-
decision: z4.literal("entry-condition-met"),
|
|
529
|
-
/** Who attests. */
|
|
530
|
-
actor: z4.string(),
|
|
531
|
-
/** What makes the entry condition hold, in the attester's own words. */
|
|
532
|
-
evidence: z4.string()
|
|
533
|
-
}).strict(),
|
|
534
|
-
z4.object({
|
|
535
|
-
decision: z4.literal("override"),
|
|
536
|
-
/** Who is opening it anyway. */
|
|
537
|
-
actor: z4.string(),
|
|
538
|
-
/** Why, in the operator's own words. */
|
|
539
|
-
reason: z4.string()
|
|
540
|
-
}).strict()
|
|
541
|
-
]);
|
|
542
|
-
var openPhaseRequestSchema = z4.object({
|
|
543
|
-
phase: z4.string().nullable(),
|
|
544
|
-
entry: phaseEntryDecisionSchema
|
|
545
|
-
}).strict();
|
|
546
|
-
var phaseEntryDecisionRecordSchema = z4.object({
|
|
547
|
-
decisionId: z4.string(),
|
|
548
|
-
decision: z4.enum(["entry-condition-met", "override"]),
|
|
549
|
-
actor: z4.string(),
|
|
550
|
-
/** The evidence (attestation) or the reason (override) — one basis per row. */
|
|
551
|
-
justification: z4.string(),
|
|
552
|
-
blockingFinding: z4.string().nullable(),
|
|
553
|
-
decidedAt: utcIso86012
|
|
554
|
-
}).strict();
|
|
555
|
-
var openPhaseResponseSchema = z4.object({
|
|
556
|
-
engagement: engagementStatusResponseSchema,
|
|
557
|
-
/** What is open now. `null` is project scope. */
|
|
558
|
-
phase: z4.string().nullable(),
|
|
559
|
-
/** What was open before this decision. `null` is project scope. */
|
|
560
|
-
previousPhase: z4.string().nullable(),
|
|
561
|
-
decision: phaseEntryDecisionRecordSchema
|
|
562
|
-
}).strict();
|
|
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
|
+
}
|
|
563
666
|
|
|
564
|
-
//
|
|
565
|
-
import {
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
APPROVED: "approved",
|
|
572
|
-
/** Confirm: the decision was recorded (approve or decline). 200. */
|
|
573
|
-
RECORDED: "recorded",
|
|
574
|
-
/** The human refused at the confirmation page. 400 on the poll. */
|
|
575
|
-
DECLINED: "declined",
|
|
576
|
-
/** The code's absolute expiry has passed. 400. */
|
|
577
|
-
EXPIRED: "expired",
|
|
578
|
-
/** Never minted, or already spent. 400. */
|
|
579
|
-
UNKNOWN: "unknown",
|
|
580
|
-
/** Confirm: neither `approve` nor `decline` was named. Nothing was bound. 400. */
|
|
581
|
-
INVALID_DECISION: "invalid-decision",
|
|
582
|
-
/** Confirm: an approval arrived with no signed-in identity. 400. */
|
|
583
|
-
IDENTITY_REQUIRED: "identity-required",
|
|
584
|
-
/** This deployment has no `SIGNIN_BASE_URL` / `DEVICE_AUTH_CONFIRM_SECRET`. 503. */
|
|
585
|
-
NOT_CONFIGURED: "not-configured",
|
|
586
|
-
/** Confirm: the shared confirmation secret was missing or wrong. 401. */
|
|
587
|
-
UNAUTHORIZED: "unauthorized",
|
|
588
|
-
/** Confirm: the code was already approved, declined or claimed. 409. */
|
|
589
|
-
ALREADY_DECIDED: "already-decided"
|
|
590
|
-
};
|
|
591
|
-
var DEVICE_POLL_REFUSALS = [
|
|
592
|
-
DEVICE_AUTH_STATUS.DECLINED,
|
|
593
|
-
DEVICE_AUTH_STATUS.EXPIRED,
|
|
594
|
-
DEVICE_AUTH_STATUS.UNKNOWN
|
|
595
|
-
];
|
|
596
|
-
var deviceAuthStartResponseSchema = z5.object({
|
|
597
|
-
deviceCode: z5.string().min(1),
|
|
598
|
-
userCode: z5.string().min(1),
|
|
599
|
-
verificationUrl: z5.string().min(1),
|
|
600
|
-
expiresAt: utcIso86013,
|
|
601
|
-
pollIntervalMs: z5.number().int().positive()
|
|
602
|
-
}).strict();
|
|
603
|
-
var devicePollPendingSchema = z5.object({
|
|
604
|
-
status: z5.literal(DEVICE_AUTH_STATUS.PENDING),
|
|
605
|
-
pollIntervalMs: z5.number().int().positive()
|
|
606
|
-
}).strict();
|
|
607
|
-
var devicePollApprovedSchema = z5.object({
|
|
608
|
-
status: z5.literal(DEVICE_AUTH_STATUS.APPROVED),
|
|
609
|
-
accountId: z5.string().min(1),
|
|
610
|
-
credential: z5.string().min(1),
|
|
611
|
-
expiresAt: utcIso86013
|
|
612
|
-
}).strict();
|
|
613
|
-
var deviceAuthRefusalSchema = z5.object({
|
|
614
|
-
status: z5.enum([
|
|
615
|
-
DEVICE_AUTH_STATUS.DECLINED,
|
|
616
|
-
DEVICE_AUTH_STATUS.EXPIRED,
|
|
617
|
-
DEVICE_AUTH_STATUS.UNKNOWN,
|
|
618
|
-
DEVICE_AUTH_STATUS.INVALID_DECISION,
|
|
619
|
-
DEVICE_AUTH_STATUS.IDENTITY_REQUIRED,
|
|
620
|
-
DEVICE_AUTH_STATUS.NOT_CONFIGURED,
|
|
621
|
-
DEVICE_AUTH_STATUS.UNAUTHORIZED,
|
|
622
|
-
DEVICE_AUTH_STATUS.ALREADY_DECIDED
|
|
623
|
-
]),
|
|
624
|
-
message: z5.string().min(1)
|
|
625
|
-
}).strict();
|
|
626
|
-
var devicePollResponseSchema = z5.union([
|
|
627
|
-
devicePollPendingSchema,
|
|
628
|
-
devicePollApprovedSchema,
|
|
629
|
-
deviceAuthRefusalSchema
|
|
630
|
-
]);
|
|
631
|
-
var deviceConfirmRequestSchema = z5.object({
|
|
632
|
-
userCode: z5.string().min(1),
|
|
633
|
-
decision: z5.enum(["approve", "decline"]),
|
|
634
|
-
externalAuthId: z5.string().min(1).optional(),
|
|
635
|
-
email: z5.string().optional()
|
|
636
|
-
}).strict();
|
|
637
|
-
var deviceConfirmRecordedSchema = z5.object({
|
|
638
|
-
status: z5.literal(DEVICE_AUTH_STATUS.RECORDED),
|
|
639
|
-
accountId: z5.string().min(1).optional()
|
|
640
|
-
}).strict();
|
|
641
|
-
var deviceConfirmResponseSchema = z5.union([
|
|
642
|
-
deviceConfirmRecordedSchema,
|
|
643
|
-
deviceAuthRefusalSchema
|
|
644
|
-
]);
|
|
645
|
-
var LOOPBACK_REDIRECT_HOST = "127.0.0.1";
|
|
646
|
-
var LOOPBACK_PORT_MIN = 1024;
|
|
647
|
-
var LOOPBACK_PORT_MAX = 65535;
|
|
648
|
-
var LOOPBACK_CALLBACK_PATH = "/halfcycle-cli-callback";
|
|
649
|
-
var LOOPBACK_QUERY_PORT = "loopback_port";
|
|
650
|
-
var LOOPBACK_QUERY_STATE = "loopback_state";
|
|
651
|
-
var LOOPBACK_QUERY_RESULT = "result";
|
|
652
|
-
var LOOPBACK_RESULT = {
|
|
653
|
-
APPROVED: "approved",
|
|
654
|
-
DECLINED: "declined"
|
|
655
|
-
};
|
|
656
|
-
function loopbackVerificationUrl(verificationUrl, target) {
|
|
657
|
-
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) {
|
|
658
674
|
try {
|
|
659
|
-
|
|
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;
|
|
660
681
|
} catch {
|
|
661
|
-
return
|
|
682
|
+
return null;
|
|
662
683
|
}
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
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;
|
|
666
762
|
}
|
|
667
763
|
|
|
668
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";
|
|
669
767
|
var HALFCYCLE_STATE_FORMAT = "halfcycle-state-file/v1";
|
|
670
768
|
var HALFCYCLE_STATE_NOTE = [
|
|
671
769
|
"This file is a local cache written ONCE when Halfcycle was installed. It is not a",
|
|
@@ -801,6 +899,18 @@ function writeCollisionSafe(targetAbsPath, targetRepoRoot, content) {
|
|
|
801
899
|
writeFileSync4(targetAbsPath, content, "utf-8");
|
|
802
900
|
return "written";
|
|
803
901
|
}
|
|
902
|
+
function writeOwned(targetAbsPath, targetRepoRoot, content) {
|
|
903
|
+
const rel = relative(targetRepoRoot, targetAbsPath).replace(/\\/g, "/");
|
|
904
|
+
if (!isAllowlisted(rel)) {
|
|
905
|
+
throw new Error(`[bundle install] Write-allowlist violation: attempted to write "${rel}". Only the method surface and scaffolding paths are writable.`);
|
|
906
|
+
}
|
|
907
|
+
if (existsSync3(targetAbsPath) && readFileSync5(targetAbsPath, "utf-8") === content) {
|
|
908
|
+
return "skipped";
|
|
909
|
+
}
|
|
910
|
+
mkdirSync4(dirname2(targetAbsPath), { recursive: true });
|
|
911
|
+
writeFileSync4(targetAbsPath, content, "utf-8");
|
|
912
|
+
return "written";
|
|
913
|
+
}
|
|
804
914
|
function record(report, outcome, rel) {
|
|
805
915
|
const bucket = {
|
|
806
916
|
written: report.writtenPaths,
|
|
@@ -921,6 +1031,42 @@ if [ -z "$REPO_ROOT" ]; then exit 0; fi
|
|
|
921
1031
|
HASH="$(echo -n "$REPO_ROOT" | shasum -a 256 | cut -c1-12)"
|
|
922
1032
|
MARKER_FILE="\${TMPDIR%/}/halfcycle-session-\${HASH}.ref"
|
|
923
1033
|
git -C "$REPO_ROOT" rev-parse HEAD > "$MARKER_FILE" 2>/dev/null || true
|
|
1034
|
+
|
|
1035
|
+
# ---------------------------------------------------------------------------
|
|
1036
|
+
# STANDING GUARD-COVERAGE STATEMENT (T-27).
|
|
1037
|
+
#
|
|
1038
|
+
# A repository with no credential on this machine is UNGUARDED: the PostToolUse
|
|
1039
|
+
# hook will find nothing to evaluate with, and it says so once per edit, after
|
|
1040
|
+
# the edit. This says it once, up front, before anything is written \u2014 which is
|
|
1041
|
+
# the one thing a session-start hook can do that nothing else can.
|
|
1042
|
+
#
|
|
1043
|
+
# Everything below runs in a SUBSHELL: it sources a credential store, and a
|
|
1044
|
+
# session-start hook must not leak an engagement's variables into whatever the
|
|
1045
|
+
# editor runs next. Any failure inside it is swallowed; this script's exit
|
|
1046
|
+
# status is 0 either way, and the marker above has already been written.
|
|
1047
|
+
# ---------------------------------------------------------------------------
|
|
1048
|
+
${engagementResolutionShell()}
|
|
1049
|
+
|
|
1050
|
+
(
|
|
1051
|
+
if halfcycle_env_file "$REPO_ROOT"; then
|
|
1052
|
+
set -a
|
|
1053
|
+
. "$HALFCYCLE_ENV_FILE"
|
|
1054
|
+
set +a
|
|
1055
|
+
fi
|
|
1056
|
+
hc_missing=""
|
|
1057
|
+
for hc_key in ${GUARD_ENV_KEYS.join(" ")}; do
|
|
1058
|
+
eval "hc_value=\\\${$hc_key:-}"
|
|
1059
|
+
if [ -z "$hc_value" ]; then hc_missing="$hc_missing $hc_key"; fi
|
|
1060
|
+
done
|
|
1061
|
+
if [ -n "$hc_missing" ]; then
|
|
1062
|
+
echo "[halfcycle] NO GUARD COVERAGE IN THIS REPOSITORY. Nothing will evaluate the edits made in"
|
|
1063
|
+
echo "[halfcycle] this session: this machine has no Halfcycle credential for it, so the guard hook"
|
|
1064
|
+
echo "[halfcycle] has nothing to call with (missing:$hc_missing)."
|
|
1065
|
+
echo "[halfcycle] Run \\"npx halfcycle\\" in this repository to fix it \u2014 it signs you in through your"
|
|
1066
|
+
echo "[halfcycle] browser if needed, writes the credential to \\$HOME/.halfcycle outside this tree,"
|
|
1067
|
+
echo "[halfcycle] and needs nothing configured first."
|
|
1068
|
+
fi
|
|
1069
|
+
) 2>/dev/null || true
|
|
924
1070
|
exit 0
|
|
925
1071
|
`;
|
|
926
1072
|
}
|
|
@@ -934,6 +1080,13 @@ function generateUserPromptReminderHook() {
|
|
|
934
1080
|
# one home: RULE_SENTENCE_PLAIN in the method repo's own position-house-rule
|
|
935
1081
|
# test governs it \u2014 reword there and here together, in the same commit.
|
|
936
1082
|
#
|
|
1083
|
+
# SILENT WHEN THE TOOL IT NAMES IS NOT THERE. The sentence tells the session to
|
|
1084
|
+
# call halfcycle_resolve, which arrives over the Halfcycle MCP server registered
|
|
1085
|
+
# in .mcp.json. If this repository carries no such registration, the instruction
|
|
1086
|
+
# cannot be followed, and a per-prompt instruction that cannot be followed is
|
|
1087
|
+
# worse than silence. The standing "no coverage" statement belongs in the
|
|
1088
|
+
# session-start banner, which says it once; this hook just stops talking.
|
|
1089
|
+
#
|
|
937
1090
|
# FAILS OPEN. This hook's stdout is always plain text, never JSON, so it
|
|
938
1091
|
# cannot form a {"decision":"block",...} body (UserPromptSubmit's only way to
|
|
939
1092
|
# hold the turn). But a write failure here (a closed stdout, say) must still
|
|
@@ -941,6 +1094,26 @@ function generateUserPromptReminderHook() {
|
|
|
941
1094
|
# than propagate.
|
|
942
1095
|
trap 'exit 0' ERR
|
|
943
1096
|
set -e
|
|
1097
|
+
|
|
1098
|
+
# The project root is resolved from THIS SCRIPT'S OWN LOCATION, never from the
|
|
1099
|
+
# session's cwd \u2014 a session started in a subdirectory must find the same root
|
|
1100
|
+
# .mcp.json the editor loaded, which is the bug mcp-headers.sh already paid for.
|
|
1101
|
+
HC_SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
|
1102
|
+
HC_PROJECT_ROOT=$(CDPATH= cd -- "$HC_SCRIPT_DIR/../.." && pwd)
|
|
1103
|
+
HC_REGISTRATION="$HC_PROJECT_ROOT/${MCP_REGISTRATION_REL}"
|
|
1104
|
+
|
|
1105
|
+
# Presence of the FILE is not enough \u2014 a developer's own .mcp.json with no
|
|
1106
|
+
# Halfcycle server in it registers no halfcycle_resolve. \`grep\` is guarded with
|
|
1107
|
+
# \`|| true\` because a non-match is a status, not an error, and \`set -e\` would
|
|
1108
|
+
# otherwise route it through the trap (same outcome here, but by accident).
|
|
1109
|
+
HC_REGISTERED=""
|
|
1110
|
+
if [ -f "$HC_REGISTRATION" ]; then
|
|
1111
|
+
HC_REGISTERED=$(tr -d '\\n' < "$HC_REGISTRATION" | grep -c '"${MCP_SERVER_KEY}"[[:space:]]*:' || true)
|
|
1112
|
+
fi
|
|
1113
|
+
if [ -z "$HC_REGISTERED" ] || [ "$HC_REGISTERED" = "0" ]; then
|
|
1114
|
+
exit 0
|
|
1115
|
+
fi
|
|
1116
|
+
|
|
944
1117
|
printf '%s\\n' '${USER_PROMPT_REMINDER_SENTENCE}'
|
|
945
1118
|
exit 0
|
|
946
1119
|
`;
|
|
@@ -989,6 +1162,13 @@ var ENV_EXAMPLE_NAMES = [
|
|
|
989
1162
|
"GUARD_SERVICE_URL",
|
|
990
1163
|
"GUARD_SERVICE_TOKEN",
|
|
991
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.
|
|
992
1172
|
"HALFCYCLE_SERVICE_URL",
|
|
993
1173
|
"HALFCYCLE_MCP_URL",
|
|
994
1174
|
"HALFCYCLE_TOKEN",
|
|
@@ -1022,8 +1202,14 @@ GUARD_ENGAGEMENT_ID=<argType:runtime>
|
|
|
1022
1202
|
# TWO ORIGINS, TWO SERVICES (T-22). HALFCYCLE_SERVICE_URL is the Halfcycle
|
|
1023
1203
|
# CONTROL plane \u2014 it serves engagement creation and the board's first-visit
|
|
1024
1204
|
# code. HALFCYCLE_MCP_URL is the method-delivery service, which serves /mcp and
|
|
1025
|
-
# nothing else here.
|
|
1026
|
-
#
|
|
1205
|
+
# nothing else here. The platform reports the second on create, so it is written
|
|
1206
|
+
# for you and .mcp.json carries the composed address.
|
|
1207
|
+
#
|
|
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.
|
|
1027
1213
|
HALFCYCLE_SERVICE_URL=<argType:runtime>
|
|
1028
1214
|
HALFCYCLE_MCP_URL=<argType:runtime>
|
|
1029
1215
|
HALFCYCLE_TOKEN=<argType:runtime>
|
|
@@ -1036,6 +1222,7 @@ CONTROL_TELEMETRY_URL=<argType:runtime>
|
|
|
1036
1222
|
`;
|
|
1037
1223
|
}
|
|
1038
1224
|
var MCP_REGISTRATION_REL = ".mcp.json";
|
|
1225
|
+
var MCP_SERVER_KEY = "halfcycle";
|
|
1039
1226
|
var MCP_HEADERS_HELPER_REL = ".halfcycle/mcp-headers.sh";
|
|
1040
1227
|
var MCP_HEADERS_HELPER_COMMAND = `/bin/sh -c 'd=$(pwd); while [ ! -f "$d/${MCP_HEADERS_HELPER_REL}" ] && [ "$d" != / ]; do d=$(dirname "$d"); done; [ -f "$d/${MCP_HEADERS_HELPER_REL}" ] || { echo "halfcycle: ${MCP_HEADERS_HELPER_REL} not found at or above $(pwd); re-run npx halfcycle" >&2; exit 1; }; exec /bin/sh "$d/${MCP_HEADERS_HELPER_REL}"'`;
|
|
1041
1228
|
function generateMcpHeadersHelper() {
|
|
@@ -1148,7 +1335,7 @@ function generateMcpRegistration(existing, mcpOrigin) {
|
|
|
1148
1335
|
if (typeof base.mcpServers !== "object" || base.mcpServers === null) {
|
|
1149
1336
|
base.mcpServers = {};
|
|
1150
1337
|
}
|
|
1151
|
-
base.mcpServers[
|
|
1338
|
+
base.mcpServers[MCP_SERVER_KEY] = {
|
|
1152
1339
|
type: "http",
|
|
1153
1340
|
url: mcpEndpointUrl(mcpOrigin),
|
|
1154
1341
|
headersHelper: MCP_HEADERS_HELPER_COMMAND
|
|
@@ -1208,6 +1395,16 @@ function writeBundlePin(targetRepoRoot, version, engagementId, engagementType, w
|
|
|
1208
1395
|
const pinPath = join5(targetRepoRoot, ".halfcycle", "bundle.json");
|
|
1209
1396
|
writeAllowlisted(pinPath, targetRepoRoot, JSON.stringify(pin, null, 2) + "\n", writtenPaths);
|
|
1210
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
|
+
}
|
|
1211
1408
|
function readBundlePin(targetRepoRoot) {
|
|
1212
1409
|
const pinPath = join5(targetRepoRoot, ".halfcycle", "bundle.json");
|
|
1213
1410
|
if (!existsSync3(pinPath))
|
|
@@ -1370,7 +1567,7 @@ async function install(options) {
|
|
|
1370
1567
|
}
|
|
1371
1568
|
const vendoredBinSrc = resolveVendoredBinary();
|
|
1372
1569
|
const vendoredBinDest = join5(targetRepo, ".halfcycle", "bin", "bin.bundle.mjs");
|
|
1373
|
-
record(report,
|
|
1570
|
+
record(report, writeOwned(vendoredBinDest, targetRepo, readFileSync5(vendoredBinSrc, "utf-8")), ".halfcycle/bin/bin.bundle.mjs");
|
|
1374
1571
|
const settingsPath = join5(targetRepo, ".claude", "settings.json");
|
|
1375
1572
|
const settingsPreexisted = existsSync3(settingsPath);
|
|
1376
1573
|
const generatedSettings = JSON.parse(generateSettingsJson());
|
|
@@ -1439,6 +1636,7 @@ async function install(options) {
|
|
|
1439
1636
|
break;
|
|
1440
1637
|
}
|
|
1441
1638
|
writeBundlePin(targetRepo, manifest.version, engagementId, engagementType, report.writtenPaths);
|
|
1639
|
+
writeCrewRoster(targetRepo, report);
|
|
1442
1640
|
const scanResult = runBootstrapScan(targetRepo);
|
|
1443
1641
|
return {
|
|
1444
1642
|
version: manifest.version,
|
|
@@ -1451,6 +1649,18 @@ async function install(options) {
|
|
|
1451
1649
|
};
|
|
1452
1650
|
}
|
|
1453
1651
|
|
|
1652
|
+
// dist/control-origin.js
|
|
1653
|
+
var DEFAULT_CONTROL_ORIGIN = "https://control.halfcycle.ai";
|
|
1654
|
+
function resolveControlOrigin(env = process.env) {
|
|
1655
|
+
const configured = env["HALFCYCLE_SERVICE_URL"]?.trim();
|
|
1656
|
+
if (configured)
|
|
1657
|
+
return { origin: configured.replace(/\/+$/, ""), source: "environment" };
|
|
1658
|
+
return { origin: DEFAULT_CONTROL_ORIGIN, source: "default" };
|
|
1659
|
+
}
|
|
1660
|
+
function controlOriginNote(resolved) {
|
|
1661
|
+
return resolved.source === "environment" ? `${resolved.origin} (from HALFCYCLE_SERVICE_URL)` : `${resolved.origin} (the Halfcycle plane)`;
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1454
1664
|
// dist/create-engagement.js
|
|
1455
1665
|
var CreateEngagementRefused = class extends Error {
|
|
1456
1666
|
status;
|
|
@@ -1508,7 +1718,7 @@ function planeRefusal(detail) {
|
|
|
1508
1718
|
error: typeof candidate.error === "string" ? candidate.error : void 0
|
|
1509
1719
|
};
|
|
1510
1720
|
}
|
|
1511
|
-
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.`;
|
|
1512
1722
|
async function createEngagement(baseUrl, name, credential) {
|
|
1513
1723
|
return requestEngagementValues(`${baseUrl.replace(/\/+$/, "")}/engagements`, name ? { name } : {}, credential, { action: "Create-engagement", nothingHappened: "No engagement was created", route: "POST /engagements" });
|
|
1514
1724
|
}
|
|
@@ -2500,7 +2710,7 @@ async function closePhase(credential, phase, outcome, close) {
|
|
|
2500
2710
|
}
|
|
2501
2711
|
const parsedClose = phaseCloseDecisionSchema.safeParse(close);
|
|
2502
2712
|
if (!parsedClose.success) {
|
|
2503
|
-
throw new Error(`[halfcycle] That is not a phase-close decision this platform accepts: ${parsedClose.error.issues.map((i) => i.message).join("; ")}.`);
|
|
2713
|
+
throw new Error(`[halfcycle] That is not a phase-close decision this platform accepts: ${parsedClose.error.issues.map((i) => `${i.path.join(".") || "(decision)"}: ${i.message}`).join("; ")}.`);
|
|
2504
2714
|
}
|
|
2505
2715
|
const url = `${credential.serviceUrl}/engagements/${encodeURIComponent(credential.engagementId)}/accept`;
|
|
2506
2716
|
let res;
|
|
@@ -2774,45 +2984,43 @@ ${USAGE}`);
|
|
|
2774
2984
|
return;
|
|
2775
2985
|
}
|
|
2776
2986
|
try {
|
|
2777
|
-
const
|
|
2778
|
-
const
|
|
2779
|
-
|
|
2987
|
+
const controlOrigin = resolveControlOrigin(process.env);
|
|
2988
|
+
const serviceUrl = controlOrigin.origin;
|
|
2989
|
+
process.stdout.write(`[halfcycle] Halfcycle plane: ${controlOriginNote(controlOrigin)}
|
|
2990
|
+
`);
|
|
2991
|
+
const pinned = readPinnedEngagement(targetRepo);
|
|
2992
|
+
const requestedId = engagementIdArg ?? pinned?.engagementId;
|
|
2993
|
+
const reusable = pinned !== null && pinned.engagementId === requestedId ? pinned.credential : void 0;
|
|
2994
|
+
let engagementId;
|
|
2780
2995
|
let credential;
|
|
2781
|
-
if (pinned) {
|
|
2782
|
-
|
|
2996
|
+
if (reusable !== void 0 && pinned !== null) {
|
|
2997
|
+
engagementId = pinned.engagementId;
|
|
2998
|
+
credential = reusable;
|
|
2783
2999
|
process.stdout.write(`[halfcycle] Re-using the engagement already pinned in this repo: ${pinned.engagementId}
|
|
2784
3000
|
`);
|
|
2785
|
-
if (
|
|
2786
|
-
const joined = await joinPinnedEngagement(serviceUrl, pinned.engagementId);
|
|
2787
|
-
credential = {
|
|
2788
|
-
serviceUrl,
|
|
2789
|
-
token: joined.sessionToken,
|
|
2790
|
-
mcpUrl: joined.mcpUrl,
|
|
2791
|
-
guardUrl: joined.guardUrl,
|
|
2792
|
-
// T-10 — optional; absent on a plane that has not configured
|
|
2793
|
-
// CONTROL_TELEMETRY_URL yet. `writeEngagementCredential` writes '' for
|
|
2794
|
-
// an absent value, never `undefined` verbatim.
|
|
2795
|
-
controlTelemetryUrl: joined.controlTelemetryUrl
|
|
2796
|
-
};
|
|
2797
|
-
process.stdout.write(`[halfcycle] Joined engagement ${joined.engagementId} \u2014 this credential is yours. Everyone else's keeps working; nobody was signed out and nothing was pasted.
|
|
2798
|
-
`);
|
|
2799
|
-
} else if (credential === void 0) {
|
|
2800
|
-
process.stdout.write(`[halfcycle] This machine holds no credential for it, and HALFCYCLE_SERVICE_URL is not
|
|
2801
|
-
[halfcycle] set \u2014 so there is no plane to ask for one. Nothing will be minted, and the
|
|
2802
|
-
[halfcycle] board link and MCP calls will not work until it is. Looked in
|
|
2803
|
-
[halfcycle] ${engagementEnvPath(pinned.engagementId)}.
|
|
2804
|
-
[halfcycle] Set that variable and re-run to get a credential of your own for this
|
|
2805
|
-
[halfcycle] engagement. To start a NEW engagement instead, remove
|
|
2806
|
-
[halfcycle] .halfcycle/bundle.json first.
|
|
2807
|
-
`);
|
|
2808
|
-
} else if (pinned.fromLegacyEnvLocal) {
|
|
3001
|
+
if (pinned.fromLegacyEnvLocal) {
|
|
2809
3002
|
process.stdout.write(`[halfcycle] Its credential is in this repository's .env.local \u2014 an install from before
|
|
2810
3003
|
[halfcycle] credentials moved out of the tree. It is being copied to
|
|
2811
3004
|
[halfcycle] ${engagementEnvPath(pinned.engagementId)} and taken back out of .env.local.
|
|
2812
3005
|
[halfcycle] Anything else that file holds is untouched.
|
|
2813
3006
|
`);
|
|
2814
3007
|
}
|
|
2815
|
-
} else if (
|
|
3008
|
+
} else if (requestedId !== void 0) {
|
|
3009
|
+
const joined = await joinPinnedEngagement(serviceUrl, requestedId);
|
|
3010
|
+
engagementId = joined.engagementId;
|
|
3011
|
+
credential = {
|
|
3012
|
+
serviceUrl,
|
|
3013
|
+
token: joined.sessionToken,
|
|
3014
|
+
mcpUrl: joined.mcpUrl,
|
|
3015
|
+
guardUrl: joined.guardUrl,
|
|
3016
|
+
// T-10 — optional; absent on a plane that has not configured
|
|
3017
|
+
// CONTROL_TELEMETRY_URL yet. `writeEngagementCredential` writes '' for
|
|
3018
|
+
// an absent value, never `undefined` verbatim.
|
|
3019
|
+
controlTelemetryUrl: joined.controlTelemetryUrl
|
|
3020
|
+
};
|
|
3021
|
+
process.stdout.write(`[halfcycle] Joined engagement ${joined.engagementId} \u2014 this credential is yours. Everyone else's keeps working; nobody was signed out and nothing was pasted.
|
|
3022
|
+
`);
|
|
3023
|
+
} else {
|
|
2816
3024
|
const created = await createOwnedEngagement(serviceUrl);
|
|
2817
3025
|
engagementId = created.engagementId;
|
|
2818
3026
|
credential = {
|
|
@@ -2863,44 +3071,32 @@ ${USAGE}`);
|
|
|
2863
3071
|
[halfcycle] ${missing.length === 1 ? "it" : "them"}) and re-run, and the credential moves out of the tree.
|
|
2864
3072
|
`);
|
|
2865
3073
|
}
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
process.stdout.write(`[halfcycle] Method delivery (MCP): ${credential.mcpUrl} \u2014 reachable (${probe.serverName})
|
|
2870
|
-
`);
|
|
2871
|
-
} else {
|
|
2872
|
-
process.stdout.write(`[halfcycle] Method delivery (MCP): ${credential.mcpUrl} \u2014 NOT reachable: ${probe.problem}.
|
|
2873
|
-
[halfcycle] The Halfcycle commands will have no method context until that address answers. This is the SECOND of two origins and the platform supplied it, so it is not your HALFCYCLE_SERVICE_URL (${credential.serviceUrl}) that is wrong \u2014 that one just worked. It is recorded as HALFCYCLE_MCP_URL in this engagement's credential store; re-run this installer once the service is up.
|
|
2874
|
-
`);
|
|
2875
|
-
}
|
|
2876
|
-
}
|
|
2877
|
-
if (credential) {
|
|
2878
|
-
process.stdout.write(`[halfcycle] Guard evaluation: ON \u2014 the hook evaluates against ${credential.guardUrl}. Credentials (${GUARD_ENV_KEYS.join(", ")}) are in ${engagementEnvPath(engagementId)}, outside this repository; the hook loads them itself, so you do not export anything and nothing here can be committed.
|
|
3074
|
+
const probe = await probeMcpOrigin(credential.mcpUrl);
|
|
3075
|
+
if (probe.reached) {
|
|
3076
|
+
process.stdout.write(`[halfcycle] Method delivery (MCP): ${credential.mcpUrl} \u2014 reachable (${probe.serverName})
|
|
2879
3077
|
`);
|
|
2880
3078
|
} else {
|
|
2881
|
-
|
|
2882
|
-
|
|
3079
|
+
process.stdout.write(`[halfcycle] Method delivery (MCP): ${credential.mcpUrl} \u2014 NOT reachable: ${probe.problem}.
|
|
3080
|
+
[halfcycle] The Halfcycle commands will have no method context until that address answers. This is the SECOND of two origins and the platform supplied it, so it is not the CONTROL origin (${credential.serviceUrl}) that is wrong \u2014 that one just worked. It is recorded as HALFCYCLE_MCP_URL in this engagement's credential store; re-run this installer once the service is up.
|
|
2883
3081
|
`);
|
|
2884
3082
|
}
|
|
2885
|
-
|
|
2886
|
-
process.stdout.write(`[halfcycle] Your credential is at ${engagementEnvPath(engagementId)} \u2014 outside this repository, readable only by you. .mcp.json reads it at connection time through .halfcycle/mcp-headers.sh, so there is no token in this tree to commit and nothing for you to export.
|
|
3083
|
+
process.stdout.write(`[halfcycle] Guard evaluation: ON \u2014 the hook evaluates against ${credential.guardUrl}. Credentials (${GUARD_ENV_KEYS.join(", ")}) are in ${engagementEnvPath(engagementId)}, outside this repository; the hook loads them itself, so you do not export anything and nothing here can be committed.
|
|
2887
3084
|
`);
|
|
2888
|
-
|
|
3085
|
+
process.stdout.write(`[halfcycle] Your credential is at ${engagementEnvPath(engagementId)} \u2014 outside this repository, readable only by you. .mcp.json reads it at connection time through .halfcycle/mcp-headers.sh, so there is no token in this tree to commit and nothing for you to export.
|
|
2889
3086
|
`);
|
|
2890
|
-
|
|
3087
|
+
process.stdout.write(`[halfcycle] Expect a workspace trust prompt the first time you open this folder \u2014 the helper is a shell command, and Claude Code will not run it until you accept. Needs Claude Code ${MIN_HEADERS_HELPER_VERSION} or newer (${MIN_HEADER_ROTATION_VERSION}+ to re-authenticate a rotated token without restarting).
|
|
2891
3088
|
`);
|
|
2892
|
-
|
|
2893
|
-
}
|
|
2894
|
-
if (credential) {
|
|
2895
|
-
try {
|
|
2896
|
-
const minted = await mintBoardEnterCode(credential.serviceUrl, credential.token);
|
|
2897
|
-
process.stdout.write(`[halfcycle] Your board (first visit): ${minted.boardUrl}
|
|
3089
|
+
process.stdout.write(`[halfcycle] If Halfcycle tools appear but every call returns 401, the helper could not read HALFCYCLE_TOKEN from ${engagementEnvPath(engagementId)} \u2014 re-run this installer.
|
|
2898
3090
|
`);
|
|
2899
|
-
|
|
3091
|
+
reportClaudeCodeVersion();
|
|
3092
|
+
try {
|
|
3093
|
+
const minted = await mintBoardEnterCode(credential.serviceUrl, credential.token);
|
|
3094
|
+
process.stdout.write(`[halfcycle] Your board (first visit): ${minted.boardUrl}
|
|
2900
3095
|
`);
|
|
2901
|
-
}
|
|
2902
|
-
|
|
2903
|
-
|
|
3096
|
+
process.stdout.write(`[halfcycle] First-visit code \u2014 single use, expires ${minted.expiresAt}; paste it on that page, and re-run this installer for a fresh one: ${minted.enterCode}
|
|
3097
|
+
`);
|
|
3098
|
+
} catch {
|
|
3099
|
+
process.stdout.write("[halfcycle] Your board: no first-visit code was issued just now \u2014 re-run this installer to get one.\n");
|
|
2904
3100
|
}
|
|
2905
3101
|
process.exit(0);
|
|
2906
3102
|
} catch (err) {
|