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/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";
|
|
@@ -279,103 +446,6 @@ function mergePermissions(existing, generated) {
|
|
|
279
446
|
// dist/scan.js
|
|
280
447
|
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
|
|
281
448
|
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
|
-
}).strict();
|
|
310
|
-
var resultEnvelopeSchema = z2.object({
|
|
311
|
-
guardsFired: z2.array(firedGuardSchema),
|
|
312
|
-
severity: severitySchema.nullable(),
|
|
313
|
-
blocking: z2.boolean(),
|
|
314
|
-
explanation: z2.string()
|
|
315
|
-
}).strict();
|
|
316
|
-
var wireErrorSchema = z2.object({
|
|
317
|
-
statusCode: z2.number(),
|
|
318
|
-
error: z2.string(),
|
|
319
|
-
message: z2.string()
|
|
320
|
-
}).strict();
|
|
321
|
-
|
|
322
|
-
// ../events/dist/telemetry.js
|
|
323
|
-
import { z as z3 } from "zod";
|
|
324
|
-
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)");
|
|
325
|
-
var guardEvalOutcomeSchema = z3.enum([
|
|
326
|
-
"evaluated",
|
|
327
|
-
"infra-error",
|
|
328
|
-
"contract-error",
|
|
329
|
-
"unconfigured"
|
|
330
|
-
]);
|
|
331
|
-
var guardEvalRunSchema = z3.object({
|
|
332
|
-
// `runId`/`outcome` are the E2 run-observability additions, declared
|
|
333
|
-
// field-level-OPTIONAL as the FIRST (expand) step of an expand-contract
|
|
334
|
-
// migration. Every new record populated by the migrated writer (T-07)
|
|
335
|
-
// carries both; a legacy line predating the amendment carries neither and
|
|
336
|
-
// must still parse (the mixed-era guard-eval log the Build-Record reader
|
|
337
|
-
// assembles from, and the runner emit not yet migrated). So the presence
|
|
338
|
-
// refinement below is skipped for a legacy line (no `outcome`), and the
|
|
339
|
-
// per-writer guarantee lives with the writer + reader-normalisation (T-07),
|
|
340
|
-
// where the schema may then be tightened to required. runId is uuid-checked
|
|
341
|
-
// WHEN present; outcome is enum-checked when present.
|
|
342
|
-
runId: z3.string().uuid().optional(),
|
|
343
|
-
engagementId: z3.string().optional(),
|
|
344
|
-
runType: z3.enum(["hook", "ci"]),
|
|
345
|
-
occurredAt: utcIso8601,
|
|
346
|
-
outcome: guardEvalOutcomeSchema.optional(),
|
|
347
|
-
failureReason: z3.string().optional(),
|
|
348
|
-
phase: z3.string().optional(),
|
|
349
|
-
scope: z3.record(z3.string(), z3.unknown()),
|
|
350
|
-
cost: z3.record(z3.string(), z3.unknown()).optional(),
|
|
351
|
-
result: resultEnvelopeSchema.optional()
|
|
352
|
-
}).strict().superRefine((run, ctx) => {
|
|
353
|
-
if (run.outcome === void 0)
|
|
354
|
-
return;
|
|
355
|
-
if (run.outcome === "evaluated" && run.result === void 0) {
|
|
356
|
-
ctx.addIssue({
|
|
357
|
-
code: z3.ZodIssueCode.custom,
|
|
358
|
-
path: ["result"],
|
|
359
|
-
message: "result is required when outcome is 'evaluated'"
|
|
360
|
-
});
|
|
361
|
-
}
|
|
362
|
-
if (run.outcome !== "evaluated" && run.result !== void 0) {
|
|
363
|
-
ctx.addIssue({
|
|
364
|
-
code: z3.ZodIssueCode.custom,
|
|
365
|
-
path: ["result"],
|
|
366
|
-
message: "result is only present when outcome is 'evaluated'"
|
|
367
|
-
});
|
|
368
|
-
}
|
|
369
|
-
if (run.outcome !== "unconfigured" && run.engagementId === void 0) {
|
|
370
|
-
ctx.addIssue({
|
|
371
|
-
code: z3.ZodIssueCode.custom,
|
|
372
|
-
path: ["engagementId"],
|
|
373
|
-
message: "engagementId is required unless outcome is 'unconfigured'"
|
|
374
|
-
});
|
|
375
|
-
}
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
// dist/scan.js
|
|
379
449
|
var HALFCYCLE_STATE_FORMAT = "halfcycle-state-file/v1";
|
|
380
450
|
var HALFCYCLE_STATE_NOTE = [
|
|
381
451
|
"This file is a local cache written ONCE when Halfcycle was installed. It is not a",
|
|
@@ -526,6 +596,18 @@ function writeCollisionSafe(targetAbsPath, targetRepoRoot, content) {
|
|
|
526
596
|
writeFileSync3(targetAbsPath, content, "utf-8");
|
|
527
597
|
return "written";
|
|
528
598
|
}
|
|
599
|
+
function writeOwned(targetAbsPath, targetRepoRoot, content) {
|
|
600
|
+
const rel = relative(targetRepoRoot, targetAbsPath).replace(/\\/g, "/");
|
|
601
|
+
if (!isAllowlisted(rel)) {
|
|
602
|
+
throw new Error(`[bundle install] Write-allowlist violation: attempted to write "${rel}". Only the method surface and scaffolding paths are writable.`);
|
|
603
|
+
}
|
|
604
|
+
if (existsSync3(targetAbsPath) && readFileSync4(targetAbsPath, "utf-8") === content) {
|
|
605
|
+
return "skipped";
|
|
606
|
+
}
|
|
607
|
+
mkdirSync3(dirname2(targetAbsPath), { recursive: true });
|
|
608
|
+
writeFileSync3(targetAbsPath, content, "utf-8");
|
|
609
|
+
return "written";
|
|
610
|
+
}
|
|
529
611
|
function record(report, outcome, rel) {
|
|
530
612
|
const bucket = {
|
|
531
613
|
written: report.writtenPaths,
|
|
@@ -646,6 +728,42 @@ if [ -z "$REPO_ROOT" ]; then exit 0; fi
|
|
|
646
728
|
HASH="$(echo -n "$REPO_ROOT" | shasum -a 256 | cut -c1-12)"
|
|
647
729
|
MARKER_FILE="\${TMPDIR%/}/halfcycle-session-\${HASH}.ref"
|
|
648
730
|
git -C "$REPO_ROOT" rev-parse HEAD > "$MARKER_FILE" 2>/dev/null || true
|
|
731
|
+
|
|
732
|
+
# ---------------------------------------------------------------------------
|
|
733
|
+
# STANDING GUARD-COVERAGE STATEMENT (T-27).
|
|
734
|
+
#
|
|
735
|
+
# A repository with no credential on this machine is UNGUARDED: the PostToolUse
|
|
736
|
+
# hook will find nothing to evaluate with, and it says so once per edit, after
|
|
737
|
+
# the edit. This says it once, up front, before anything is written \u2014 which is
|
|
738
|
+
# the one thing a session-start hook can do that nothing else can.
|
|
739
|
+
#
|
|
740
|
+
# Everything below runs in a SUBSHELL: it sources a credential store, and a
|
|
741
|
+
# session-start hook must not leak an engagement's variables into whatever the
|
|
742
|
+
# editor runs next. Any failure inside it is swallowed; this script's exit
|
|
743
|
+
# status is 0 either way, and the marker above has already been written.
|
|
744
|
+
# ---------------------------------------------------------------------------
|
|
745
|
+
${engagementResolutionShell()}
|
|
746
|
+
|
|
747
|
+
(
|
|
748
|
+
if halfcycle_env_file "$REPO_ROOT"; then
|
|
749
|
+
set -a
|
|
750
|
+
. "$HALFCYCLE_ENV_FILE"
|
|
751
|
+
set +a
|
|
752
|
+
fi
|
|
753
|
+
hc_missing=""
|
|
754
|
+
for hc_key in ${GUARD_ENV_KEYS.join(" ")}; do
|
|
755
|
+
eval "hc_value=\\\${$hc_key:-}"
|
|
756
|
+
if [ -z "$hc_value" ]; then hc_missing="$hc_missing $hc_key"; fi
|
|
757
|
+
done
|
|
758
|
+
if [ -n "$hc_missing" ]; then
|
|
759
|
+
echo "[halfcycle] NO GUARD COVERAGE IN THIS REPOSITORY. Nothing will evaluate the edits made in"
|
|
760
|
+
echo "[halfcycle] this session: this machine has no Halfcycle credential for it, so the guard hook"
|
|
761
|
+
echo "[halfcycle] has nothing to call with (missing:$hc_missing)."
|
|
762
|
+
echo "[halfcycle] Run \\"npx halfcycle\\" in this repository to fix it \u2014 it signs you in through your"
|
|
763
|
+
echo "[halfcycle] browser if needed, writes the credential to \\$HOME/.halfcycle outside this tree,"
|
|
764
|
+
echo "[halfcycle] and needs nothing configured first."
|
|
765
|
+
fi
|
|
766
|
+
) 2>/dev/null || true
|
|
649
767
|
exit 0
|
|
650
768
|
`;
|
|
651
769
|
}
|
|
@@ -659,6 +777,13 @@ function generateUserPromptReminderHook() {
|
|
|
659
777
|
# one home: RULE_SENTENCE_PLAIN in the method repo's own position-house-rule
|
|
660
778
|
# test governs it \u2014 reword there and here together, in the same commit.
|
|
661
779
|
#
|
|
780
|
+
# SILENT WHEN THE TOOL IT NAMES IS NOT THERE. The sentence tells the session to
|
|
781
|
+
# call halfcycle_resolve, which arrives over the Halfcycle MCP server registered
|
|
782
|
+
# in .mcp.json. If this repository carries no such registration, the instruction
|
|
783
|
+
# cannot be followed, and a per-prompt instruction that cannot be followed is
|
|
784
|
+
# worse than silence. The standing "no coverage" statement belongs in the
|
|
785
|
+
# session-start banner, which says it once; this hook just stops talking.
|
|
786
|
+
#
|
|
662
787
|
# FAILS OPEN. This hook's stdout is always plain text, never JSON, so it
|
|
663
788
|
# cannot form a {"decision":"block",...} body (UserPromptSubmit's only way to
|
|
664
789
|
# hold the turn). But a write failure here (a closed stdout, say) must still
|
|
@@ -666,6 +791,26 @@ function generateUserPromptReminderHook() {
|
|
|
666
791
|
# than propagate.
|
|
667
792
|
trap 'exit 0' ERR
|
|
668
793
|
set -e
|
|
794
|
+
|
|
795
|
+
# The project root is resolved from THIS SCRIPT'S OWN LOCATION, never from the
|
|
796
|
+
# session's cwd \u2014 a session started in a subdirectory must find the same root
|
|
797
|
+
# .mcp.json the editor loaded, which is the bug mcp-headers.sh already paid for.
|
|
798
|
+
HC_SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
|
799
|
+
HC_PROJECT_ROOT=$(CDPATH= cd -- "$HC_SCRIPT_DIR/../.." && pwd)
|
|
800
|
+
HC_REGISTRATION="$HC_PROJECT_ROOT/${MCP_REGISTRATION_REL}"
|
|
801
|
+
|
|
802
|
+
# Presence of the FILE is not enough \u2014 a developer's own .mcp.json with no
|
|
803
|
+
# Halfcycle server in it registers no halfcycle_resolve. \`grep\` is guarded with
|
|
804
|
+
# \`|| true\` because a non-match is a status, not an error, and \`set -e\` would
|
|
805
|
+
# otherwise route it through the trap (same outcome here, but by accident).
|
|
806
|
+
HC_REGISTERED=""
|
|
807
|
+
if [ -f "$HC_REGISTRATION" ]; then
|
|
808
|
+
HC_REGISTERED=$(tr -d '\\n' < "$HC_REGISTRATION" | grep -c '"${MCP_SERVER_KEY}"[[:space:]]*:' || true)
|
|
809
|
+
fi
|
|
810
|
+
if [ -z "$HC_REGISTERED" ] || [ "$HC_REGISTERED" = "0" ]; then
|
|
811
|
+
exit 0
|
|
812
|
+
fi
|
|
813
|
+
|
|
669
814
|
printf '%s\\n' '${USER_PROMPT_REMINDER_SENTENCE}'
|
|
670
815
|
exit 0
|
|
671
816
|
`;
|
|
@@ -714,6 +859,13 @@ var ENV_EXAMPLE_NAMES = [
|
|
|
714
859
|
"GUARD_SERVICE_URL",
|
|
715
860
|
"GUARD_SERVICE_TOKEN",
|
|
716
861
|
"GUARD_ENGAGEMENT_ID",
|
|
862
|
+
// Declared for ONE reason only: it is written into the credential store, and the
|
|
863
|
+
// INV-004 check below requires every written name to be named here. It is NOT
|
|
864
|
+
// read from a client's environment — the generated CI stanza sets the three
|
|
865
|
+
// GUARD_* names and not this one, and `packages/runner` never reads it at all.
|
|
866
|
+
// It is how an install REMEMBERS the plane it was created against, which matters
|
|
867
|
+
// only to our own development and CI planes (operator amendment, 2026-08-30);
|
|
868
|
+
// nothing tells a client to set it, and the generated prose says so.
|
|
717
869
|
"HALFCYCLE_SERVICE_URL",
|
|
718
870
|
"HALFCYCLE_MCP_URL",
|
|
719
871
|
"HALFCYCLE_TOKEN",
|
|
@@ -747,8 +899,14 @@ GUARD_ENGAGEMENT_ID=<argType:runtime>
|
|
|
747
899
|
# TWO ORIGINS, TWO SERVICES (T-22). HALFCYCLE_SERVICE_URL is the Halfcycle
|
|
748
900
|
# CONTROL plane \u2014 it serves engagement creation and the board's first-visit
|
|
749
901
|
# code. HALFCYCLE_MCP_URL is the method-delivery service, which serves /mcp and
|
|
750
|
-
# nothing else here.
|
|
751
|
-
#
|
|
902
|
+
# nothing else here. The platform reports the second on create, so it is written
|
|
903
|
+
# for you and .mcp.json carries the composed address.
|
|
904
|
+
#
|
|
905
|
+
# NEITHER IS A SETTING. Both names are here because both are WRITTEN by the
|
|
906
|
+
# installer into this engagement's credential, and every name it writes is declared
|
|
907
|
+
# here \u2014 that is the rule this section exists for. Halfcycle's address ships in the
|
|
908
|
+
# CLI and is the same one for everybody, so there is nothing to look up and nothing
|
|
909
|
+
# to put in this file. Leave both lines exactly as they are.
|
|
752
910
|
HALFCYCLE_SERVICE_URL=<argType:runtime>
|
|
753
911
|
HALFCYCLE_MCP_URL=<argType:runtime>
|
|
754
912
|
HALFCYCLE_TOKEN=<argType:runtime>
|
|
@@ -761,6 +919,7 @@ CONTROL_TELEMETRY_URL=<argType:runtime>
|
|
|
761
919
|
`;
|
|
762
920
|
}
|
|
763
921
|
var MCP_REGISTRATION_REL = ".mcp.json";
|
|
922
|
+
var MCP_SERVER_KEY = "halfcycle";
|
|
764
923
|
var MCP_HEADERS_HELPER_REL = ".halfcycle/mcp-headers.sh";
|
|
765
924
|
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}"'`;
|
|
766
925
|
function generateMcpHeadersHelper() {
|
|
@@ -873,7 +1032,7 @@ function generateMcpRegistration(existing, mcpOrigin) {
|
|
|
873
1032
|
if (typeof base.mcpServers !== "object" || base.mcpServers === null) {
|
|
874
1033
|
base.mcpServers = {};
|
|
875
1034
|
}
|
|
876
|
-
base.mcpServers[
|
|
1035
|
+
base.mcpServers[MCP_SERVER_KEY] = {
|
|
877
1036
|
type: "http",
|
|
878
1037
|
url: mcpEndpointUrl(mcpOrigin),
|
|
879
1038
|
headersHelper: MCP_HEADERS_HELPER_COMMAND
|
|
@@ -933,6 +1092,16 @@ function writeBundlePin(targetRepoRoot, version, engagementId, engagementType, w
|
|
|
933
1092
|
const pinPath = join5(targetRepoRoot, ".halfcycle", "bundle.json");
|
|
934
1093
|
writeAllowlisted(pinPath, targetRepoRoot, JSON.stringify(pin, null, 2) + "\n", writtenPaths);
|
|
935
1094
|
}
|
|
1095
|
+
function writeCrewRoster(targetRepoRoot, report) {
|
|
1096
|
+
const doc = {
|
|
1097
|
+
$comment: "GENERATED by the Halfcycle installer \u2014 do not edit by hand. Re-run the installer to pick up a roster change.",
|
|
1098
|
+
crew: CREW_ROSTER
|
|
1099
|
+
};
|
|
1100
|
+
const rendered = `${JSON.stringify(doc, null, 2)}
|
|
1101
|
+
`;
|
|
1102
|
+
const crewPath = join5(targetRepoRoot, ".halfcycle", "crew.json");
|
|
1103
|
+
record(report, writeOwned(crewPath, targetRepoRoot, rendered), ".halfcycle/crew.json");
|
|
1104
|
+
}
|
|
936
1105
|
function readBundlePin(targetRepoRoot) {
|
|
937
1106
|
const pinPath = join5(targetRepoRoot, ".halfcycle", "bundle.json");
|
|
938
1107
|
if (!existsSync3(pinPath))
|
|
@@ -1058,7 +1227,7 @@ async function install(options) {
|
|
|
1058
1227
|
}
|
|
1059
1228
|
const vendoredBinSrc = resolveVendoredBinary();
|
|
1060
1229
|
const vendoredBinDest = join5(targetRepo, ".halfcycle", "bin", "bin.bundle.mjs");
|
|
1061
|
-
record(report,
|
|
1230
|
+
record(report, writeOwned(vendoredBinDest, targetRepo, readFileSync4(vendoredBinSrc, "utf-8")), ".halfcycle/bin/bin.bundle.mjs");
|
|
1062
1231
|
const settingsPath = join5(targetRepo, ".claude", "settings.json");
|
|
1063
1232
|
const settingsPreexisted = existsSync3(settingsPath);
|
|
1064
1233
|
const generatedSettings = JSON.parse(generateSettingsJson());
|
|
@@ -1127,6 +1296,7 @@ async function install(options) {
|
|
|
1127
1296
|
break;
|
|
1128
1297
|
}
|
|
1129
1298
|
writeBundlePin(targetRepo, manifest.version, engagementId, engagementType, report.writtenPaths);
|
|
1299
|
+
writeCrewRoster(targetRepo, report);
|
|
1130
1300
|
const scanResult = runBootstrapScan(targetRepo);
|
|
1131
1301
|
return {
|
|
1132
1302
|
version: manifest.version,
|
|
@@ -1139,6 +1309,9 @@ async function install(options) {
|
|
|
1139
1309
|
};
|
|
1140
1310
|
}
|
|
1141
1311
|
|
|
1312
|
+
// dist/control-origin.js
|
|
1313
|
+
var DEFAULT_CONTROL_ORIGIN = "https://control.halfcycle.ai";
|
|
1314
|
+
|
|
1142
1315
|
// dist/create-engagement.js
|
|
1143
1316
|
var CreateEngagementRefused = class extends Error {
|
|
1144
1317
|
status;
|
|
@@ -1196,7 +1369,7 @@ function planeRefusal(detail) {
|
|
|
1196
1369
|
error: typeof candidate.error === "string" ? candidate.error : void 0
|
|
1197
1370
|
};
|
|
1198
1371
|
}
|
|
1199
|
-
var CONTROL_ORIGIN_HINT =
|
|
1372
|
+
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.`;
|
|
1200
1373
|
async function createEngagement(baseUrl, name, credential) {
|
|
1201
1374
|
return requestEngagementValues(`${baseUrl.replace(/\/+$/, "")}/engagements`, name ? { name } : {}, credential, { action: "Create-engagement", nothingHappened: "No engagement was created", route: "POST /engagements" });
|
|
1202
1375
|
}
|