halfcycle 0.3.37 → 0.3.39
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 +1 -1
- package/README.md +2 -0
- package/bin/bin.bundle.mjs +6 -7
- package/dist/bin.js +220 -83
- package/dist/bin.js.map +2 -2
- package/dist/build-record/obtain.d.ts +7 -3
- package/dist/build-record/obtain.d.ts.map +1 -1
- package/dist/build-record/render-v2.d.ts +4 -3
- package/dist/build-record/render-v2.d.ts.map +1 -1
- package/dist/cli-contract.d.ts +7 -0
- package/dist/cli-contract.d.ts.map +1 -1
- package/dist/control-origin.d.ts +2 -2
- package/dist/create-engagement.d.ts +27 -1
- package/dist/create-engagement.d.ts.map +1 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +171 -59
- package/dist/index.js.map +2 -2
- package/dist/install.d.ts +33 -1
- package/dist/install.d.ts.map +1 -1
- package/dist/open-phase.d.ts +12 -3
- package/dist/open-phase.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/mint-board-code.d.ts +0 -41
- package/dist/mint-board-code.d.ts.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "halfcycle",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.39",
|
|
4
4
|
"description": "Halfcycle Method bundle — resolution-stub slash commands, remote method-delivery registration, and governance-hook wiring for a Halfcycle engagement repo. It ships NO worker role files: a project's roles are authored from that project's own recorded decisions at orchestration kickoff (FX-2, W3-F-27).",
|
|
5
5
|
"commands": [
|
|
6
6
|
{
|
package/README.md
CHANGED
|
@@ -146,6 +146,8 @@ This matters enough to be specific about.
|
|
|
146
146
|
|
|
147
147
|
**Halfcycle never holds or proxies your Claude credential.** Inference runs on your own Claude Code, on your own subscription or seats. We are not a hosted runtime reselling you inference.
|
|
148
148
|
|
|
149
|
+
**Your own Halfcycle credential lives outside this repository, in a file named `env` under your home directory — never in anything you'd commit.** Closing a phase rewrites it, to clear which phase was open, and it is also renewed there automatically to keep your connection current. Nothing else changes it.
|
|
150
|
+
|
|
149
151
|
**What the checks receive is the shape of a change, not its contents.** File paths, structural shapes, environment variable names and how they are used, the tables a write touches, route signatures, outbound host names. There is no file body and no diff in the request.
|
|
150
152
|
|
|
151
153
|
**Source excerpts are off, at both ends, for every engagement today.** The wire format does have a field for them, so the honest version of this is a conditional statement rather than a flat one, and it rests on three things rather than on our good intentions: your runner does not attach an excerpt unless you turn a flag on, and that flag is off by default; anything that does arrive for an engagement that has not opted in is stripped on our side before the checks or any model see it; and there is no way to opt in today, because nothing in the product can set that flag. If that ever changes, our privacy policy changes with it, and it is the policy — not this paragraph — that states the guarantee.
|
package/bin/bin.bundle.mjs
CHANGED
|
@@ -15289,7 +15289,9 @@ var engagementCredentialResponseSchema = external_exports.object({
|
|
|
15289
15289
|
sessionToken: external_exports.string(),
|
|
15290
15290
|
mcpUrl: external_exports.string(),
|
|
15291
15291
|
guardUrl: external_exports.string(),
|
|
15292
|
-
controlTelemetryUrl: external_exports.string()
|
|
15292
|
+
controlTelemetryUrl: external_exports.string(),
|
|
15293
|
+
methodVersion: external_exports.string().optional(),
|
|
15294
|
+
consoleUrl: external_exports.string().optional()
|
|
15293
15295
|
}).strict();
|
|
15294
15296
|
var ciTokenExchangeResponseSchema = external_exports.object({
|
|
15295
15297
|
token: external_exports.string(),
|
|
@@ -15357,7 +15359,7 @@ function hookReply(event) {
|
|
|
15357
15359
|
};
|
|
15358
15360
|
}
|
|
15359
15361
|
function infraWarningText(message) {
|
|
15360
|
-
return `[Halfcycle] WARNING: Guard service unavailable \u2014 ${message}.
|
|
15362
|
+
return `[Halfcycle] WARNING: Guard service unavailable \u2014 ${message}. The session continues without checks.`;
|
|
15361
15363
|
}
|
|
15362
15364
|
function localFaultText(message) {
|
|
15363
15365
|
return `[Halfcycle] WARNING: Local fault \u2014 ${message}. The guard service was not contacted. Session continues (fail-open).`;
|
|
@@ -15434,11 +15436,8 @@ function classifyFailure(failure) {
|
|
|
15434
15436
|
return {
|
|
15435
15437
|
kind: "service",
|
|
15436
15438
|
message: failure.message,
|
|
15437
|
-
|
|
15438
|
-
|
|
15439
|
-
// ONE condition out of this branch; it did not redefine the branch.
|
|
15440
|
-
label: "infrastructure failure",
|
|
15441
|
-
reason: `infrastructure failure: ${failure.message}`
|
|
15439
|
+
label: "service unavailable",
|
|
15440
|
+
reason: failure.message
|
|
15442
15441
|
};
|
|
15443
15442
|
}
|
|
15444
15443
|
var CREDENTIAL_REJECTED_EXIT_TOOL_USE = UNCONFIGURED_EXIT_TOOL_USE;
|
package/dist/bin.js
CHANGED
|
@@ -234,6 +234,11 @@ function looksLikeSecret(text) {
|
|
|
234
234
|
var MCP_REGISTRATION_REL = ".mcp.json";
|
|
235
235
|
var MCP_SERVER_KEY = "halfcycle";
|
|
236
236
|
|
|
237
|
+
// ../core/dist/shell-quote.js
|
|
238
|
+
function quoteForShell(value) {
|
|
239
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
240
|
+
}
|
|
241
|
+
|
|
237
242
|
// ../events/dist/result.js
|
|
238
243
|
import { z as z2 } from "zod";
|
|
239
244
|
var firedGuardSchema = z2.object({
|
|
@@ -606,9 +611,14 @@ var closedBySchema = z4.discriminatedUnion("kind", [
|
|
|
606
611
|
z4.object({ kind: z4.literal("account-removed") }).strict(),
|
|
607
612
|
z4.object({ kind: z4.literal("not-recorded") }).strict()
|
|
608
613
|
]);
|
|
614
|
+
var STEP_NAME_MAX = 200;
|
|
615
|
+
var stepNameSchema = plainText(STEP_NAME_MAX).refine((v) => v.trim().length > 0, {
|
|
616
|
+
message: "must not be blank"
|
|
617
|
+
});
|
|
609
618
|
var unfinishedStepSchema = z4.object({
|
|
610
619
|
stepId: plainText(),
|
|
611
|
-
state: z4.enum(["parked", "failed"])
|
|
620
|
+
state: z4.enum(["parked", "failed"]),
|
|
621
|
+
name: stepNameSchema.optional()
|
|
612
622
|
}).strict();
|
|
613
623
|
var crossedSchema = z4.discriminatedUnion("kind", [
|
|
614
624
|
z4.object({ kind: z4.literal("none") }).strict(),
|
|
@@ -1217,7 +1227,9 @@ var engagementCredentialResponseSchema = z10.object({
|
|
|
1217
1227
|
sessionToken: z10.string(),
|
|
1218
1228
|
mcpUrl: z10.string(),
|
|
1219
1229
|
guardUrl: z10.string(),
|
|
1220
|
-
controlTelemetryUrl: z10.string()
|
|
1230
|
+
controlTelemetryUrl: z10.string(),
|
|
1231
|
+
methodVersion: z10.string().optional(),
|
|
1232
|
+
consoleUrl: z10.string().optional()
|
|
1221
1233
|
}).strict();
|
|
1222
1234
|
var ciTokenExchangeResponseSchema = z10.object({
|
|
1223
1235
|
token: z10.string(),
|
|
@@ -2708,9 +2720,10 @@ function previousPinForEngagement(targetRepoRoot, engagementId) {
|
|
|
2708
2720
|
function nonEmpty(value) {
|
|
2709
2721
|
return typeof value === "string" && value.trim() !== "" ? value : void 0;
|
|
2710
2722
|
}
|
|
2711
|
-
function writeBundlePin(targetRepoRoot, version, engagementId, engagementType, accountId, writtenPaths) {
|
|
2723
|
+
function writeBundlePin(targetRepoRoot, version, engagementId, engagementType, accountId, methodVersion, writtenPaths) {
|
|
2712
2724
|
const previous = previousPinForEngagement(targetRepoRoot, engagementId);
|
|
2713
2725
|
const carried = nonEmpty(accountId) ?? nonEmpty(previous?.accountId);
|
|
2726
|
+
const carriedMethodVersion = nonEmpty(methodVersion) ?? nonEmpty(previous?.methodVersion);
|
|
2714
2727
|
const installedAt = nonEmpty(previous?.installedAt) ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
2715
2728
|
const projectPage = projectPageUrl(engagementId);
|
|
2716
2729
|
const pin = {
|
|
@@ -2719,6 +2732,7 @@ function writeBundlePin(targetRepoRoot, version, engagementId, engagementType, a
|
|
|
2719
2732
|
engagementType,
|
|
2720
2733
|
installedAt,
|
|
2721
2734
|
...carried !== void 0 ? { accountId: carried } : {},
|
|
2735
|
+
...carriedMethodVersion !== void 0 ? { methodVersion: carriedMethodVersion } : {},
|
|
2722
2736
|
...projectPage !== void 0 ? { projectPage } : {}
|
|
2723
2737
|
};
|
|
2724
2738
|
const pinPath = join5(targetRepoRoot, BUNDLE_PIN_REL);
|
|
@@ -2794,7 +2808,7 @@ function checkDrift(targetRepoRoot) {
|
|
|
2794
2808
|
const pin = readBundlePin(targetRepoRoot);
|
|
2795
2809
|
const installed = pin?.version ?? null;
|
|
2796
2810
|
const current = manifest.version;
|
|
2797
|
-
return { drifted: installed !== current, installed, current };
|
|
2811
|
+
return { drifted: installed !== current, installed, current, methodVersion: pin?.methodVersion ?? null };
|
|
2798
2812
|
}
|
|
2799
2813
|
function writeProjectIdentity(targetRepo) {
|
|
2800
2814
|
const path = join5(targetRepo, PROJECT_IDENTITY_REL);
|
|
@@ -3025,7 +3039,7 @@ async function install(options) {
|
|
|
3025
3039
|
case "absent":
|
|
3026
3040
|
break;
|
|
3027
3041
|
}
|
|
3028
|
-
writeBundlePin(targetRepo, manifest.version, engagementId, engagementType, accountId, report.writtenPaths);
|
|
3042
|
+
writeBundlePin(targetRepo, manifest.version, engagementId, engagementType, accountId, credential?.methodVersion, report.writtenPaths);
|
|
3029
3043
|
noteFile(draft, targetRepo, BUNDLE_PIN_REL, "written");
|
|
3030
3044
|
noteFile(draft, targetRepo, CREW_ROSTER_REL, writeCrewRoster(targetRepo, report));
|
|
3031
3045
|
const scanResult = runBootstrapScan(targetRepo);
|
|
@@ -3599,7 +3613,7 @@ function planeRefusal(detail) {
|
|
|
3599
3613
|
reason: typeof candidate.reason === "string" ? candidate.reason : void 0
|
|
3600
3614
|
};
|
|
3601
3615
|
}
|
|
3602
|
-
var CONTROL_ORIGIN_HINT = `This CLI talks to ${DEFAULT_CONTROL_ORIGIN}; it must be the CONTROL origin \u2014 the one that serves /engagements
|
|
3616
|
+
var CONTROL_ORIGIN_HINT = `This CLI talks to ${DEFAULT_CONTROL_ORIGIN}; it must be the CONTROL origin \u2014 the one that serves /engagements. The MCP server runs at a different address, and the platform supplies that one itself; you configure neither.`;
|
|
3603
3617
|
async function createEngagement(baseUrl, name, credential, derivedName) {
|
|
3604
3618
|
return requestEngagementValues(
|
|
3605
3619
|
`${baseUrl.replace(/\/+$/, "")}/engagements`,
|
|
@@ -3679,12 +3693,16 @@ async function requestEngagementValues(url, requestBody, credential, shape) {
|
|
|
3679
3693
|
}).join(", ");
|
|
3680
3694
|
throw new Error(`[bundle install] ${shape.action} response from ${url} is not the credential response this CLI is built to read (${fields}). ${shape.nothingHappened}. Either that service is a different version from this installer, or ${url} is not a Halfcycle service.`);
|
|
3681
3695
|
}
|
|
3696
|
+
const consoleUrl = typeof body.consoleUrl === "string" && body.consoleUrl.trim() !== "" ? body.consoleUrl.trim() : void 0;
|
|
3697
|
+
const methodVersion = typeof body.methodVersion === "string" && body.methodVersion.trim() !== "" ? body.methodVersion.trim() : void 0;
|
|
3682
3698
|
return {
|
|
3683
3699
|
engagementId: body.engagementId,
|
|
3684
3700
|
sessionToken: body.sessionToken,
|
|
3685
3701
|
mcpUrl: body.mcpUrl.trim().replace(/\/+$/, ""),
|
|
3686
3702
|
guardUrl: body.guardUrl.trim().replace(/\/+$/, ""),
|
|
3687
|
-
...controlTelemetryUrl !== void 0 ? { controlTelemetryUrl } : {}
|
|
3703
|
+
...controlTelemetryUrl !== void 0 ? { controlTelemetryUrl } : {},
|
|
3704
|
+
...consoleUrl !== void 0 ? { consoleUrl } : {},
|
|
3705
|
+
...methodVersion !== void 0 ? { methodVersion } : {}
|
|
3688
3706
|
};
|
|
3689
3707
|
}
|
|
3690
3708
|
|
|
@@ -4295,37 +4313,6 @@ async function joinPinnedEngagement(serviceUrl, engagementId, targetRepo, deps =
|
|
|
4295
4313
|
}
|
|
4296
4314
|
}
|
|
4297
4315
|
|
|
4298
|
-
// dist/mint-board-code.js
|
|
4299
|
-
async function mintBoardEnterCode(baseUrl, sessionToken) {
|
|
4300
|
-
const url = `${baseUrl.replace(/\/+$/, "")}/board/enter-codes`;
|
|
4301
|
-
let res;
|
|
4302
|
-
try {
|
|
4303
|
-
res = await fetch(url, {
|
|
4304
|
-
method: "POST",
|
|
4305
|
-
headers: {
|
|
4306
|
-
"content-type": "application/json",
|
|
4307
|
-
authorization: `Bearer ${sessionToken}`
|
|
4308
|
-
},
|
|
4309
|
-
body: "{}"
|
|
4310
|
-
});
|
|
4311
|
-
} catch (err) {
|
|
4312
|
-
throw new Error(`[bundle install] Could not reach the Halfcycle board mint endpoint at ${url}: ${err instanceof Error ? err.message : String(err)}.`);
|
|
4313
|
-
}
|
|
4314
|
-
if (!res.ok) {
|
|
4315
|
-
throw new Error(`[bundle install] Board code mint failed: ${url} returned ${res.status}.`);
|
|
4316
|
-
}
|
|
4317
|
-
const body = await res.json().catch(() => null);
|
|
4318
|
-
if (!body || typeof body.enterCode !== "string" || typeof body.engagementId !== "string" || typeof body.boardUrl !== "string" || typeof body.expiresAt !== "string") {
|
|
4319
|
-
throw new Error(`[bundle install] Board code mint response from ${url} did not carry {enterCode, engagementId, boardUrl, expiresAt}.`);
|
|
4320
|
-
}
|
|
4321
|
-
return {
|
|
4322
|
-
enterCode: body.enterCode,
|
|
4323
|
-
engagementId: body.engagementId,
|
|
4324
|
-
boardUrl: body.boardUrl,
|
|
4325
|
-
expiresAt: body.expiresAt
|
|
4326
|
-
};
|
|
4327
|
-
}
|
|
4328
|
-
|
|
4329
4316
|
// dist/probe-mcp.js
|
|
4330
4317
|
var PROBE_TIMEOUT_MS = 5e3;
|
|
4331
4318
|
async function probeMcpOrigin(mcpUrl, token) {
|
|
@@ -4407,20 +4394,127 @@ import { basename as basename2, join as join6, resolve as resolve2, relative as
|
|
|
4407
4394
|
|
|
4408
4395
|
// dist/build-record/render-v2.js
|
|
4409
4396
|
var HIDDEN = /[\p{Cc}\p{Zl}\p{Zp}\p{Default_Ignorable_Code_Point}]/u;
|
|
4410
|
-
var
|
|
4397
|
+
var SPACE = new RegExp("\\p{Zs}", "u");
|
|
4398
|
+
var WORD = /[\p{L}\p{N}]/u;
|
|
4399
|
+
var ASCII_PUNCTUATION = /[!-/:-@[-`{-~]/;
|
|
4400
|
+
var CHARACTER_REFERENCE = /^#?[A-Za-z0-9]+;/;
|
|
4401
|
+
var PAIRED = ["*", "_", "~", "$"];
|
|
4402
|
+
function codeSpanMask(cps) {
|
|
4403
|
+
const mask = cps.map(() => false);
|
|
4404
|
+
const runs = [];
|
|
4405
|
+
for (let i = 0; i < cps.length; ) {
|
|
4406
|
+
if (cps[i] !== "`") {
|
|
4407
|
+
i++;
|
|
4408
|
+
continue;
|
|
4409
|
+
}
|
|
4410
|
+
let j = i;
|
|
4411
|
+
while (j < cps.length && cps[j] === "`")
|
|
4412
|
+
j++;
|
|
4413
|
+
runs.push({ start: i, length: j - i });
|
|
4414
|
+
i = j;
|
|
4415
|
+
}
|
|
4416
|
+
for (let k = 0; k < runs.length; ) {
|
|
4417
|
+
const open = runs[k];
|
|
4418
|
+
let m = k + 1;
|
|
4419
|
+
while (m < runs.length && runs[m].length !== open.length)
|
|
4420
|
+
m++;
|
|
4421
|
+
if (m === runs.length) {
|
|
4422
|
+
k++;
|
|
4423
|
+
continue;
|
|
4424
|
+
}
|
|
4425
|
+
const close = runs[m];
|
|
4426
|
+
for (let i = open.start; i < close.start + close.length; i++)
|
|
4427
|
+
mask[i] = true;
|
|
4428
|
+
k = m + 1;
|
|
4429
|
+
}
|
|
4430
|
+
return mask;
|
|
4431
|
+
}
|
|
4432
|
+
function escapeMask(cps, inSpan) {
|
|
4433
|
+
const n = cps.length;
|
|
4434
|
+
const escape = cps.map(() => false);
|
|
4435
|
+
const isSpace = (i) => i >= 0 && i < n && SPACE.test(cps[i]) && !HIDDEN.test(cps[i]);
|
|
4436
|
+
const isWord = (i) => i >= 0 && i < n && WORD.test(cps[i]);
|
|
4437
|
+
const rest = (i) => cps.slice(i).join("");
|
|
4438
|
+
for (let i = 0; i < n; i++) {
|
|
4439
|
+
if (inSpan[i])
|
|
4440
|
+
continue;
|
|
4441
|
+
const ch = cps[i];
|
|
4442
|
+
const next = i + 1 < n ? cps[i + 1] : null;
|
|
4443
|
+
switch (ch) {
|
|
4444
|
+
case "\\":
|
|
4445
|
+
escape[i] = next === null || ASCII_PUNCTUATION.test(next);
|
|
4446
|
+
break;
|
|
4447
|
+
case "<":
|
|
4448
|
+
escape[i] = next !== null && !isSpace(i + 1) && rest(i + 1).includes(">");
|
|
4449
|
+
break;
|
|
4450
|
+
case "&":
|
|
4451
|
+
escape[i] = CHARACTER_REFERENCE.test(rest(i + 1));
|
|
4452
|
+
break;
|
|
4453
|
+
case "]":
|
|
4454
|
+
escape[i] = next === "(";
|
|
4455
|
+
break;
|
|
4456
|
+
case "[":
|
|
4457
|
+
case "{":
|
|
4458
|
+
escape[i] = (next === "+" || next === "-") && (rest(i + 2).includes(`${next}]`) || rest(i + 2).includes(`${next}}`));
|
|
4459
|
+
break;
|
|
4460
|
+
default:
|
|
4461
|
+
break;
|
|
4462
|
+
}
|
|
4463
|
+
}
|
|
4464
|
+
for (let i = 0; i < n; ) {
|
|
4465
|
+
if (inSpan[i] || cps[i] !== "#") {
|
|
4466
|
+
i++;
|
|
4467
|
+
continue;
|
|
4468
|
+
}
|
|
4469
|
+
let j = i;
|
|
4470
|
+
while (j < n && cps[j] === "#" && !inSpan[j])
|
|
4471
|
+
j++;
|
|
4472
|
+
let k = j;
|
|
4473
|
+
while (k < n && isSpace(k))
|
|
4474
|
+
k++;
|
|
4475
|
+
if (k === n && (i === 0 || isSpace(i - 1)))
|
|
4476
|
+
for (let m = i; m < j; m++)
|
|
4477
|
+
escape[m] = true;
|
|
4478
|
+
i = j;
|
|
4479
|
+
}
|
|
4480
|
+
for (const c of PAIRED) {
|
|
4481
|
+
const live = [];
|
|
4482
|
+
for (let i = 0; i < n; ) {
|
|
4483
|
+
if (inSpan[i] || cps[i] !== c) {
|
|
4484
|
+
i++;
|
|
4485
|
+
continue;
|
|
4486
|
+
}
|
|
4487
|
+
let j = i;
|
|
4488
|
+
while (j < n && cps[j] === c && !inSpan[j])
|
|
4489
|
+
j++;
|
|
4490
|
+
const inert = c !== "$" && (isSpace(i - 1) && isSpace(j) || c === "_" && isWord(i - 1) && isWord(j));
|
|
4491
|
+
if (!inert)
|
|
4492
|
+
live.push({ start: i, end: j });
|
|
4493
|
+
i = j;
|
|
4494
|
+
}
|
|
4495
|
+
if (live.length < 2)
|
|
4496
|
+
continue;
|
|
4497
|
+
for (const run of live)
|
|
4498
|
+
for (let m = run.start; m < run.end; m++)
|
|
4499
|
+
escape[m] = true;
|
|
4500
|
+
}
|
|
4501
|
+
return escape;
|
|
4502
|
+
}
|
|
4411
4503
|
function printRecordText(text) {
|
|
4412
4504
|
if (text === "")
|
|
4413
4505
|
return "(blank)";
|
|
4506
|
+
const cps = [...text];
|
|
4507
|
+
const escape = escapeMask(cps, codeSpanMask(cps));
|
|
4414
4508
|
let out = "";
|
|
4415
|
-
|
|
4509
|
+
cps.forEach((ch, i) => {
|
|
4416
4510
|
if (HIDDEN.test(ch)) {
|
|
4417
4511
|
out += `U+${ch.codePointAt(0).toString(16).toUpperCase().padStart(4, "0")}`;
|
|
4418
|
-
} else if (
|
|
4512
|
+
} else if (escape[i]) {
|
|
4419
4513
|
out += `\\${ch}`;
|
|
4420
4514
|
} else {
|
|
4421
4515
|
out += ch;
|
|
4422
4516
|
}
|
|
4423
|
-
}
|
|
4517
|
+
});
|
|
4424
4518
|
return out;
|
|
4425
4519
|
}
|
|
4426
4520
|
function makePrinter() {
|
|
@@ -4510,12 +4604,18 @@ function headline(observed, attested, checks) {
|
|
|
4510
4604
|
if (problemsReported) {
|
|
4511
4605
|
return "**Closed with problems reported.** Halfcycle saw nothing outstanding, and the person closing the phase reported problems found in its acceptance walk.";
|
|
4512
4606
|
}
|
|
4513
|
-
return checks === "not-observed" ? "**Closed with nothing outstanding, but no
|
|
4607
|
+
return checks === "not-observed" ? "**Closed with nothing outstanding, but no check on record.** Halfcycle saw nothing outstanding, and the person closing the phase stated its finish conditions were met, but Halfcycle has no record of any check running during this phase, so this is not a clean result." : "**Closed cleanly.** Halfcycle saw nothing outstanding, and the person closing the phase stated its finish conditions were met.";
|
|
4514
4608
|
case "unfinished-steps":
|
|
4515
4609
|
case "clean-over-findings":
|
|
4516
|
-
|
|
4610
|
+
if (!met) {
|
|
4611
|
+
return "**Closed over outstanding work.** Halfcycle saw work still outstanding, and the person closing the phase chose to override it.";
|
|
4612
|
+
}
|
|
4613
|
+
return problemsReported ? "**Closed over outstanding work.** Halfcycle saw work still outstanding, and the person closing the phase closed it as it stood, reporting problems found in its acceptance walk." : "**Closed over outstanding work.** Halfcycle saw work still outstanding, and the person closing the phase stated its finish conditions were met anyway.";
|
|
4517
4614
|
case "not-recorded":
|
|
4518
|
-
|
|
4615
|
+
if (!met) {
|
|
4616
|
+
return "**Outstanding work not recorded.** Halfcycle has no record of what was outstanding at this close. The person closing the phase chose to override.";
|
|
4617
|
+
}
|
|
4618
|
+
return problemsReported ? "**Outstanding work not recorded.** Halfcycle has no record of what was outstanding at this close. The person closing the phase reported problems found in its acceptance walk." : "**Outstanding work not recorded.** Halfcycle has no record of what was outstanding at this close. The person closing the phase stated its finish conditions were met.";
|
|
4519
4619
|
}
|
|
4520
4620
|
}
|
|
4521
4621
|
function closedAtText(closedAt, p) {
|
|
@@ -4534,30 +4634,38 @@ function signedInAs(closedBy, p) {
|
|
|
4534
4634
|
return "not recorded for this close";
|
|
4535
4635
|
}
|
|
4536
4636
|
}
|
|
4637
|
+
var OUTSTANDING_LABEL = "- Outstanding work in Halfcycle's own records of this phase (the method's steps, and problems recorded against the phase): ";
|
|
4537
4638
|
function outstanding(crossed, p) {
|
|
4538
4639
|
switch (crossed.kind) {
|
|
4539
4640
|
case "none":
|
|
4540
|
-
return [
|
|
4641
|
+
return [`${OUTSTANDING_LABEL}none.`];
|
|
4541
4642
|
case "not-recorded":
|
|
4542
|
-
return [
|
|
4643
|
+
return [`${OUTSTANDING_LABEL}not recorded for this close.`];
|
|
4543
4644
|
case "clean-over-findings":
|
|
4544
|
-
return [
|
|
4545
|
-
`- Outstanding work Halfcycle could see: ${counted(crossed.findings, "problem", "problems")} recorded against this phase.`
|
|
4546
|
-
];
|
|
4645
|
+
return [`${OUTSTANDING_LABEL}${counted(crossed.findings, "problem", "problems")} recorded against this phase.`];
|
|
4547
4646
|
case "unfinished-steps":
|
|
4548
4647
|
return [
|
|
4549
|
-
|
|
4550
|
-
...crossed.steps.map((step) => ` - Step: ${p.text(step.stepId)} \u2014 ${step.state === "failed" ? "failed" : "set aside unfinished"}`)
|
|
4648
|
+
`${OUTSTANDING_LABEL}${counted(crossed.steps.length, "step", "steps")} of the method left unfinished. ${shownBy(crossed.steps)}`,
|
|
4649
|
+
...crossed.steps.map((step) => ` - Step: ${p.text(step.name ?? step.stepId)} \u2014 ${step.state === "failed" ? "failed" : "set aside unfinished"}`)
|
|
4551
4650
|
];
|
|
4552
4651
|
}
|
|
4553
4652
|
}
|
|
4554
|
-
function
|
|
4653
|
+
function shownBy(steps) {
|
|
4654
|
+
const named = steps.filter((s) => s.name !== void 0).length;
|
|
4655
|
+
if (named === 0)
|
|
4656
|
+
return "Each is shown by the method's own label for it:";
|
|
4657
|
+
if (named === steps.length)
|
|
4658
|
+
return "Each is shown by the method's own name for it:";
|
|
4659
|
+
return "Each is shown by the method's own name for it, or by its label where the record holds no name:";
|
|
4660
|
+
}
|
|
4661
|
+
function machines(coverage, checks) {
|
|
4555
4662
|
const label = "- Computers signed in to Halfcycle for this project when the phase closed: ";
|
|
4556
4663
|
if (coverage === null)
|
|
4557
4664
|
return `${label}not recorded.`;
|
|
4558
4665
|
if (coverage.enrolled === 0)
|
|
4559
4666
|
return `${label}none.`;
|
|
4560
|
-
|
|
4667
|
+
const line = `${label}${String(coverage.enrolled)}. Of these, ${String(coverage.conformant)} had reported Halfcycle's checks as fully set up.`;
|
|
4668
|
+
return checks === "not-observed" && coverage.conformant > 0 ? `${line} Set up is not the same as running: Halfcycle has no record of any check running during this phase.` : line;
|
|
4561
4669
|
}
|
|
4562
4670
|
function walkLines(walk, p) {
|
|
4563
4671
|
const label = "> - Acceptance walk (the finished work tried as a user would, by someone other than its builder): ";
|
|
@@ -4577,9 +4685,15 @@ function walkLines(walk, p) {
|
|
|
4577
4685
|
function statedOrNot(value, p) {
|
|
4578
4686
|
return value === null ? "Not stated by the client." : p.text(value);
|
|
4579
4687
|
}
|
|
4688
|
+
function decisionLine(attested) {
|
|
4689
|
+
if (attested.decision === "override") {
|
|
4690
|
+
return "> - Decision: close the phase by override, without stating its finish conditions were met.";
|
|
4691
|
+
}
|
|
4692
|
+
return attested.walk !== null && attested.walk.verdict === "defects" ? "> - Decision: close the phase as it stood, with the problems its acceptance walk found." : "> - Decision: the phase's agreed finish conditions were met.";
|
|
4693
|
+
}
|
|
4580
4694
|
function renderStated(attested, p) {
|
|
4581
4695
|
const lines = [
|
|
4582
|
-
attested
|
|
4696
|
+
decisionLine(attested),
|
|
4583
4697
|
`> - Name given: ${attested.actor === null ? "removed with the account that made this close" : p.text(attested.actor)}`,
|
|
4584
4698
|
`> - Reason given: ${attested.reason === null ? "none." : p.text(attested.reason)}`,
|
|
4585
4699
|
...walkLines(attested.walk, p),
|
|
@@ -4607,7 +4721,7 @@ function renderClose(heading, observed, attested, checks, p) {
|
|
|
4607
4721
|
`- Closed at: ${closedAtText(observed.closedAt, p)}`,
|
|
4608
4722
|
`- Signed in as: ${signedInAs(observed.closedBy, p)}`,
|
|
4609
4723
|
...outstanding(observed.crossed, p),
|
|
4610
|
-
machines(observed.seatCoverage)
|
|
4724
|
+
machines(observed.seatCoverage, checks)
|
|
4611
4725
|
].join("\n"),
|
|
4612
4726
|
STATED_LABEL,
|
|
4613
4727
|
renderStated(attested, p)
|
|
@@ -4868,8 +4982,7 @@ async function obtainBuildRecord(credential, repoRoot, phase, io = NODE_RECORD_P
|
|
|
4868
4982
|
};
|
|
4869
4983
|
}
|
|
4870
4984
|
function refetchCommand(phase) {
|
|
4871
|
-
|
|
4872
|
-
return `npx halfcycle@latest build-record ${quoted}`;
|
|
4985
|
+
return `npx halfcycle@latest build-record ${quoteForShell(phase)}`;
|
|
4873
4986
|
}
|
|
4874
4987
|
|
|
4875
4988
|
// dist/open-phase.js
|
|
@@ -4878,10 +4991,12 @@ import { join as join7 } from "node:path";
|
|
|
4878
4991
|
var OpenPhaseRefused = class extends Error {
|
|
4879
4992
|
status;
|
|
4880
4993
|
finding;
|
|
4881
|
-
|
|
4994
|
+
previousPhase;
|
|
4995
|
+
constructor(status, message, finding, previousPhase = null) {
|
|
4882
4996
|
super(message);
|
|
4883
4997
|
this.status = status;
|
|
4884
4998
|
this.finding = finding;
|
|
4999
|
+
this.previousPhase = previousPhase;
|
|
4885
5000
|
this.name = "OpenPhaseRefused";
|
|
4886
5001
|
}
|
|
4887
5002
|
};
|
|
@@ -4960,7 +5075,8 @@ async function openPhase(credential, phase, entry, home) {
|
|
|
4960
5075
|
const body2 = await res.json().catch(() => null);
|
|
4961
5076
|
const message = typeof body2?.message === "string" ? body2.message : `${url} returned ${res.status}.`;
|
|
4962
5077
|
const finding = typeof body2?.finding === "string" ? body2.finding : null;
|
|
4963
|
-
|
|
5078
|
+
const previousPhase = typeof body2?.previousPhase === "string" ? body2.previousPhase : null;
|
|
5079
|
+
throw new OpenPhaseRefused(res.status, message, finding, previousPhase);
|
|
4964
5080
|
}
|
|
4965
5081
|
const body = await res.json().catch(() => null);
|
|
4966
5082
|
if (!body || !(typeof body.phase === "string" || body.phase === null) || !(typeof body.previousPhase === "string" || body.previousPhase === null) || typeof body.decision !== "object" || body.decision === null || typeof body.decision.decisionId !== "string") {
|
|
@@ -5186,7 +5302,7 @@ function requirementMessage(verb, flag) {
|
|
|
5186
5302
|
function runnableInvocation(verb, phase) {
|
|
5187
5303
|
const contract = contractFor(verb);
|
|
5188
5304
|
const flags = (contract?.required ?? []).map((req) => `${req.flag} ${PLACEHOLDERS[req.flag] ?? '"\u2026"'}`).join(" ");
|
|
5189
|
-
return `halfcycle ${verb} ${phase}${flags ? ` ${flags}` : ""}`;
|
|
5305
|
+
return `halfcycle ${verb} ${quoteForShell(phase)}${flags ? ` ${flags}` : ""}`;
|
|
5190
5306
|
}
|
|
5191
5307
|
var PLACEHOLDERS = {
|
|
5192
5308
|
"--verdict": "<clean|defects>",
|
|
@@ -6113,7 +6229,12 @@ ${USAGE}`);
|
|
|
6113
6229
|
// T-10 — optional; absent on a plane that has not configured
|
|
6114
6230
|
// CONTROL_TELEMETRY_URL yet. `writeEngagementCredential` writes '' for
|
|
6115
6231
|
// an absent value, never `undefined` verbatim.
|
|
6116
|
-
controlTelemetryUrl: joined.controlTelemetryUrl
|
|
6232
|
+
controlTelemetryUrl: joined.controlTelemetryUrl,
|
|
6233
|
+
// T-01 (Connect Phase 4 follow-ups) — optional; absent on a control
|
|
6234
|
+
// deployment that predates it. The join route's own stamped pin,
|
|
6235
|
+
// which can be older than this deployment's current default.
|
|
6236
|
+
consoleUrl: joined.consoleUrl,
|
|
6237
|
+
methodVersion: joined.methodVersion
|
|
6117
6238
|
};
|
|
6118
6239
|
foundLine = `this repo is pinned to one of your projects \u2014 joining it as you (${shortId(joined.engagementId)})`;
|
|
6119
6240
|
}
|
|
@@ -6137,7 +6258,12 @@ ${USAGE}`);
|
|
|
6137
6258
|
// T-10 — the FOURTH origin, optional. Absent on a plane that has not
|
|
6138
6259
|
// configured CONTROL_TELEMETRY_URL yet; the runner's own telemetry
|
|
6139
6260
|
// emission is fail-open on that absence.
|
|
6140
|
-
controlTelemetryUrl: created.controlTelemetryUrl
|
|
6261
|
+
controlTelemetryUrl: created.controlTelemetryUrl,
|
|
6262
|
+
// T-01 (Connect Phase 4 follow-ups) — optional; absent on a control
|
|
6263
|
+
// deployment that predates it. The create route's own deployment
|
|
6264
|
+
// default at the moment this engagement was stamped.
|
|
6265
|
+
consoleUrl: created.consoleUrl,
|
|
6266
|
+
methodVersion: created.methodVersion
|
|
6141
6267
|
};
|
|
6142
6268
|
foundLine = `no Halfcycle project here yet \u2014 creating one (${shortId(created.engagementId)})`;
|
|
6143
6269
|
if (verbose) {
|
|
@@ -6239,16 +6365,14 @@ ${USAGE}`);
|
|
|
6239
6365
|
process.stdout.write(`[halfcycle] Claude Code will then ask whether to use a new MCP server. The cursor starts on "Continue without using this MCP server" \u2014 choose to use it instead; without it Halfcycle will not connect.
|
|
6240
6366
|
`);
|
|
6241
6367
|
}
|
|
6242
|
-
if (credential) {
|
|
6243
|
-
|
|
6244
|
-
|
|
6245
|
-
process.stdout.write(`[halfcycle] Your project's page \u2014 how it is going, the agents working on it, and your other projects (first visit): ${minted.boardUrl}
|
|
6368
|
+
if (credential?.consoleUrl !== void 0) {
|
|
6369
|
+
const colour = decideColour(currentEnvironment(quiet));
|
|
6370
|
+
process.stdout.write(`${ink("key", `[halfcycle] Your project's console: ${credential.consoleUrl}`, colour)}
|
|
6246
6371
|
`);
|
|
6247
|
-
|
|
6372
|
+
}
|
|
6373
|
+
if (credential?.methodVersion !== void 0) {
|
|
6374
|
+
process.stdout.write(`[halfcycle] Method version: ${credential.methodVersion}
|
|
6248
6375
|
`);
|
|
6249
|
-
} catch {
|
|
6250
|
-
process.stdout.write("[halfcycle] Your project's page: no first-visit code was issued just now \u2014 re-run this installer to get one.\n");
|
|
6251
|
-
}
|
|
6252
6376
|
}
|
|
6253
6377
|
process.exit(0);
|
|
6254
6378
|
} catch (err) {
|
|
@@ -6295,7 +6419,9 @@ ${USAGE}`);
|
|
|
6295
6419
|
return;
|
|
6296
6420
|
}
|
|
6297
6421
|
try {
|
|
6298
|
-
const { drifted, installed, current } = checkDrift(targetRepo2);
|
|
6422
|
+
const { drifted, installed, current, methodVersion } = checkDrift(targetRepo2);
|
|
6423
|
+
process.stdout.write(`[halfcycle-bundle] Method version: ${methodVersion ?? "not recorded"}
|
|
6424
|
+
`);
|
|
6299
6425
|
if (drifted) {
|
|
6300
6426
|
process.stdout.write(`[halfcycle-bundle] DRIFT DETECTED: installed=${installed ?? "none"} current=${current}
|
|
6301
6427
|
[halfcycle-bundle] Re-run "halfcycle-bundle install ${targetRepo2}" to sync.
|
|
@@ -6367,8 +6493,13 @@ ${USAGE}`);
|
|
|
6367
6493
|
const opened = await openPhase(credential, toProjectScope ? null : phaseArg, entry);
|
|
6368
6494
|
const what = opened.phase === null ? "project scope" : `phase "${opened.phase}"`;
|
|
6369
6495
|
const from = opened.previousPhase === null ? "project scope" : `phase "${opened.previousPhase}"`;
|
|
6370
|
-
|
|
6496
|
+
if (opened.previousPhase === opened.phase) {
|
|
6497
|
+
process.stdout.write(opened.phase === null ? "[halfcycle] Already at project scope \u2014 nothing moved.\n" : `[halfcycle] Phase "${opened.phase}" is already open \u2014 nothing moved.
|
|
6371
6498
|
`);
|
|
6499
|
+
} else {
|
|
6500
|
+
process.stdout.write(`[halfcycle] Moved from ${from} to ${what}.
|
|
6501
|
+
`);
|
|
6502
|
+
}
|
|
6372
6503
|
process.stdout.write(`[halfcycle] Recorded as ${opened.decision.decision} by ${opened.decision.actor} at ${opened.decision.decidedAt}.
|
|
6373
6504
|
`);
|
|
6374
6505
|
if (opened.decision.blockingFinding) {
|
|
@@ -6385,8 +6516,7 @@ halfcycle open-phase: until ${opened.stampFailure.path} can be written, guard ev
|
|
|
6385
6516
|
process.exit(0);
|
|
6386
6517
|
} catch (err) {
|
|
6387
6518
|
if (err instanceof InvalidPhaseIdentityError) {
|
|
6388
|
-
process.stderr.write(`halfcycle open-phase: ${err.
|
|
6389
|
-
halfcycle open-phase: a phase name has to work as a single folder name on this machine \u2014 that is what this machine files its guard evaluations under. Nothing was opened.
|
|
6519
|
+
process.stderr.write(`halfcycle open-phase: '${String(err.identity)}' cannot be used as a phase name \u2014 it has to work as a single folder name on this machine. Nothing was opened.
|
|
6390
6520
|
`);
|
|
6391
6521
|
process.exit(2);
|
|
6392
6522
|
return;
|
|
@@ -6399,8 +6529,7 @@ halfcycle open-phase: a phase name has to work as a single folder name on this m
|
|
|
6399
6529
|
`);
|
|
6400
6530
|
}
|
|
6401
6531
|
if (err.status === 409) {
|
|
6402
|
-
|
|
6403
|
-
process.stderr.write(`halfcycle open-phase: close the phase first \u2014 \`${runnableInvocation("close-phase", stuck ?? "<phase>")}\` (add --finding "\u2026" once per finding when the verdict is defects) \u2014 or re-run with --override --reason "\u2026", which is recorded, not waived.
|
|
6532
|
+
process.stderr.write(`halfcycle open-phase: close the phase first \u2014 \`${runnableInvocation("close-phase", err.previousPhase ?? "<phase>")}\` (add --finding "\u2026" once per finding when the verdict is defects) \u2014 or re-run with --override --reason "\u2026", which is recorded, not waived.
|
|
6404
6533
|
`);
|
|
6405
6534
|
}
|
|
6406
6535
|
process.exit(1);
|
|
@@ -6511,17 +6640,22 @@ halfcycle close-phase: until ${closed.stampFailure.path} can be written, guard e
|
|
|
6511
6640
|
return;
|
|
6512
6641
|
}
|
|
6513
6642
|
if (err instanceof ClosePhaseRefused) {
|
|
6643
|
+
if (err.finding) {
|
|
6644
|
+
process.stderr.write(`halfcycle close-phase: refused (${err.status}).
|
|
6645
|
+
`);
|
|
6646
|
+
process.stderr.write(`halfcycle close-phase: outstanding work \u2014 ${plainOutstandingFinding(err.finding)}
|
|
6647
|
+
`);
|
|
6648
|
+
process.stderr.write(`halfcycle close-phase: close it anyway with \`halfcycle close-phase ${quoteForShell(phaseArg)} --verdict ${verdict} --actor ${quoteForShell(closeActor)} --override --reason "\u2026"\` \u2014 recorded, not waived.
|
|
6649
|
+
`);
|
|
6650
|
+
process.exit(1);
|
|
6651
|
+
return;
|
|
6652
|
+
}
|
|
6514
6653
|
process.stderr.write(`halfcycle close-phase: refused (${err.status}). ${err.message}
|
|
6515
6654
|
`);
|
|
6516
6655
|
if (err.status === 400) {
|
|
6517
6656
|
process.stderr.write("halfcycle close-phase: Nothing was closed.\n");
|
|
6518
6657
|
}
|
|
6519
|
-
if (err.
|
|
6520
|
-
process.stderr.write(`halfcycle close-phase: outstanding work \u2014 ${err.finding}
|
|
6521
|
-
`);
|
|
6522
|
-
process.stderr.write(`halfcycle close-phase: close it anyway with \`halfcycle close-phase ${phaseArg} --verdict ${verdict} --actor "${closeActor}" --override --reason "\u2026"\` \u2014 recorded, not waived.
|
|
6523
|
-
`);
|
|
6524
|
-
} else if (err.code === "PhaseAlreadyClosed") {
|
|
6658
|
+
if (err.code === "PhaseAlreadyClosed") {
|
|
6525
6659
|
process.stderr.write("halfcycle close-phase: to close a phase again, open it again first.\nhalfcycle close-phase: To get a closed phase's Build Record: npx halfcycle@latest build-record <the phase that closed>\n");
|
|
6526
6660
|
}
|
|
6527
6661
|
process.exit(1);
|
|
@@ -6560,6 +6694,9 @@ function readStatementFlags(argv) {
|
|
|
6560
6694
|
}
|
|
6561
6695
|
return { thesis, outcome, deviations };
|
|
6562
6696
|
}
|
|
6697
|
+
function plainOutstandingFinding(finding) {
|
|
6698
|
+
return finding.replace(/\(attempted-failed\)/g, "(attempted, and did not finish)");
|
|
6699
|
+
}
|
|
6563
6700
|
function writtenLines(written) {
|
|
6564
6701
|
return `[halfcycle] Build Record written:
|
|
6565
6702
|
${written.mdPath}
|