halfcycle 0.3.18 → 0.3.20
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 +189 -64
- package/dist/bin.js.map +3 -3
- package/dist/confirm-identity.d.ts +51 -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 +59 -10
- package/dist/own-engagement.d.ts.map +1 -1
- package/dist/resolve-credential.d.ts +17 -0
- package/dist/resolve-credential.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.20",
|
|
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);
|
|
@@ -2622,6 +2699,9 @@ function accountLabel(identity, storedAccountId, verbose2 = false) {
|
|
|
2622
2699
|
return `account ${accountId} (no email on record for it)`;
|
|
2623
2700
|
return "the account this credential belongs to, which this service did not name";
|
|
2624
2701
|
}
|
|
2702
|
+
function isInteractiveRun(env = process.env) {
|
|
2703
|
+
return process.stdin.isTTY === true && !isNonInteractive(env);
|
|
2704
|
+
}
|
|
2625
2705
|
function onlyDate(iso) {
|
|
2626
2706
|
if (iso === void 0)
|
|
2627
2707
|
return void 0;
|
|
@@ -2650,7 +2730,9 @@ function readDecision(answer) {
|
|
|
2650
2730
|
}
|
|
2651
2731
|
function actingAs(credential, identity) {
|
|
2652
2732
|
const accountId = identity?.accountId ?? credential.accountId;
|
|
2653
|
-
|
|
2733
|
+
if (accountId === void 0 && identity === void 0)
|
|
2734
|
+
return credential;
|
|
2735
|
+
return { ...credential, ...accountId !== void 0 ? { accountId } : {}, identity };
|
|
2654
2736
|
}
|
|
2655
2737
|
function question(colour) {
|
|
2656
2738
|
return `[halfcycle] Continue as that account? ${ink("good", "[y]", colour)} yes ${ink("key", "[s]", colour)} sign in as someone else ${ink("faint", "[anything else]", colour)} stop: `;
|
|
@@ -2713,6 +2795,10 @@ function armScript(credential, act, label, plainLabel, home) {
|
|
|
2713
2795
|
}
|
|
2714
2796
|
}
|
|
2715
2797
|
}
|
|
2798
|
+
function identitySwitchRemedy(home, interactive) {
|
|
2799
|
+
const setOrRemove = `set HALFCYCLE_TOKEN for that account, or remove ${accountStorePath(home)}`;
|
|
2800
|
+
return interactive ? `To act as a different account: run this again and answer "s" when asked, ${setOrRemove} to be asked for a sign-in next time.` : `To act as a different account: ${setOrRemove} and run this again with a terminal to sign in.`;
|
|
2801
|
+
}
|
|
2716
2802
|
async function confirmActingIdentity(serviceUrl, credential, act, deps = {}) {
|
|
2717
2803
|
const write = deps.write ?? ((text) => process.stdout.write(text));
|
|
2718
2804
|
const colour = deps.colour ?? decideColour({ isTTY: process.stdout.isTTY === true, env: deps.env ?? process.env });
|
|
@@ -2729,7 +2815,7 @@ async function confirmActingIdentity(serviceUrl, credential, act, deps = {}) {
|
|
|
2729
2815
|
`);
|
|
2730
2816
|
return acting;
|
|
2731
2817
|
}
|
|
2732
|
-
const interactive = deps.interactive ?? (
|
|
2818
|
+
const interactive = deps.interactive ?? isInteractiveRun(deps.env);
|
|
2733
2819
|
if (!interactive) {
|
|
2734
2820
|
if (script.noTerminalNote !== "")
|
|
2735
2821
|
write(script.noTerminalNote);
|
|
@@ -2778,13 +2864,19 @@ function deriveProjectName(targetRepoRoot) {
|
|
|
2778
2864
|
}
|
|
2779
2865
|
|
|
2780
2866
|
// dist/own-engagement.js
|
|
2867
|
+
function isOutcomeBRefusal(result) {
|
|
2868
|
+
return "outcome" in result && result.outcome === "not-this-account";
|
|
2869
|
+
}
|
|
2781
2870
|
async function createOwnedEngagement(serviceUrl, targetRepo, deps = {}) {
|
|
2782
2871
|
let credential = await confirmActingIdentity(serviceUrl, await obtainCredential(serviceUrl, deps), { kind: "create", targetRepo }, deps);
|
|
2783
2872
|
for (; ; ) {
|
|
2784
2873
|
try {
|
|
2785
2874
|
return {
|
|
2786
2875
|
...await createEngagement(serviceUrl, void 0, credential.credential, deriveProjectName(targetRepo)),
|
|
2787
|
-
actingAccountId: credential.accountId
|
|
2876
|
+
actingAccountId: credential.accountId,
|
|
2877
|
+
// Carried from `confirmActingIdentity`'s own fetch (`ResolvedCredential.identity`'s
|
|
2878
|
+
// docblock) — `bin.ts`'s final report reuses this instead of asking again.
|
|
2879
|
+
actingIdentity: credential.identity
|
|
2788
2880
|
};
|
|
2789
2881
|
} catch (err) {
|
|
2790
2882
|
if (!(err instanceof CreateEngagementRefused) || !err.authRefused)
|
|
@@ -2806,12 +2898,21 @@ async function joinPinnedEngagement(serviceUrl, engagementId, targetRepo, deps =
|
|
|
2806
2898
|
try {
|
|
2807
2899
|
return {
|
|
2808
2900
|
...await joinEngagement(serviceUrl, engagementId, credential.credential),
|
|
2809
|
-
actingAccountId: credential.accountId
|
|
2901
|
+
actingAccountId: credential.accountId,
|
|
2902
|
+
// Same reuse as the create path above — see its comment.
|
|
2903
|
+
actingIdentity: credential.identity
|
|
2810
2904
|
};
|
|
2811
2905
|
} catch (err) {
|
|
2812
2906
|
if (!(err instanceof CreateEngagementRefused))
|
|
2813
2907
|
throw err;
|
|
2814
2908
|
if (err.status === 403) {
|
|
2909
|
+
if (isNotThisAccountRefusal({ reason: err.reason })) {
|
|
2910
|
+
return {
|
|
2911
|
+
outcome: "not-this-account",
|
|
2912
|
+
message: err.serverMessage,
|
|
2913
|
+
actingAccountId: credential.accountId
|
|
2914
|
+
};
|
|
2915
|
+
}
|
|
2815
2916
|
throw new SignInRefused(`join-${err.status}`, `${err.serverMessage} ${JOIN_REFUSED_LOCAL_REMEDY}`);
|
|
2816
2917
|
}
|
|
2817
2918
|
if (err.status !== 401) {
|
|
@@ -3806,7 +3907,9 @@ ${USAGE}`);
|
|
|
3806
3907
|
const reusable = pinned !== null && pinned.engagementId === requestedId ? pinned.credential : void 0;
|
|
3807
3908
|
let engagementId;
|
|
3808
3909
|
let credential;
|
|
3910
|
+
let notThisAccountMessage;
|
|
3809
3911
|
let actingAccountId;
|
|
3912
|
+
let actingIdentity;
|
|
3810
3913
|
let foundLine = "";
|
|
3811
3914
|
if (reusable !== void 0 && pinned !== null) {
|
|
3812
3915
|
engagementId = pinned.engagementId;
|
|
@@ -3826,27 +3929,38 @@ ${USAGE}`);
|
|
|
3826
3929
|
}
|
|
3827
3930
|
} else if (requestedId !== void 0) {
|
|
3828
3931
|
const joined = await joinPinnedEngagement(serviceUrl, requestedId, targetRepo, { assumeYes, verbose });
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3932
|
+
if (isOutcomeBRefusal(joined)) {
|
|
3933
|
+
engagementId = requestedId;
|
|
3934
|
+
actingAccountId = joined.actingAccountId;
|
|
3935
|
+
credential = void 0;
|
|
3936
|
+
notThisAccountMessage = joined.message;
|
|
3937
|
+
foundLine = `this repo is pinned to a project that belongs to a different Halfcycle account`;
|
|
3938
|
+
writeNotThisAccountMarker(engagementId);
|
|
3939
|
+
} else {
|
|
3940
|
+
engagementId = joined.engagementId;
|
|
3941
|
+
actingAccountId = joined.actingAccountId;
|
|
3942
|
+
actingIdentity = joined.actingIdentity;
|
|
3943
|
+
credential = {
|
|
3944
|
+
serviceUrl,
|
|
3945
|
+
token: joined.sessionToken,
|
|
3946
|
+
mcpUrl: joined.mcpUrl,
|
|
3947
|
+
guardUrl: joined.guardUrl,
|
|
3948
|
+
// T-10 — optional; absent on a plane that has not configured
|
|
3949
|
+
// CONTROL_TELEMETRY_URL yet. `writeEngagementCredential` writes '' for
|
|
3950
|
+
// an absent value, never `undefined` verbatim.
|
|
3951
|
+
controlTelemetryUrl: joined.controlTelemetryUrl
|
|
3952
|
+
};
|
|
3953
|
+
foundLine = `this repo is pinned to a shared project \u2014 joining it as you (${shortId(joined.engagementId)})`;
|
|
3954
|
+
}
|
|
3955
|
+
if (verbose && credential !== void 0) {
|
|
3956
|
+
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
3957
|
`);
|
|
3845
3958
|
}
|
|
3846
3959
|
} else {
|
|
3847
3960
|
const created = await createOwnedEngagement(serviceUrl, targetRepo, { assumeYes, verbose });
|
|
3848
3961
|
engagementId = created.engagementId;
|
|
3849
3962
|
actingAccountId = created.actingAccountId;
|
|
3963
|
+
actingIdentity = created.actingIdentity;
|
|
3850
3964
|
credential = {
|
|
3851
3965
|
serviceUrl,
|
|
3852
3966
|
token: created.sessionToken,
|
|
@@ -3901,18 +4015,20 @@ ${USAGE}`);
|
|
|
3901
4015
|
[halfcycle] ${missing.length === 1 ? "it" : "them"}) and re-run, and the credential moves out of the tree.
|
|
3902
4016
|
`);
|
|
3903
4017
|
}
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
if (
|
|
3907
|
-
|
|
4018
|
+
if (credential) {
|
|
4019
|
+
const probe = await probeMcpOrigin(credential.mcpUrl);
|
|
4020
|
+
if (probe.reached) {
|
|
4021
|
+
if (verbose) {
|
|
4022
|
+
process.stdout.write(`[halfcycle] Method delivery (MCP): ${credential.mcpUrl} \u2014 reachable (${probe.serverName})
|
|
3908
4023
|
`);
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
4024
|
+
}
|
|
4025
|
+
} else {
|
|
4026
|
+
process.stdout.write(`[halfcycle] Method delivery (MCP): ${credential.mcpUrl} \u2014 NOT reachable: ${probe.problem}.
|
|
3912
4027
|
[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
4028
|
`);
|
|
4029
|
+
}
|
|
3914
4030
|
}
|
|
3915
|
-
if (verbose) {
|
|
4031
|
+
if (credential && verbose) {
|
|
3916
4032
|
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
4033
|
`);
|
|
3918
4034
|
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 +4036,38 @@ ${USAGE}`);
|
|
|
3920
4036
|
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
4037
|
`);
|
|
3922
4038
|
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.
|
|
4039
|
+
`);
|
|
4040
|
+
}
|
|
4041
|
+
if (credential === void 0 && notThisAccountMessage !== void 0) {
|
|
4042
|
+
process.stdout.write(`[halfcycle] ${notThisAccountMessage} 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(void 0, isInteractiveRun())}
|
|
3923
4043
|
`);
|
|
3924
4044
|
}
|
|
3925
4045
|
reportClaudeCodeVersion(verbose);
|
|
3926
|
-
const identity = await describeAccount(credential.serviceUrl, credential.token);
|
|
4046
|
+
const identity = actingIdentity ?? (credential ? await describeAccount(credential.serviceUrl, credential.token) : void 0);
|
|
3927
4047
|
const written = result.writtenPaths.length;
|
|
3928
4048
|
const merged = result.mergedPaths.length;
|
|
3929
4049
|
process.stdout.write(`[halfcycle] Signed in as ${accountLabel(identity, actingAccountId, verbose)}
|
|
3930
4050
|
`);
|
|
3931
4051
|
process.stdout.write(`[halfcycle] Found: ${foundLine}
|
|
3932
4052
|
`);
|
|
3933
|
-
process.stdout.write(`[halfcycle] Set up: ${written} file${written === 1 ? "" : "s"} written${merged > 0 ? ` (${merged} merged)` : ""}, guards armed on every edit
|
|
4053
|
+
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
4054
|
`);
|
|
3935
|
-
|
|
4055
|
+
if (credential) {
|
|
4056
|
+
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
4057
|
`);
|
|
3937
|
-
|
|
4058
|
+
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
4059
|
`);
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
4060
|
+
}
|
|
4061
|
+
if (credential) {
|
|
4062
|
+
try {
|
|
4063
|
+
const minted = await mintBoardEnterCode(credential.serviceUrl, credential.token);
|
|
4064
|
+
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
4065
|
`);
|
|
3943
|
-
|
|
4066
|
+
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
4067
|
`);
|
|
3945
|
-
|
|
3946
|
-
|
|
4068
|
+
} catch {
|
|
4069
|
+
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");
|
|
4070
|
+
}
|
|
3947
4071
|
}
|
|
3948
4072
|
process.exit(0);
|
|
3949
4073
|
} catch (err) {
|
|
@@ -3988,13 +4112,14 @@ ${USAGE}`);
|
|
|
3988
4112
|
if (cmd === "build-record") {
|
|
3989
4113
|
const phaseArg = rest[0];
|
|
3990
4114
|
const repoRoot = rest.includes("--repo") ? rest[rest.indexOf("--repo") + 1] : process.cwd();
|
|
3991
|
-
if (
|
|
4115
|
+
if (phaseArg === void 0) {
|
|
3992
4116
|
process.stderr.write("halfcycle: usage: halfcycle build-record <phase> [--repo <root>]\n");
|
|
3993
4117
|
process.exit(1);
|
|
3994
4118
|
return;
|
|
3995
4119
|
}
|
|
3996
4120
|
const phaseId = phaseArg;
|
|
3997
4121
|
try {
|
|
4122
|
+
assertValidPhaseIdentity(phaseId, BUILD_RECORD_DIR);
|
|
3998
4123
|
const inputPath = join11(repoRoot, BUILD_RECORD_ZONE_B_DIR, `${renderPhaseSegment(phaseId)}.input.json`);
|
|
3999
4124
|
const input = JSON.parse(readFileSync9(inputPath, "utf-8"));
|
|
4000
4125
|
const result = assemblePhaseBuildRecord({
|