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/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 ${GUARD_ENV_KEYS.join(" ")}; do
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
- # GUARD_SERVICE_URL: \${{ secrets.GUARD_SERVICE_URL }}
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
- "GUARD_SERVICE_URL",
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
- "HALFCYCLE_SERVICE_URL",
817
- "HALFCYCLE_MCP_URL",
818
- "HALFCYCLE_TOKEN",
819
- // T-10 — declared because it is now WRITTEN into the credential store, and
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 runner (generated by the Halfcycle installer \u2014 names only, no values)
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 names are declared because they ARE read from the environment in one
839
- # place: a CI job, which has no browser and no per-user store and supplies them as
840
- # workflow secrets (see .halfcycle/ci-stanza.yml). HALFCYCLE_TOKEN set in the
841
- # environment also takes precedence over the stored credential everywhere, which is
842
- # the escape hatch for running as a specific identity.
843
- GUARD_SERVICE_URL=<argType:runtime>
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 = `With nothing configured this CLI talks to ${DEFAULT_CONTROL_ORIGIN}; HALFCYCLE_SERVICE_URL overrides that, and is only for a self-hosted or development plane. Whichever is in use, 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 do not configure it.`;
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
  }