halfcycle 0.3.18 → 0.3.19
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 +1 -1
- package/bin/bin.bundle.mjs +21 -4
- package/dist/bin.js +171 -60
- package/dist/bin.js.map +3 -3
- package/dist/confirm-identity.d.ts +25 -0
- package/dist/confirm-identity.d.ts.map +1 -1
- package/dist/create-engagement.d.ts +29 -1
- package/dist/create-engagement.d.ts.map +1 -1
- package/dist/engagement-credential.d.ts +27 -0
- package/dist/engagement-credential.d.ts.map +1 -1
- package/dist/index.js +73 -22
- package/dist/index.js.map +1 -1
- package/dist/install.d.ts.map +1 -1
- package/dist/own-engagement.d.ts +50 -10
- package/dist/own-engagement.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "halfcycle",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.19",
|
|
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
|
@@ -42,7 +42,7 @@ A method document cannot fix that, because a document cannot interrupt you. This
|
|
|
42
42
|
|
|
43
43
|
Credit where it is due, and it is due. GStack, Matt Pocock's skills, GitHub Spec Kit, BMAD, OpenSpec and claude-flow are genuinely good, and between them they taught this whole category that the bottleneck is discipline rather than model intelligence. We took two things from them deliberately and say so: the one-question-at-a-time interrogation shape, and an install that is one command and then stays out of your way.
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
The objection this usually meets is that it must be too much for a small project. It would be, if there were one setting. A folder you download has exactly one, so whoever wrote it had to choose who to disappoint, and it is usually the small project that gets buried and quietly drops the process in week three. This reads the work instead and sizes itself to it, which is the section further down.
|
|
46
46
|
|
|
47
47
|
Writing the process down was never the hard part. What a folder you download structurally cannot have is these four things.
|
|
48
48
|
|
package/bin/bin.bundle.mjs
CHANGED
|
@@ -39,6 +39,9 @@ function readConfig() {
|
|
|
39
39
|
}
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
|
+
function readNeverConfiguredReason() {
|
|
43
|
+
return process.env["HALFCYCLE_NOT_THIS_ACCOUNT"] === "1" ? "not-this-account" : "no-credential-yet";
|
|
44
|
+
}
|
|
42
45
|
function readExcerptConfig() {
|
|
43
46
|
const raw = process.env["HALFCYCLE_SOURCE_EXCERPTS"];
|
|
44
47
|
const enabled = (raw ?? "").trim().toLowerCase() === "true";
|
|
@@ -14781,6 +14784,8 @@ function renderPhaseSegment(identity) {
|
|
|
14781
14784
|
return `${PHASE_SEGMENT_PREFIX}${identity}`;
|
|
14782
14785
|
}
|
|
14783
14786
|
function describeIdentity(identity) {
|
|
14787
|
+
if (identity === "")
|
|
14788
|
+
return "(empty)";
|
|
14784
14789
|
if (typeof identity === "string")
|
|
14785
14790
|
return identity;
|
|
14786
14791
|
if (identity === null)
|
|
@@ -16194,8 +16199,8 @@ This is not a service outage: the guard service answered. The suspect is this re
|
|
|
16194
16199
|
}
|
|
16195
16200
|
var UNCONFIGURED_EXIT_TOOL_USE = 2;
|
|
16196
16201
|
var UNCONFIGURED_EXIT_STOP_FAMILY = 1;
|
|
16197
|
-
function emitNeverConfigured(missing, exitCode) {
|
|
16198
|
-
writeSync(2,
|
|
16202
|
+
function emitNeverConfigured(missing, exitCode, reason) {
|
|
16203
|
+
writeSync(2, neverConfiguredReport(missing, reason));
|
|
16199
16204
|
process.exitCode = exitCode;
|
|
16200
16205
|
}
|
|
16201
16206
|
var HOOK_REMEDY = `Fix it by running "npx halfcycle" in this repository: it may open your browser to sign in first, then writes this engagement's credentials to ~/.halfcycle \u2014 outside the repository \u2014 and the hook loads them itself.`;
|
|
@@ -16207,6 +16212,18 @@ This is not a service outage. It means the guard has never run here \u2014 not o
|
|
|
16207
16212
|
${remedy}
|
|
16208
16213
|
`;
|
|
16209
16214
|
}
|
|
16215
|
+
function notThisAccountReport(label) {
|
|
16216
|
+
return `${label} This project belongs to another Halfcycle account. Halfcycle is Solo today \u2014 one person works on a project at a time \u2014 so no credential exists for this machine and none will be issued. Nothing here needs fixing; this copy simply is not checked. Ask the project's owner for view access, or \u2014 if this account is wrong \u2014 sign in as the one that owns the project and run "npx halfcycle" again.
|
|
16217
|
+
`;
|
|
16218
|
+
}
|
|
16219
|
+
function neverConfiguredReport(missing, reason, label = "[Halfcycle]") {
|
|
16220
|
+
switch (reason) {
|
|
16221
|
+
case "no-credential-yet":
|
|
16222
|
+
return neverConfiguredMessage(missing, HOOK_REMEDY, label);
|
|
16223
|
+
case "not-this-account":
|
|
16224
|
+
return notThisAccountReport(label);
|
|
16225
|
+
}
|
|
16226
|
+
}
|
|
16210
16227
|
var CREDENTIAL_REJECTED_STATUSES = /* @__PURE__ */ new Set([401, 403]);
|
|
16211
16228
|
function classifyFailure(failure) {
|
|
16212
16229
|
if (failure.kind === "contract-error") {
|
|
@@ -16430,7 +16447,7 @@ async function runPostToolUse() {
|
|
|
16430
16447
|
const configResult = readConfig();
|
|
16431
16448
|
if (!configResult.config) {
|
|
16432
16449
|
await emitRunRecord({ runType: "hook", outcome: "unconfigured", changeSet });
|
|
16433
|
-
emitNeverConfigured(configResult.missing, UNCONFIGURED_EXIT_TOOL_USE);
|
|
16450
|
+
emitNeverConfigured(configResult.missing, UNCONFIGURED_EXIT_TOOL_USE, readNeverConfiguredReason());
|
|
16434
16451
|
return;
|
|
16435
16452
|
}
|
|
16436
16453
|
const { guardServiceUrl, guardServiceToken, guardEngagementId } = configResult.config;
|
|
@@ -16696,7 +16713,7 @@ async function runSessionDiff(input, kind) {
|
|
|
16696
16713
|
const configResult = readConfig();
|
|
16697
16714
|
if (!configResult.config) {
|
|
16698
16715
|
await emitRunRecord({ runType: "hook", outcome: "unconfigured", changeSet, diffBase });
|
|
16699
|
-
emitNeverConfigured(configResult.missing, UNCONFIGURED_EXIT_STOP_FAMILY);
|
|
16716
|
+
emitNeverConfigured(configResult.missing, UNCONFIGURED_EXIT_STOP_FAMILY, readNeverConfiguredReason());
|
|
16700
16717
|
return;
|
|
16701
16718
|
}
|
|
16702
16719
|
const { guardServiceUrl, guardServiceToken, guardEngagementId } = configResult.config;
|
package/dist/bin.js
CHANGED
|
@@ -441,11 +441,26 @@ function loopbackVerificationUrl(verificationUrl, target) {
|
|
|
441
441
|
return url.toString();
|
|
442
442
|
}
|
|
443
443
|
|
|
444
|
-
// ../events/dist/
|
|
444
|
+
// ../events/dist/join-refusal.js
|
|
445
445
|
import { z as z7 } from "zod";
|
|
446
|
-
var
|
|
447
|
-
|
|
448
|
-
|
|
446
|
+
var NOT_THIS_ACCOUNT_REASON = "not-this-account";
|
|
447
|
+
var JOIN_REFUSAL_STATUS = 403;
|
|
448
|
+
var notThisAccountRefusalSchema = z7.object({
|
|
449
|
+
statusCode: z7.literal(JOIN_REFUSAL_STATUS),
|
|
450
|
+
error: z7.string(),
|
|
451
|
+
message: z7.string(),
|
|
452
|
+
reason: z7.literal(NOT_THIS_ACCOUNT_REASON)
|
|
453
|
+
}).strict();
|
|
454
|
+
var joinRefusalSchema = z7.union([notThisAccountRefusalSchema, wireErrorSchema]);
|
|
455
|
+
function isNotThisAccountRefusal(body) {
|
|
456
|
+
return typeof body === "object" && body !== null && body.reason === NOT_THIS_ACCOUNT_REASON;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// ../events/dist/account-identity.js
|
|
460
|
+
import { z as z8 } from "zod";
|
|
461
|
+
var accountIdentitySchema = z8.object({
|
|
462
|
+
accountId: z8.string().min(1),
|
|
463
|
+
email: z8.string().min(1).optional()
|
|
449
464
|
}).strict();
|
|
450
465
|
function safeParseAccountIdentity(payload) {
|
|
451
466
|
return accountIdentitySchema.safeParse(payload);
|
|
@@ -495,6 +510,8 @@ function renderPhaseSegment(identity) {
|
|
|
495
510
|
return `${PHASE_SEGMENT_PREFIX}${identity}`;
|
|
496
511
|
}
|
|
497
512
|
function describeIdentity(identity) {
|
|
513
|
+
if (identity === "")
|
|
514
|
+
return "(empty)";
|
|
498
515
|
if (typeof identity === "string")
|
|
499
516
|
return identity;
|
|
500
517
|
if (identity === null)
|
|
@@ -595,6 +612,18 @@ function engagementStateDir(engagementId, home) {
|
|
|
595
612
|
function engagementEnvPath(engagementId, home) {
|
|
596
613
|
return join2(engagementStateDir(engagementId, home), ENV_FILENAME);
|
|
597
614
|
}
|
|
615
|
+
var NOT_THIS_ACCOUNT_MARKER_FILENAME = "not-this-account";
|
|
616
|
+
function notThisAccountMarkerPath(engagementId, home) {
|
|
617
|
+
return join2(engagementStateDir(engagementId, home), NOT_THIS_ACCOUNT_MARKER_FILENAME);
|
|
618
|
+
}
|
|
619
|
+
function writeNotThisAccountMarker(engagementId, home) {
|
|
620
|
+
const dir = engagementStateDir(engagementId, home);
|
|
621
|
+
mkdirSync2(dir, { recursive: true, mode: 448 });
|
|
622
|
+
const path = notThisAccountMarkerPath(engagementId, home);
|
|
623
|
+
writeFileSync2(path, `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
624
|
+
`, { mode: 384 });
|
|
625
|
+
applyOwnerOnly(path, dir);
|
|
626
|
+
}
|
|
598
627
|
var GUARD_ENV_KEYS = [
|
|
599
628
|
"GUARD_SERVICE_URL",
|
|
600
629
|
"GUARD_SERVICE_TOKEN",
|
|
@@ -736,14 +765,15 @@ function engagementResolutionShell() {
|
|
|
736
765
|
# desktop session's PATH, so the id is cut out with sed \u2014 after flattening the
|
|
737
766
|
# file, because sed is line-oriented and the pin's line breaks are not a contract.
|
|
738
767
|
#
|
|
739
|
-
# Sets HALFCYCLE_ENV_FILE, HALFCYCLE_ENV_ENGAGEMENT
|
|
740
|
-
# the CALLER's shell and returns a status. Call
|
|
741
|
-
# never as \`$(halfcycle_env_file \u2026)\` \u2014 a
|
|
742
|
-
# would discard everything it set.
|
|
768
|
+
# Sets HALFCYCLE_ENV_FILE, HALFCYCLE_ENV_ENGAGEMENT, HALFCYCLE_ENV_PROBLEM and
|
|
769
|
+
# HALFCYCLE_ENV_NOT_THIS_ACCOUNT in the CALLER's shell and returns a status. Call
|
|
770
|
+
# it as \`if halfcycle_env_file \u2026\`, never as \`$(halfcycle_env_file \u2026)\` \u2014 a
|
|
771
|
+
# command substitution is a subshell and would discard everything it set.
|
|
743
772
|
halfcycle_env_file() {
|
|
744
773
|
HALFCYCLE_ENV_FILE=""
|
|
745
774
|
HALFCYCLE_ENV_ENGAGEMENT=""
|
|
746
775
|
HALFCYCLE_ENV_PROBLEM=""
|
|
776
|
+
HALFCYCLE_ENV_NOT_THIS_ACCOUNT=""
|
|
747
777
|
hc_pin="$1/.halfcycle/bundle.json"
|
|
748
778
|
if [ ! -f "$hc_pin" ]; then
|
|
749
779
|
HALFCYCLE_ENV_PROBLEM="no-pin"
|
|
@@ -759,6 +789,9 @@ halfcycle_env_file() {
|
|
|
759
789
|
HALFCYCLE_ENV_PROBLEM="no-home"
|
|
760
790
|
return 1
|
|
761
791
|
fi
|
|
792
|
+
if [ -f "$HOME/.halfcycle/${ENGAGEMENTS_DIR}/$hc_id/${NOT_THIS_ACCOUNT_MARKER_FILENAME}" ]; then
|
|
793
|
+
HALFCYCLE_ENV_NOT_THIS_ACCOUNT=1
|
|
794
|
+
fi
|
|
762
795
|
hc_env="$HOME/.halfcycle/${ENGAGEMENTS_DIR}/$hc_id/${ENV_FILENAME}"
|
|
763
796
|
if [ ! -f "$hc_env" ]; then
|
|
764
797
|
HALFCYCLE_ENV_PROBLEM="no-credential"
|
|
@@ -1130,6 +1163,13 @@ if halfcycle_env_file "$PROJECT_DIR"; then
|
|
|
1130
1163
|
set +a
|
|
1131
1164
|
fi
|
|
1132
1165
|
|
|
1166
|
+
# Pass along WHY this machine has no credential, as a plain variable \u2014 the
|
|
1167
|
+
# marker check above already answered it, and this is the one bit of that
|
|
1168
|
+
# answer the runner binary needs to say the right thing on every edit.
|
|
1169
|
+
if [ "$HALFCYCLE_ENV_NOT_THIS_ACCOUNT" = "1" ]; then
|
|
1170
|
+
export HALFCYCLE_NOT_THIS_ACCOUNT=1
|
|
1171
|
+
fi
|
|
1172
|
+
|
|
1133
1173
|
exec node "$PROJECT_DIR/${VENDORED_BIN_REL}" "$@"
|
|
1134
1174
|
`;
|
|
1135
1175
|
}
|
|
@@ -1145,13 +1185,20 @@ MARKER_FILE="\${TMPDIR%/}/halfcycle-session-\${HASH}.ref"
|
|
|
1145
1185
|
git -C "$REPO_ROOT" rev-parse HEAD > "$MARKER_FILE" 2>/dev/null || true
|
|
1146
1186
|
|
|
1147
1187
|
# ---------------------------------------------------------------------------
|
|
1148
|
-
# STANDING GUARD-COVERAGE STATEMENT (T-27).
|
|
1188
|
+
# STANDING GUARD-COVERAGE STATEMENT (T-27; two arms since T-02b).
|
|
1149
1189
|
#
|
|
1150
1190
|
# A repository with no credential on this machine is UNGUARDED: the PostToolUse
|
|
1151
1191
|
# hook will find nothing to evaluate with, and it says so once per edit, after
|
|
1152
1192
|
# the edit. This says it once, up front, before anything is written \u2014 which is
|
|
1153
1193
|
# the one thing a session-start hook can do that nothing else can.
|
|
1154
1194
|
#
|
|
1195
|
+
# TWO REASONS TO HAVE NO CREDENTIAL, TWO DIFFERENT THINGS TO SAY (T-02b,
|
|
1196
|
+
# solo-project-ownership \u2014 outcome B). "You have not signed in here yet" is
|
|
1197
|
+
# fixed by re-running the installer; "this is not your project" is permanent,
|
|
1198
|
+
# and the same remedy would send the reader in a circle forever. The two are
|
|
1199
|
+
# told apart by HALFCYCLE_ENV_NOT_THIS_ACCOUNT, set by \`halfcycle_env_file\`
|
|
1200
|
+
# below from a machine-level marker beside the credential store.
|
|
1201
|
+
#
|
|
1155
1202
|
# Everything below runs in a SUBSHELL: it sources a credential store, and a
|
|
1156
1203
|
# session-start hook must not leak an engagement's variables into whatever the
|
|
1157
1204
|
# editor runs next. Any failure inside it is swallowed; this script's exit
|
|
@@ -1165,18 +1212,33 @@ ${engagementResolutionShell()}
|
|
|
1165
1212
|
. "$HALFCYCLE_ENV_FILE"
|
|
1166
1213
|
set +a
|
|
1167
1214
|
fi
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
echo "[halfcycle]
|
|
1175
|
-
echo "[halfcycle]
|
|
1176
|
-
echo "[halfcycle]
|
|
1177
|
-
echo "[halfcycle]
|
|
1178
|
-
echo "[halfcycle]
|
|
1179
|
-
echo "[halfcycle]
|
|
1215
|
+
if [ "$HALFCYCLE_ENV_NOT_THIS_ACCOUNT" = "1" ]; then
|
|
1216
|
+
# OUTCOME B (T-02b, solo-project-ownership). This is not the missing-keys
|
|
1217
|
+
# case below wearing different words: there is nothing to fix, and the loop
|
|
1218
|
+
# that follows is skipped entirely rather than run and then overridden \u2014 a
|
|
1219
|
+
# repository whose credential is absent for THIS reason must never be told
|
|
1220
|
+
# to re-run the installer, which can never obtain one for it.
|
|
1221
|
+
echo "[halfcycle] This project belongs to another Halfcycle account, so nothing here is checked."
|
|
1222
|
+
echo "[halfcycle] Your work carries on exactly as it would without Halfcycle \u2014 no edit is"
|
|
1223
|
+
echo "[halfcycle] evaluated, and nothing you do is reported as having passed."
|
|
1224
|
+
echo "[halfcycle] Halfcycle is Solo today \u2014 one developer per project \u2014 with Team plans coming."
|
|
1225
|
+
echo "[halfcycle] Ask the project's owner for view access."
|
|
1226
|
+
echo "[halfcycle] If this account is wrong, sign in as the one that owns the project and run"
|
|
1227
|
+
echo "[halfcycle] \\"npx halfcycle\\" again."
|
|
1228
|
+
else
|
|
1229
|
+
hc_missing=""
|
|
1230
|
+
for hc_key in ${GUARD_COVERAGE_REQUIRED_KEYS.join(" ")}; do
|
|
1231
|
+
eval "hc_value=\\\${$hc_key:-}"
|
|
1232
|
+
if [ -z "$hc_value" ]; then hc_missing="$hc_missing $hc_key"; fi
|
|
1233
|
+
done
|
|
1234
|
+
if [ -n "$hc_missing" ]; then
|
|
1235
|
+
echo "[halfcycle] NO GUARD COVERAGE IN THIS REPOSITORY. Nothing will evaluate the edits made in"
|
|
1236
|
+
echo "[halfcycle] this session: this machine has no Halfcycle credential for it, so the guard hook"
|
|
1237
|
+
echo "[halfcycle] has nothing to call with (missing:$hc_missing)."
|
|
1238
|
+
echo "[halfcycle] Run \\"npx halfcycle\\" in this repository to fix it \u2014 it signs you in through your"
|
|
1239
|
+
echo "[halfcycle] browser if needed, writes the credential to \\$HOME/.halfcycle outside this tree,"
|
|
1240
|
+
echo "[halfcycle] and needs nothing configured first."
|
|
1241
|
+
fi
|
|
1180
1242
|
fi
|
|
1181
1243
|
) 2>/dev/null || true
|
|
1182
1244
|
exit 0
|
|
@@ -1336,8 +1398,20 @@ if [ -z "$TOKEN" ]; then
|
|
|
1336
1398
|
no-home)
|
|
1337
1399
|
echo "halfcycle: HOME is not set, so the credential store cannot be located." >&2 ;;
|
|
1338
1400
|
*)
|
|
1339
|
-
|
|
1340
|
-
|
|
1401
|
+
# OUTCOME B (T-02b, solo-project-ownership) is a \`no-credential\` PROBLEM
|
|
1402
|
+
# with a REASON attached, checked here rather than as a fifth case value
|
|
1403
|
+
# \u2014 see \`engagementResolutionShell\`'s own docblock for why. Re-running
|
|
1404
|
+
# this installer can never obtain a credential for this reason, so it is
|
|
1405
|
+
# the one sentence in this file that must not say to.
|
|
1406
|
+
if [ "$HALFCYCLE_ENV_NOT_THIS_ACCOUNT" = "1" ]; then
|
|
1407
|
+
echo "halfcycle: this project belongs to another Halfcycle account, so this machine has no" >&2
|
|
1408
|
+
echo "halfcycle: credential for it and the Halfcycle tools are not available here. Nothing is" >&2
|
|
1409
|
+
echo "halfcycle: broken \u2014 ask the project's owner for view access, or sign in as the account" >&2
|
|
1410
|
+
echo "halfcycle: that owns it and run \\"npx halfcycle\\" again." >&2
|
|
1411
|
+
else
|
|
1412
|
+
echo "halfcycle: this machine holds no credential for engagement $HALFCYCLE_ENV_ENGAGEMENT." >&2
|
|
1413
|
+
echo "halfcycle: run \\"npx halfcycle\\" in this repository \u2014 it will sign you in through your browser if needed, and write the credential to $HOME/.halfcycle, outside this repository." >&2
|
|
1414
|
+
fi ;;
|
|
1341
1415
|
esac
|
|
1342
1416
|
exit 1
|
|
1343
1417
|
fi
|
|
@@ -2142,11 +2216,13 @@ var CreateEngagementRefused = class extends Error {
|
|
|
2142
2216
|
status;
|
|
2143
2217
|
serverMessage;
|
|
2144
2218
|
errorCode;
|
|
2145
|
-
|
|
2219
|
+
reason;
|
|
2220
|
+
constructor(status, serverMessage, errorCode, reason, message) {
|
|
2146
2221
|
super(message);
|
|
2147
2222
|
this.status = status;
|
|
2148
2223
|
this.serverMessage = serverMessage;
|
|
2149
2224
|
this.errorCode = errorCode;
|
|
2225
|
+
this.reason = reason;
|
|
2150
2226
|
this.name = "CreateEngagementRefused";
|
|
2151
2227
|
}
|
|
2152
2228
|
/**
|
|
@@ -2191,7 +2267,8 @@ function planeRefusal(detail) {
|
|
|
2191
2267
|
return void 0;
|
|
2192
2268
|
return {
|
|
2193
2269
|
message: candidate.message,
|
|
2194
|
-
error: typeof candidate.error === "string" ? candidate.error : void 0
|
|
2270
|
+
error: typeof candidate.error === "string" ? candidate.error : void 0,
|
|
2271
|
+
reason: typeof candidate.reason === "string" ? candidate.reason : void 0
|
|
2195
2272
|
};
|
|
2196
2273
|
}
|
|
2197
2274
|
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.`;
|
|
@@ -2232,7 +2309,7 @@ async function requestEngagementValues(url, requestBody, credential, shape) {
|
|
|
2232
2309
|
const detail = await res.text().catch(() => "");
|
|
2233
2310
|
const refusal = res.status === 404 ? void 0 : planeRefusal(detail);
|
|
2234
2311
|
if (refusal) {
|
|
2235
|
-
throw new CreateEngagementRefused(res.status, refusal.message, refusal.error, `[bundle install] ${shape.action} failed: ${url} returned ${res.status}. ${refusal.message} ${shape.nothingHappened}.`);
|
|
2312
|
+
throw new CreateEngagementRefused(res.status, refusal.message, refusal.error, refusal.reason, `[bundle install] ${shape.action} failed: ${url} returned ${res.status}. ${refusal.message} ${shape.nothingHappened}.`);
|
|
2236
2313
|
}
|
|
2237
2314
|
const wrongOrigin = res.status === 404 ? `That address answered, but it does not serve ${shape.route} \u2014 so it is not the Halfcycle service. ` : "";
|
|
2238
2315
|
throw new Error(`[bundle install] ${shape.action} failed: ${url} returned ${res.status}. ${wrongOrigin}${detail ? `Response: ${detail.slice(0, 300)}. ` : ""}${shape.nothingHappened}. ` + CONTROL_ORIGIN_HINT);
|
|
@@ -2713,6 +2790,9 @@ function armScript(credential, act, label, plainLabel, home) {
|
|
|
2713
2790
|
}
|
|
2714
2791
|
}
|
|
2715
2792
|
}
|
|
2793
|
+
function identitySwitchRemedy(home) {
|
|
2794
|
+
return `To act as a different account: run this again and answer "s" when asked, set HALFCYCLE_TOKEN for that account, or remove ${accountStorePath(home)} to be asked for a sign-in next time.`;
|
|
2795
|
+
}
|
|
2716
2796
|
async function confirmActingIdentity(serviceUrl, credential, act, deps = {}) {
|
|
2717
2797
|
const write = deps.write ?? ((text) => process.stdout.write(text));
|
|
2718
2798
|
const colour = deps.colour ?? decideColour({ isTTY: process.stdout.isTTY === true, env: deps.env ?? process.env });
|
|
@@ -2778,6 +2858,9 @@ function deriveProjectName(targetRepoRoot) {
|
|
|
2778
2858
|
}
|
|
2779
2859
|
|
|
2780
2860
|
// dist/own-engagement.js
|
|
2861
|
+
function isOutcomeBRefusal(result) {
|
|
2862
|
+
return "outcome" in result && result.outcome === "not-this-account";
|
|
2863
|
+
}
|
|
2781
2864
|
async function createOwnedEngagement(serviceUrl, targetRepo, deps = {}) {
|
|
2782
2865
|
let credential = await confirmActingIdentity(serviceUrl, await obtainCredential(serviceUrl, deps), { kind: "create", targetRepo }, deps);
|
|
2783
2866
|
for (; ; ) {
|
|
@@ -2812,6 +2895,13 @@ async function joinPinnedEngagement(serviceUrl, engagementId, targetRepo, deps =
|
|
|
2812
2895
|
if (!(err instanceof CreateEngagementRefused))
|
|
2813
2896
|
throw err;
|
|
2814
2897
|
if (err.status === 403) {
|
|
2898
|
+
if (isNotThisAccountRefusal({ reason: err.reason })) {
|
|
2899
|
+
return {
|
|
2900
|
+
outcome: "not-this-account",
|
|
2901
|
+
message: err.serverMessage,
|
|
2902
|
+
actingAccountId: credential.accountId
|
|
2903
|
+
};
|
|
2904
|
+
}
|
|
2815
2905
|
throw new SignInRefused(`join-${err.status}`, `${err.serverMessage} ${JOIN_REFUSED_LOCAL_REMEDY}`);
|
|
2816
2906
|
}
|
|
2817
2907
|
if (err.status !== 401) {
|
|
@@ -3806,6 +3896,7 @@ ${USAGE}`);
|
|
|
3806
3896
|
const reusable = pinned !== null && pinned.engagementId === requestedId ? pinned.credential : void 0;
|
|
3807
3897
|
let engagementId;
|
|
3808
3898
|
let credential;
|
|
3899
|
+
let notThisAccountMessage;
|
|
3809
3900
|
let actingAccountId;
|
|
3810
3901
|
let foundLine = "";
|
|
3811
3902
|
if (reusable !== void 0 && pinned !== null) {
|
|
@@ -3826,21 +3917,30 @@ ${USAGE}`);
|
|
|
3826
3917
|
}
|
|
3827
3918
|
} else if (requestedId !== void 0) {
|
|
3828
3919
|
const joined = await joinPinnedEngagement(serviceUrl, requestedId, targetRepo, { assumeYes, verbose });
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3920
|
+
if (isOutcomeBRefusal(joined)) {
|
|
3921
|
+
engagementId = requestedId;
|
|
3922
|
+
actingAccountId = joined.actingAccountId;
|
|
3923
|
+
credential = void 0;
|
|
3924
|
+
notThisAccountMessage = joined.message;
|
|
3925
|
+
foundLine = `this repo is pinned to a project that belongs to a different Halfcycle account`;
|
|
3926
|
+
writeNotThisAccountMarker(engagementId);
|
|
3927
|
+
} else {
|
|
3928
|
+
engagementId = joined.engagementId;
|
|
3929
|
+
actingAccountId = joined.actingAccountId;
|
|
3930
|
+
credential = {
|
|
3931
|
+
serviceUrl,
|
|
3932
|
+
token: joined.sessionToken,
|
|
3933
|
+
mcpUrl: joined.mcpUrl,
|
|
3934
|
+
guardUrl: joined.guardUrl,
|
|
3935
|
+
// T-10 — optional; absent on a plane that has not configured
|
|
3936
|
+
// CONTROL_TELEMETRY_URL yet. `writeEngagementCredential` writes '' for
|
|
3937
|
+
// an absent value, never `undefined` verbatim.
|
|
3938
|
+
controlTelemetryUrl: joined.controlTelemetryUrl
|
|
3939
|
+
};
|
|
3940
|
+
foundLine = `this repo is pinned to a shared project \u2014 joining it as you (${shortId(joined.engagementId)})`;
|
|
3941
|
+
}
|
|
3942
|
+
if (verbose && credential !== void 0) {
|
|
3943
|
+
process.stdout.write(`[halfcycle] Joined engagement ${engagementId} \u2014 this credential is yours. Everyone else's keeps working; nobody was signed out and nothing was pasted.
|
|
3844
3944
|
`);
|
|
3845
3945
|
}
|
|
3846
3946
|
} else {
|
|
@@ -3901,18 +4001,20 @@ ${USAGE}`);
|
|
|
3901
4001
|
[halfcycle] ${missing.length === 1 ? "it" : "them"}) and re-run, and the credential moves out of the tree.
|
|
3902
4002
|
`);
|
|
3903
4003
|
}
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
if (
|
|
3907
|
-
|
|
4004
|
+
if (credential) {
|
|
4005
|
+
const probe = await probeMcpOrigin(credential.mcpUrl);
|
|
4006
|
+
if (probe.reached) {
|
|
4007
|
+
if (verbose) {
|
|
4008
|
+
process.stdout.write(`[halfcycle] Method delivery (MCP): ${credential.mcpUrl} \u2014 reachable (${probe.serverName})
|
|
3908
4009
|
`);
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
4010
|
+
}
|
|
4011
|
+
} else {
|
|
4012
|
+
process.stdout.write(`[halfcycle] Method delivery (MCP): ${credential.mcpUrl} \u2014 NOT reachable: ${probe.problem}.
|
|
3912
4013
|
[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.
|
|
3913
4014
|
`);
|
|
4015
|
+
}
|
|
3914
4016
|
}
|
|
3915
|
-
if (verbose) {
|
|
4017
|
+
if (credential && verbose) {
|
|
3916
4018
|
process.stdout.write(`[halfcycle] Guard evaluation: ON \u2014 the hook evaluates against ${credential.guardUrl}. Credentials (${GUARD_COVERAGE_REQUIRED_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.
|
|
3917
4019
|
`);
|
|
3918
4020
|
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.
|
|
@@ -3920,30 +4022,38 @@ ${USAGE}`);
|
|
|
3920
4022
|
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).
|
|
3921
4023
|
`);
|
|
3922
4024
|
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.
|
|
4025
|
+
`);
|
|
4026
|
+
}
|
|
4027
|
+
if (credential === void 0 && notThisAccountMessage !== void 0) {
|
|
4028
|
+
process.stdout.write(`[halfcycle] ${notThisAccountMessage} Halfcycle is Solo today \u2014 one developer per project \u2014 with Team plans coming. Your work carries on exactly as it would without Halfcycle: no edit is evaluated, and nothing you do here is reported as having passed. Ask the project's owner for view access. ${identitySwitchRemedy()}
|
|
3923
4029
|
`);
|
|
3924
4030
|
}
|
|
3925
4031
|
reportClaudeCodeVersion(verbose);
|
|
3926
|
-
const identity = await describeAccount(credential.serviceUrl, credential.token);
|
|
4032
|
+
const identity = credential ? await describeAccount(credential.serviceUrl, credential.token) : void 0;
|
|
3927
4033
|
const written = result.writtenPaths.length;
|
|
3928
4034
|
const merged = result.mergedPaths.length;
|
|
3929
4035
|
process.stdout.write(`[halfcycle] Signed in as ${accountLabel(identity, actingAccountId, verbose)}
|
|
3930
4036
|
`);
|
|
3931
4037
|
process.stdout.write(`[halfcycle] Found: ${foundLine}
|
|
3932
4038
|
`);
|
|
3933
|
-
process.stdout.write(`[halfcycle] Set up: ${written} file${written === 1 ? "" : "s"} written${merged > 0 ? ` (${merged} merged)` : ""}, guards armed on every edit
|
|
4039
|
+
process.stdout.write(`[halfcycle] Set up: ${written} file${written === 1 ? "" : "s"} written${merged > 0 ? ` (${merged} merged)` : ""}, ${credential ? "guards armed on every edit" : "not checked \u2014 this copy is not this account\u2019s project"}
|
|
3934
4040
|
`);
|
|
3935
|
-
|
|
4041
|
+
if (credential) {
|
|
4042
|
+
process.stdout.write(`[halfcycle] Next: open this folder in Claude Code \u2014 accept the workspace-trust prompt, it is expected \u2014 then run /halfcycle-setup and answer what it asks about your project
|
|
3936
4043
|
`);
|
|
3937
|
-
|
|
4044
|
+
process.stdout.write(`[halfcycle] Claude Code will also ask permission the first time Halfcycle needs to look up your next step. Choose allow \u2014 without it nothing can run.
|
|
3938
4045
|
`);
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
4046
|
+
}
|
|
4047
|
+
if (credential) {
|
|
4048
|
+
try {
|
|
4049
|
+
const minted = await mintBoardEnterCode(credential.serviceUrl, credential.token);
|
|
4050
|
+
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}
|
|
3942
4051
|
`);
|
|
3943
|
-
|
|
4052
|
+
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}
|
|
3944
4053
|
`);
|
|
3945
|
-
|
|
3946
|
-
|
|
4054
|
+
} catch {
|
|
4055
|
+
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");
|
|
4056
|
+
}
|
|
3947
4057
|
}
|
|
3948
4058
|
process.exit(0);
|
|
3949
4059
|
} catch (err) {
|
|
@@ -3988,13 +4098,14 @@ ${USAGE}`);
|
|
|
3988
4098
|
if (cmd === "build-record") {
|
|
3989
4099
|
const phaseArg = rest[0];
|
|
3990
4100
|
const repoRoot = rest.includes("--repo") ? rest[rest.indexOf("--repo") + 1] : process.cwd();
|
|
3991
|
-
if (
|
|
4101
|
+
if (phaseArg === void 0) {
|
|
3992
4102
|
process.stderr.write("halfcycle: usage: halfcycle build-record <phase> [--repo <root>]\n");
|
|
3993
4103
|
process.exit(1);
|
|
3994
4104
|
return;
|
|
3995
4105
|
}
|
|
3996
4106
|
const phaseId = phaseArg;
|
|
3997
4107
|
try {
|
|
4108
|
+
assertValidPhaseIdentity(phaseId, BUILD_RECORD_DIR);
|
|
3998
4109
|
const inputPath = join11(repoRoot, BUILD_RECORD_ZONE_B_DIR, `${renderPhaseSegment(phaseId)}.input.json`);
|
|
3999
4110
|
const input = JSON.parse(readFileSync9(inputPath, "utf-8"));
|
|
4000
4111
|
const result = assemblePhaseBuildRecord({
|