halfcycle 0.4.18 → 0.5.1
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 +6 -3
- package/bin/bin.bundle.mjs +1558 -630
- package/bin/pre-tool-use-filter.sh +12 -0
- package/dist/banner-facts.d.ts +11 -1
- package/dist/banner-facts.d.ts.map +1 -1
- package/dist/bin.js +0 -0
- package/dist/check-tasks.d.ts +23 -11
- package/dist/check-tasks.d.ts.map +1 -1
- package/dist/claude-version-probe.d.ts +27 -3
- package/dist/claude-version-probe.d.ts.map +1 -1
- package/dist/cli.js +1439 -105
- package/dist/cli.js.map +3 -3
- package/dist/close-phase.d.ts.map +1 -1
- package/dist/codex-install-lines.d.ts +72 -0
- package/dist/codex-install-lines.d.ts.map +1 -0
- package/dist/doctor.d.ts +5 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/index.js +1077 -24
- package/dist/index.js.map +3 -3
- package/dist/install-manifest.d.ts +41 -0
- package/dist/install-manifest.d.ts.map +1 -1
- package/dist/install-plan.d.ts +5 -1
- package/dist/install-plan.d.ts.map +1 -1
- package/dist/install.d.ts +395 -10
- package/dist/install.d.ts.map +1 -1
- package/dist/merge-settings.d.ts +56 -0
- package/dist/merge-settings.d.ts.map +1 -1
- package/dist/node-floor.d.ts +12 -3
- package/dist/node-floor.d.ts.map +1 -1
- package/dist/review.d.ts +1 -1
- package/dist/review.d.ts.map +1 -1
- package/dist/toml-lines.d.ts +59 -0
- package/dist/toml-lines.d.ts.map +1 -0
- package/dist/uninstall.d.ts +3 -1
- package/dist/uninstall.d.ts.map +1 -1
- package/package.json +13 -12
package/dist/cli.js
CHANGED
|
@@ -493,6 +493,7 @@ var TASK_PLAN_FILE = "docs/build-records/task-plan.json";
|
|
|
493
493
|
var TASK_LIST_FILE = "docs/build-records/build-tasks.json";
|
|
494
494
|
var TASK_PLAN_TASKS_MAX = 500;
|
|
495
495
|
var TASK_PLAN_ID_MAX = 64;
|
|
496
|
+
var TASK_PLAN_BUILDS_NONE = "none";
|
|
496
497
|
var TASK_PLAN_KINDS = ["build", "review", "spec", "walk", "check"];
|
|
497
498
|
var TASK_PLAN_INTERFACE_KINDS = [
|
|
498
499
|
"route",
|
|
@@ -517,7 +518,8 @@ var taskPlanTaskSchema = z2.object({
|
|
|
517
518
|
interfaces: z2.array(planText).optional()
|
|
518
519
|
}).strict().optional(),
|
|
519
520
|
wholeProject: z2.literal(true).optional(),
|
|
520
|
-
ships: z2.enum(["deploy", "release"]).optional()
|
|
521
|
+
ships: z2.enum(["deploy", "release"]).optional(),
|
|
522
|
+
builds: z2.union([z2.literal(TASK_PLAN_BUILDS_NONE), z2.array(planText)]).optional()
|
|
521
523
|
}).strict().superRefine((task, ctx) => {
|
|
522
524
|
if (task.reviews !== void 0 && task.kind !== "review") {
|
|
523
525
|
ctx.addIssue({
|
|
@@ -564,6 +566,7 @@ var taskPlanLineText = {
|
|
|
564
566
|
O5: (difference) => `The task list in ${TASK_LIST_FILE} does not match the plan: ${difference}. Make the two agree, then run this again.`,
|
|
565
567
|
N1: (asked) => `The task list in ${TASK_LIST_FILE} is not for "${asked}", so it was not compared.`,
|
|
566
568
|
O6: (phase, asked) => `The plan is for "${phase}", not "${asked}".`,
|
|
569
|
+
O7: (a, file, partIds) => `"${a}" does not say which parts of the system it builds. Add "builds" with the ids of the parts it builds, from ${file}: ${partIds.join(", ")}. If it builds none of them, write "builds": "${TASK_PLAN_BUILDS_NONE}".`,
|
|
567
570
|
K1: (a) => `"${a}" is in no batch: no whole-project check waits for it. Add it to the dependsOn of the first whole-project check that it does not already wait for and that waits for every build task it waits for, or add a whole-project check after it.`,
|
|
568
571
|
K2: (a, b) => `"${a}" and "${b}" are both whole-project checks and neither waits for the other. Make the later one wait for the earlier.`,
|
|
569
572
|
K3: (b, a, c) => `"${b}" builds on "${a}" before "${c}" has checked it. Add "${c}" to the dependsOn of "${b}".`,
|
|
@@ -810,6 +813,12 @@ function refusal(code, text) {
|
|
|
810
813
|
function isBuildTask(task) {
|
|
811
814
|
return task.kind === "build" && ((task.touches?.files?.length ?? 0) > 0 || (task.touches?.interfaces?.length ?? 0) > 0);
|
|
812
815
|
}
|
|
816
|
+
function mustNameParts(task) {
|
|
817
|
+
return task.kind === "build" && task.heldBy === void 0 && task.ships === void 0;
|
|
818
|
+
}
|
|
819
|
+
function namesNoPart(task) {
|
|
820
|
+
return task.builds === void 0 || Array.isArray(task.builds) && task.builds.length === 0;
|
|
821
|
+
}
|
|
813
822
|
function isWholeProjectCheck(task) {
|
|
814
823
|
return task.kind === "check" && task.wholeProject === true;
|
|
815
824
|
}
|
|
@@ -944,6 +953,7 @@ function checkTaskPlan(plan, request) {
|
|
|
944
953
|
}
|
|
945
954
|
const tasks = plan.tasks;
|
|
946
955
|
const lines = [];
|
|
956
|
+
const system = request.system;
|
|
947
957
|
const ids = new Set(tasks.map((task) => task.id));
|
|
948
958
|
const seen = /* @__PURE__ */ new Set();
|
|
949
959
|
const reportedTwice = /* @__PURE__ */ new Set();
|
|
@@ -981,6 +991,9 @@ function checkTaskPlan(plan, request) {
|
|
|
981
991
|
}
|
|
982
992
|
}
|
|
983
993
|
}
|
|
994
|
+
if (system !== void 0 && system.partIds.length > 0 && mustNameParts(task) && namesNoPart(task)) {
|
|
995
|
+
lines.push(refusal("O7", taskPlanLineText.O7(task.id, system.file, system.partIds)));
|
|
996
|
+
}
|
|
984
997
|
}
|
|
985
998
|
const { before, cycles } = waitsFor(tasks);
|
|
986
999
|
for (const path of cycles)
|
|
@@ -1019,6 +1032,101 @@ function checkTaskPlan(plan, request) {
|
|
|
1019
1032
|
return { lines, refused: refused3 };
|
|
1020
1033
|
}
|
|
1021
1034
|
|
|
1035
|
+
// ../core/dist/agent-profile.js
|
|
1036
|
+
var AGENT_IDS = ["claude-code", "codex"];
|
|
1037
|
+
var CLAUDE_CODE_PROFILE = {
|
|
1038
|
+
id: "claude-code",
|
|
1039
|
+
settingsFiles: [SETTINGS_REL, LOCAL_SETTINGS_REL],
|
|
1040
|
+
wiring: HALFCYCLE_HOOK_WIRING,
|
|
1041
|
+
commands: HALFCYCLE_HOOK_COMMANDS,
|
|
1042
|
+
wiringPaths: HALFCYCLE_HOOK_WIRING_PATHS,
|
|
1043
|
+
writeCapableTools: WRITE_CAPABLE_TOOLS,
|
|
1044
|
+
projectRoot: "claude-project-dir",
|
|
1045
|
+
configFolder: "claude-config",
|
|
1046
|
+
mcpRegistration: { file: MCP_REGISTRATION_REL, serversKey: "mcpServers", serverKey: MCP_SERVER_KEY },
|
|
1047
|
+
tierModels: {
|
|
1048
|
+
deep: { model: "opus" },
|
|
1049
|
+
standard: { model: "sonnet" },
|
|
1050
|
+
fast: { model: "haiku" }
|
|
1051
|
+
}
|
|
1052
|
+
};
|
|
1053
|
+
var CODEX_HOOKS_REL = ".codex/hooks.json";
|
|
1054
|
+
var CODEX_CONFIG_REL = ".codex/config.toml";
|
|
1055
|
+
function codexHookCommand(script, args2) {
|
|
1056
|
+
const invocation = `cd "$(git rev-parse --show-toplevel)" && bash ./${script}`;
|
|
1057
|
+
return args2 === void 0 ? invocation : `${invocation} ${args2}`;
|
|
1058
|
+
}
|
|
1059
|
+
var CODEX_WINDOWS_NOT_SET_UP = "Halfcycle checks did not run because Halfcycle is not set up in this folder or any folder above it; run npx halfcycle in the project folder.";
|
|
1060
|
+
var CODEX_WINDOWS_NO_GIT_BASH = "Halfcycle checks did not run because Git for Windows was not found; install Git for Windows, then open a new session.";
|
|
1061
|
+
function codexHookCommandWindows(script, args2) {
|
|
1062
|
+
const tick = (text) => "`" + text + "`";
|
|
1063
|
+
const argv = [script, ...args2 === void 0 ? [] : args2.split(" ")].map(tick).join(",");
|
|
1064
|
+
const program = [
|
|
1065
|
+
"const p=require(`path`),f=require(`fs`),c=require(`child_process`);",
|
|
1066
|
+
"let d=process.cwd();",
|
|
1067
|
+
`for(;;){if(f.existsSync(p.join(d,${tick(VENDORED_BIN_REL)})))break;`,
|
|
1068
|
+
"const u=p.dirname(d);",
|
|
1069
|
+
"if(u===d){d=``;break}",
|
|
1070
|
+
"d=u}",
|
|
1071
|
+
`if(d===\`\`){console.error(${tick(CODEX_WINDOWS_NOT_SET_UP)})}else{`,
|
|
1072
|
+
"const g=c.spawnSync(`git`,[`--exec-path`],{encoding:`utf8`});",
|
|
1073
|
+
"const b=g.status===0?p.join(g.stdout.trim(),`..`,`..`,`..`,`bin`,`bash.exe`):``;",
|
|
1074
|
+
`if(b===\`\`||!f.existsSync(b)){console.error(${tick(CODEX_WINDOWS_NO_GIT_BASH)})}else{`,
|
|
1075
|
+
`const r=c.spawnSync(b,[${argv}],{cwd:d,stdio:\`inherit\`});`,
|
|
1076
|
+
"process.exitCode=typeof r.status===`number`?r.status:1}}"
|
|
1077
|
+
].join("");
|
|
1078
|
+
return `node -e '${program}'`;
|
|
1079
|
+
}
|
|
1080
|
+
function codexHookRow(event, matcher, timeout, script, args2) {
|
|
1081
|
+
return {
|
|
1082
|
+
event,
|
|
1083
|
+
matcher,
|
|
1084
|
+
timeout,
|
|
1085
|
+
command: codexHookCommand(script, args2),
|
|
1086
|
+
script,
|
|
1087
|
+
commandWindows: codexHookCommandWindows(script, args2)
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
var CODEX_WRITE_CAPABLE_TOOLS = ["apply_patch", "Edit", "Write"];
|
|
1091
|
+
var CODEX_PRE_TOOL_USE_MATCHER = "^(Bash|apply_patch)$";
|
|
1092
|
+
var CODEX_HOOK_WIRING = [
|
|
1093
|
+
codexHookRow("SessionStart", null, 5, SESSION_START_MARKER_REL),
|
|
1094
|
+
codexHookRow("SessionStart", null, 10, GUARD_RUNNER_REL, "refresh-credential --agent codex"),
|
|
1095
|
+
codexHookRow("UserPromptSubmit", null, 5, USER_PROMPT_REMINDER_REL),
|
|
1096
|
+
codexHookRow("UserPromptSubmit", null, 10, USER_PROMPT_SCOPE_REL, "--agent codex"),
|
|
1097
|
+
codexHookRow("UserPromptSubmit", null, 10, USER_PROMPT_FINDINGS_REL, "--agent codex"),
|
|
1098
|
+
codexHookRow("PreToolUse", CODEX_PRE_TOOL_USE_MATCHER, 10, GUARD_RUNNER_REL, "hook pre-tool-use --agent codex"),
|
|
1099
|
+
codexHookRow("PostToolUse", CODEX_WRITE_CAPABLE_TOOLS.join("|"), 60, GUARD_RUNNER_REL, "hook post-tool-use --agent codex"),
|
|
1100
|
+
codexHookRow("Stop", null, 60, GUARD_RUNNER_REL, "hook stop --agent codex"),
|
|
1101
|
+
codexHookRow("SubagentStop", null, 60, GUARD_RUNNER_REL, "hook subagent-stop --agent codex"),
|
|
1102
|
+
codexHookRow("SessionEnd", null, 3, SESSION_END_MARKER_REL)
|
|
1103
|
+
];
|
|
1104
|
+
var RETIRED_CODEX_HOOK_COMMANDS = [];
|
|
1105
|
+
function codexHookCommandSet(retired) {
|
|
1106
|
+
return /* @__PURE__ */ new Set([...CODEX_HOOK_WIRING.map((entry) => entry.command), ...retired]);
|
|
1107
|
+
}
|
|
1108
|
+
var CODEX_PROFILE = {
|
|
1109
|
+
id: "codex",
|
|
1110
|
+
settingsFiles: [CODEX_HOOKS_REL],
|
|
1111
|
+
wiring: CODEX_HOOK_WIRING,
|
|
1112
|
+
commands: codexHookCommandSet(RETIRED_CODEX_HOOK_COMMANDS),
|
|
1113
|
+
wiringPaths: [.../* @__PURE__ */ new Set([CODEX_HOOKS_REL, ...CODEX_HOOK_WIRING.map((entry) => entry.script), VENDORED_BIN_REL])],
|
|
1114
|
+
writeCapableTools: CODEX_WRITE_CAPABLE_TOOLS,
|
|
1115
|
+
projectRoot: "git-toplevel",
|
|
1116
|
+
configFolder: "codex-home",
|
|
1117
|
+
mcpRegistration: { file: CODEX_CONFIG_REL, serversKey: "mcp_servers", serverKey: MCP_SERVER_KEY },
|
|
1118
|
+
tierModels: {
|
|
1119
|
+
deep: { model: "gpt-6-luna", effort: "high" },
|
|
1120
|
+
standard: { model: "gpt-6-luna", effort: "medium" },
|
|
1121
|
+
fast: { model: "gpt-6-luna", effort: "low" }
|
|
1122
|
+
}
|
|
1123
|
+
};
|
|
1124
|
+
var AGENT_PROFILES = {
|
|
1125
|
+
"claude-code": CLAUDE_CODE_PROFILE,
|
|
1126
|
+
codex: CODEX_PROFILE
|
|
1127
|
+
};
|
|
1128
|
+
var ALL_HOOK_COMMANDS = new Set(AGENT_IDS.flatMap((id) => [...AGENT_PROFILES[id].commands]));
|
|
1129
|
+
|
|
1022
1130
|
// ../events/dist/result.js
|
|
1023
1131
|
import { z as z3 } from "zod";
|
|
1024
1132
|
var firedGuardSchema = z3.object({
|
|
@@ -2102,7 +2210,10 @@ var buildSnapshotReportResultSchema = z8.object({
|
|
|
2102
2210
|
// Which phase the graph was recorded against.
|
|
2103
2211
|
phase: buildPhaseSchema,
|
|
2104
2212
|
// When it was recorded.
|
|
2105
|
-
reportedAt: utcIso8601
|
|
2213
|
+
reportedAt: utcIso8601,
|
|
2214
|
+
// One sentence to pass on to the developer about the graph just stored. Absent
|
|
2215
|
+
// when there is nothing to say.
|
|
2216
|
+
warning: z8.string().min(1).max(400).optional()
|
|
2106
2217
|
}).strict();
|
|
2107
2218
|
|
|
2108
2219
|
// ../events/dist/phase-plan.js
|
|
@@ -5359,7 +5470,7 @@ function autonomyA13(accepted, carried) {
|
|
|
5359
5470
|
}
|
|
5360
5471
|
function autonomyA14(count, parts, acceptLine) {
|
|
5361
5472
|
const typed = acceptLine === null ? 'say "review what was built" and type the lines it gives.' : `say "review what was built", then type ${acceptLine} to accept them with those parts, or type the reject line it gives for a task's tests.`;
|
|
5362
|
-
return `The phase waits for you: ${String(count)} tests the agents said pass were not run by Halfcycle, so ${namesOf(parts)} wait for you too. Only you can accept or reject those tests. In your own
|
|
5473
|
+
return `The phase waits for you: ${String(count)} tests the agents said pass were not run by Halfcycle, so ${namesOf(parts)} wait for you too. Only you can accept or reject those tests. In your own coding agent's session, ` + typed;
|
|
5363
5474
|
}
|
|
5364
5475
|
function autonomyA15(count) {
|
|
5365
5476
|
return `The phase waits for you: ${String(count)} parts were built without an approved scope, and only you can decide them.`;
|
|
@@ -5863,7 +5974,14 @@ function serializeManifest(manifest) {
|
|
|
5863
5974
|
files: [...manifest.files].sort((a, b) => byString(a.path, b.path)),
|
|
5864
5975
|
createdDirs: [...manifest.createdDirs].sort(byString),
|
|
5865
5976
|
leftAlone: [...manifest.leftAlone].sort(byString),
|
|
5866
|
-
settings
|
|
5977
|
+
settings,
|
|
5978
|
+
...manifest.codexHooks !== void 0 ? {
|
|
5979
|
+
codexHooks: {
|
|
5980
|
+
...manifest.codexHooks,
|
|
5981
|
+
hookCommands: [...manifest.codexHooks.hookCommands].sort(byString),
|
|
5982
|
+
eventsCreated: [...manifest.codexHooks.eventsCreated].sort(byString)
|
|
5983
|
+
}
|
|
5984
|
+
} : {}
|
|
5867
5985
|
};
|
|
5868
5986
|
return JSON.stringify(canonicalJson2(ordered), null, 2) + "\n";
|
|
5869
5987
|
}
|
|
@@ -5917,7 +6035,7 @@ function parseManifest(text) {
|
|
|
5917
6035
|
}
|
|
5918
6036
|
if (!isRecord2(raw))
|
|
5919
6037
|
throw new UnreadableManifestError("it is not a JSON object");
|
|
5920
|
-
onlyKeys(raw, ["format", "files", "createdDirs", "leftAlone", "settings", "mcp", "gitignore", "gitattributes"], "the manifest");
|
|
6038
|
+
onlyKeys(raw, ["format", "files", "createdDirs", "leftAlone", "settings", "mcp", "codexHooks", "codexConfig", "gitignore", "gitattributes"], "the manifest");
|
|
5921
6039
|
if (raw["format"] !== INSTALL_MANIFEST_FORMAT) {
|
|
5922
6040
|
throw new UnreadableManifestError(`its format is not ${INSTALL_MANIFEST_FORMAT}`);
|
|
5923
6041
|
}
|
|
@@ -5983,6 +6101,30 @@ function parseManifest(text) {
|
|
|
5983
6101
|
...typeof m["entrySha256"] === "string" ? { entrySha256: m["entrySha256"] } : {}
|
|
5984
6102
|
};
|
|
5985
6103
|
}
|
|
6104
|
+
let codexHooks;
|
|
6105
|
+
if (raw["codexHooks"] !== void 0) {
|
|
6106
|
+
const c = raw["codexHooks"];
|
|
6107
|
+
if (!isRecord2(c))
|
|
6108
|
+
throw new UnreadableManifestError('"codexHooks" is not an object');
|
|
6109
|
+
onlyKeys(c, ["created", "adopted", "hookCommands", "eventsCreated", "hooksCreated"], '"codexHooks"');
|
|
6110
|
+
codexHooks = {
|
|
6111
|
+
created: bool(c["created"], "codexHooks.created"),
|
|
6112
|
+
adopted: bool(c["adopted"], "codexHooks.adopted"),
|
|
6113
|
+
hookCommands: stringArray(c["hookCommands"], "codexHooks.hookCommands"),
|
|
6114
|
+
eventsCreated: stringArray(c["eventsCreated"], "codexHooks.eventsCreated"),
|
|
6115
|
+
hooksCreated: bool(c["hooksCreated"], "codexHooks.hooksCreated")
|
|
6116
|
+
};
|
|
6117
|
+
}
|
|
6118
|
+
let codexConfig;
|
|
6119
|
+
if (raw["codexConfig"] !== void 0) {
|
|
6120
|
+
const c = raw["codexConfig"];
|
|
6121
|
+
if (!isRecord2(c))
|
|
6122
|
+
throw new UnreadableManifestError('"codexConfig" is not an object');
|
|
6123
|
+
onlyKeys(c, ["created", "block"], '"codexConfig"');
|
|
6124
|
+
if (typeof c["block"] !== "string")
|
|
6125
|
+
throw new UnreadableManifestError('"codexConfig.block" is not a string');
|
|
6126
|
+
codexConfig = { created: bool(c["created"], "codexConfig.created"), block: c["block"] };
|
|
6127
|
+
}
|
|
5986
6128
|
if (raw["gitignore"] === void 0)
|
|
5987
6129
|
throw new UnreadableManifestError('"gitignore" is missing');
|
|
5988
6130
|
const gitignore = appendedTextRecord(raw["gitignore"], "gitignore");
|
|
@@ -6001,6 +6143,8 @@ function parseManifest(text) {
|
|
|
6001
6143
|
leftAlone,
|
|
6002
6144
|
settings,
|
|
6003
6145
|
...mcp !== void 0 ? { mcp } : {},
|
|
6146
|
+
...codexHooks !== void 0 ? { codexHooks } : {},
|
|
6147
|
+
...codexConfig !== void 0 ? { codexConfig } : {},
|
|
6004
6148
|
gitignore,
|
|
6005
6149
|
...gitattributes !== void 0 ? { gitattributes } : {}
|
|
6006
6150
|
};
|
|
@@ -6018,6 +6162,8 @@ function startManifest(previous, isMember, isMemberDir) {
|
|
|
6018
6162
|
createdDirs: new Set((previous?.createdDirs ?? []).filter(isMemberDir)),
|
|
6019
6163
|
settings: previous?.settings,
|
|
6020
6164
|
mcp: previous?.mcp,
|
|
6165
|
+
codexHooks: previous?.codexHooks,
|
|
6166
|
+
codexConfig: previous?.codexConfig,
|
|
6021
6167
|
gitignore: previous?.gitignore,
|
|
6022
6168
|
gitattributes: previous?.gitattributes
|
|
6023
6169
|
};
|
|
@@ -6078,6 +6224,19 @@ function recordMcp(draft, observed) {
|
|
|
6078
6224
|
...entrySha256 !== void 0 ? { entrySha256 } : {}
|
|
6079
6225
|
};
|
|
6080
6226
|
}
|
|
6227
|
+
function recordCodexHooks(draft, observed) {
|
|
6228
|
+
const prev = draft.codexHooks;
|
|
6229
|
+
draft.codexHooks = prev === void 0 ? observed : {
|
|
6230
|
+
created: prev.created,
|
|
6231
|
+
adopted: prev.adopted,
|
|
6232
|
+
hookCommands: union(prev.hookCommands, observed.hookCommands),
|
|
6233
|
+
eventsCreated: union(prev.eventsCreated, observed.eventsCreated),
|
|
6234
|
+
hooksCreated: prev.hooksCreated
|
|
6235
|
+
};
|
|
6236
|
+
}
|
|
6237
|
+
function recordCodexConfig(draft, observed) {
|
|
6238
|
+
draft.codexConfig = { created: draft.codexConfig?.created ?? observed.created, block: observed.block };
|
|
6239
|
+
}
|
|
6081
6240
|
function recordGitignore(draft, created, appended) {
|
|
6082
6241
|
draft.gitignore = carryAppended(draft.gitignore, created, appended);
|
|
6083
6242
|
}
|
|
@@ -6111,6 +6270,8 @@ function finishManifest(draft) {
|
|
|
6111
6270
|
denyCreated: false
|
|
6112
6271
|
},
|
|
6113
6272
|
...draft.mcp !== void 0 ? { mcp: draft.mcp } : {},
|
|
6273
|
+
...draft.codexHooks !== void 0 ? { codexHooks: draft.codexHooks } : {},
|
|
6274
|
+
...draft.codexConfig !== void 0 ? { codexConfig: draft.codexConfig } : {},
|
|
6114
6275
|
gitignore: draft.gitignore ?? { created: false, appended: [] },
|
|
6115
6276
|
...draft.gitattributes !== void 0 ? { gitattributes: draft.gitattributes } : {}
|
|
6116
6277
|
};
|
|
@@ -6235,6 +6396,30 @@ var PREVIEW_WORDS = Object.freeze({
|
|
|
6235
6396
|
change: "takes out an older Halfcycle sign-in; your own lines stay as they are",
|
|
6236
6397
|
leave: "an older Halfcycle sign-in that cannot be moved yet; left as it is",
|
|
6237
6398
|
yours: YOURS
|
|
6399
|
+
},
|
|
6400
|
+
"codex-checks": {
|
|
6401
|
+
add: "Codex's checks, the same ones Halfcycle runs for Claude Code",
|
|
6402
|
+
change: "adds the checks Halfcycle needs; your own entries stay as they are",
|
|
6403
|
+
leave: "already has the checks Halfcycle needs",
|
|
6404
|
+
yours: YOURS
|
|
6405
|
+
},
|
|
6406
|
+
"codex-server": {
|
|
6407
|
+
add: "tells Codex how to reach Halfcycle's service",
|
|
6408
|
+
change: "adds Halfcycle's entry; your own entries stay as they are",
|
|
6409
|
+
leave: "already has Halfcycle's entry",
|
|
6410
|
+
yours: YOURS
|
|
6411
|
+
},
|
|
6412
|
+
skill: {
|
|
6413
|
+
add: "a skill your coding agent can run to take the next Halfcycle step",
|
|
6414
|
+
change: "a newer version of this Halfcycle skill",
|
|
6415
|
+
leave: SAME,
|
|
6416
|
+
yours: YOURS
|
|
6417
|
+
},
|
|
6418
|
+
"crew-profile": {
|
|
6419
|
+
add: "a member of your build crew, so your coding agent shows who is working",
|
|
6420
|
+
change: "a newer version of this build-crew member",
|
|
6421
|
+
leave: SAME,
|
|
6422
|
+
yours: YOURS
|
|
6238
6423
|
}
|
|
6239
6424
|
});
|
|
6240
6425
|
var kindTable;
|
|
@@ -6259,13 +6444,21 @@ function kinds() {
|
|
|
6259
6444
|
[RETIRED_MCP_HEADERS_HELPER_REL, "halfcycle-folder"],
|
|
6260
6445
|
[LEGACY_CI_STANZA_REL, "halfcycle-folder"],
|
|
6261
6446
|
[CAPTURED_INDEX_REL, "captured-index"],
|
|
6262
|
-
[LEGACY_ENV_LOCAL_REL, "old-sign-in"]
|
|
6447
|
+
[LEGACY_ENV_LOCAL_REL, "old-sign-in"],
|
|
6448
|
+
[CODEX_HOOKS_REL2, "codex-checks"],
|
|
6449
|
+
[CODEX_CONFIG_REL2, "codex-server"]
|
|
6263
6450
|
]);
|
|
6264
6451
|
return kindTable;
|
|
6265
6452
|
}
|
|
6266
6453
|
function previewKindOf(rel) {
|
|
6267
6454
|
if (/^\.claude\/commands\/[^/]+\.md$/.test(rel))
|
|
6268
6455
|
return "command";
|
|
6456
|
+
if (/^\.agents\/skills\/[^/]+\/SKILL\.md$/.test(rel))
|
|
6457
|
+
return "skill";
|
|
6458
|
+
if (/^\.claude\/agents\/[^/]+\.md$/.test(rel))
|
|
6459
|
+
return "crew-profile";
|
|
6460
|
+
if (/^\.codex\/agents\/[^/]+\.toml$/.test(rel))
|
|
6461
|
+
return "crew-profile";
|
|
6269
6462
|
return kinds().get(rel);
|
|
6270
6463
|
}
|
|
6271
6464
|
function kindOf(rel) {
|
|
@@ -6291,6 +6484,7 @@ function planInstall(options) {
|
|
|
6291
6484
|
const previous = readPreviousManifest(targetRepo);
|
|
6292
6485
|
const dirsBefore = new Set([...writeSet.dirs].filter((dir) => existsSync3(join5(targetRepo, dir))));
|
|
6293
6486
|
const { version, commands } = planCommandStubs(targetRepo, previous, writeSet);
|
|
6487
|
+
const codexSkills = planCodexSkills(targetRepo, previous, writeSet);
|
|
6294
6488
|
const vendored = planVendoredBinary(targetRepo);
|
|
6295
6489
|
let settings;
|
|
6296
6490
|
try {
|
|
@@ -6304,6 +6498,8 @@ function planInstall(options) {
|
|
|
6304
6498
|
const ciStanza = planLegacyCiStanzaRemoval(targetRepo);
|
|
6305
6499
|
const mcp = planMcpRegistration(targetRepo, preview ? "pending" : credential);
|
|
6306
6500
|
const mcpHelper = mcp.kind === "none" ? null : planMcpHeadersHelperRemoval(targetRepo);
|
|
6501
|
+
const codexHooks = planCodexHooks(targetRepo);
|
|
6502
|
+
const codexConfig = planCodexConfig(targetRepo, preview ? "pending" : credential);
|
|
6307
6503
|
const gitignore = planGitignore(targetRepo);
|
|
6308
6504
|
const gitattributes = planGitattributes(targetRepo);
|
|
6309
6505
|
const identity = planProjectIdentity(targetRepo);
|
|
@@ -6315,6 +6511,7 @@ function planInstall(options) {
|
|
|
6315
6511
|
const legacy = migrateLegacyEnvLocal(targetRepo, storedAfter);
|
|
6316
6512
|
const pin = preview ? planBundlePinPreview(targetRepo, version, home) : planBundlePin(targetRepo, version, engagementId, engagementType, createdByAccountId, credential?.methodVersion);
|
|
6317
6513
|
const crew = planCrewRoster(targetRepo);
|
|
6514
|
+
const crewProfiles = planCrewProfiles(targetRepo, previous, writeSet);
|
|
6318
6515
|
const scan = { text: planBootstrapScan(targetRepo).text };
|
|
6319
6516
|
const steps = {
|
|
6320
6517
|
targetRepo,
|
|
@@ -6327,12 +6524,15 @@ function planInstall(options) {
|
|
|
6327
6524
|
previous,
|
|
6328
6525
|
dirsBefore,
|
|
6329
6526
|
commands,
|
|
6527
|
+
codexSkills,
|
|
6330
6528
|
vendored,
|
|
6331
6529
|
settings,
|
|
6332
6530
|
hooks,
|
|
6333
6531
|
ciStanza,
|
|
6334
6532
|
mcp,
|
|
6335
6533
|
mcpHelper,
|
|
6534
|
+
codexHooks,
|
|
6535
|
+
codexConfig,
|
|
6336
6536
|
gitignore,
|
|
6337
6537
|
gitattributes,
|
|
6338
6538
|
identity,
|
|
@@ -6340,6 +6540,7 @@ function planInstall(options) {
|
|
|
6340
6540
|
legacy,
|
|
6341
6541
|
pin,
|
|
6342
6542
|
crew,
|
|
6543
|
+
crewProfiles,
|
|
6343
6544
|
scan
|
|
6344
6545
|
};
|
|
6345
6546
|
const record2 = preview ? null : planInstallRecord(targetRepo, accountInstall(steps, predicted(steps)).manifestText);
|
|
@@ -6365,16 +6566,24 @@ function predicted(steps) {
|
|
|
6365
6566
|
lands(planned.rel, planned.text, planned.outcome === "written");
|
|
6366
6567
|
};
|
|
6367
6568
|
steps.commands.forEach(file);
|
|
6569
|
+
steps.codexSkills.forEach(file);
|
|
6368
6570
|
file(steps.vendored);
|
|
6369
6571
|
file(steps.settings.file);
|
|
6370
6572
|
steps.hooks.forEach(file);
|
|
6371
6573
|
if (steps.mcp.kind === "write")
|
|
6372
6574
|
lands(MCP_REGISTRATION_REL, steps.mcp.text, steps.mcp.outcome !== "skipped");
|
|
6575
|
+
if (steps.codexHooks.outcome === "written" && steps.codexHooks.text !== null) {
|
|
6576
|
+
lands(CODEX_HOOKS_REL2, steps.codexHooks.text, true);
|
|
6577
|
+
}
|
|
6578
|
+
if (steps.codexConfig.outcome === "written" && steps.codexConfig.text !== null) {
|
|
6579
|
+
lands(CODEX_CONFIG_REL2, steps.codexConfig.text, true);
|
|
6580
|
+
}
|
|
6373
6581
|
if (steps.identity.after !== null)
|
|
6374
6582
|
lands(PROJECT_IDENTITY_REL, steps.identity.after, steps.identity.action === "add");
|
|
6375
6583
|
if (steps.pin !== null)
|
|
6376
6584
|
file(steps.pin);
|
|
6377
6585
|
file(steps.crew);
|
|
6586
|
+
steps.crewProfiles.forEach(file);
|
|
6378
6587
|
if (steps.scan.text !== null)
|
|
6379
6588
|
lands(BOOTSTRAP_STATE_REL, steps.scan.text, true);
|
|
6380
6589
|
const mergeAfter = (planned) => planned.outcome === "written" ? planned.after : planned.before;
|
|
@@ -6432,6 +6641,7 @@ function entriesOf(steps, record2) {
|
|
|
6432
6641
|
entries.push({ path: rel, action: there ? "change" : "add", kind: kindOf(rel), bytes: PENDING });
|
|
6433
6642
|
};
|
|
6434
6643
|
steps.commands.forEach(fromFile);
|
|
6644
|
+
steps.codexSkills.forEach(fromFile);
|
|
6435
6645
|
fromFile(steps.vendored);
|
|
6436
6646
|
const settings = steps.settings;
|
|
6437
6647
|
entries.push(settings.file.outcome === "written" ? { path: SETTINGS_REL, action: settings.preexisted ? "change" : "add", kind: "settings", bytes: settings.file.text } : { path: SETTINGS_REL, action: "leave", kind: "settings", bytes: null });
|
|
@@ -6447,6 +6657,38 @@ function entriesOf(steps, record2) {
|
|
|
6447
6657
|
entries.push({ path: MCP_REGISTRATION_REL, action: "leave", kind: "server-entry", bytes: null });
|
|
6448
6658
|
}
|
|
6449
6659
|
retired(RETIRED_MCP_HEADERS_HELPER_REL, steps.mcpHelper);
|
|
6660
|
+
const codexHooks = steps.codexHooks;
|
|
6661
|
+
if (codexHooks.outcome === "written") {
|
|
6662
|
+
entries.push({ path: CODEX_HOOKS_REL2, action: codexHooks.before === null ? "add" : "change", kind: "codex-checks", bytes: codexHooks.text });
|
|
6663
|
+
} else if (codexHooks.outcome === "current") {
|
|
6664
|
+
entries.push({ path: CODEX_HOOKS_REL2, action: "leave", kind: "codex-checks", bytes: null });
|
|
6665
|
+
} else {
|
|
6666
|
+
entries.push({ path: CODEX_HOOKS_REL2, action: "leave", kind: "codex-checks", bytes: null, yours: true });
|
|
6667
|
+
}
|
|
6668
|
+
const codexConfig = steps.codexConfig;
|
|
6669
|
+
switch (codexConfig.outcome) {
|
|
6670
|
+
case "written":
|
|
6671
|
+
entries.push({ path: CODEX_CONFIG_REL2, action: codexConfig.before === null ? "add" : "change", kind: "codex-server", bytes: codexConfig.text });
|
|
6672
|
+
break;
|
|
6673
|
+
case "pending":
|
|
6674
|
+
pending(CODEX_CONFIG_REL2);
|
|
6675
|
+
break;
|
|
6676
|
+
case "current":
|
|
6677
|
+
case "no-credential":
|
|
6678
|
+
entries.push({ path: CODEX_CONFIG_REL2, action: "leave", kind: "codex-server", bytes: null });
|
|
6679
|
+
break;
|
|
6680
|
+
case "conflict":
|
|
6681
|
+
case "link":
|
|
6682
|
+
case "refused-address":
|
|
6683
|
+
entries.push({
|
|
6684
|
+
path: CODEX_CONFIG_REL2,
|
|
6685
|
+
action: "leave",
|
|
6686
|
+
kind: "codex-server",
|
|
6687
|
+
bytes: null,
|
|
6688
|
+
...existsSync3(join5(targetRepo, CODEX_CONFIG_REL2)) ? { yours: true } : {}
|
|
6689
|
+
});
|
|
6690
|
+
break;
|
|
6691
|
+
}
|
|
6450
6692
|
merge(GITIGNORE_REL, steps.gitignore);
|
|
6451
6693
|
merge(GITATTRIBUTES_REL, steps.gitattributes);
|
|
6452
6694
|
const identity = steps.identity;
|
|
@@ -6469,6 +6711,7 @@ function entriesOf(steps, record2) {
|
|
|
6469
6711
|
else
|
|
6470
6712
|
fromFile(steps.pin);
|
|
6471
6713
|
fromFile(steps.crew);
|
|
6714
|
+
steps.crewProfiles.forEach(fromFile);
|
|
6472
6715
|
entries.push(steps.scan.text !== null ? { path: BOOTSTRAP_STATE_REL, action: "add", kind: "halfcycle-folder", bytes: steps.scan.text } : { path: BOOTSTRAP_STATE_REL, action: "leave", kind: "halfcycle-folder", bytes: null });
|
|
6473
6716
|
if (record2 === null)
|
|
6474
6717
|
pending(INSTALL_MANIFEST_REL);
|
|
@@ -6509,6 +6752,8 @@ function applyInstallPlan(plan) {
|
|
|
6509
6752
|
}
|
|
6510
6753
|
for (const command of steps.commands)
|
|
6511
6754
|
putPlannedFile(targetRepo, command);
|
|
6755
|
+
for (const skill of steps.codexSkills)
|
|
6756
|
+
putPlannedFile(targetRepo, skill);
|
|
6512
6757
|
putPlannedFile(targetRepo, steps.vendored);
|
|
6513
6758
|
putPlannedFile(targetRepo, steps.settings.file);
|
|
6514
6759
|
for (const hook of steps.hooks)
|
|
@@ -6516,6 +6761,8 @@ function applyInstallPlan(plan) {
|
|
|
6516
6761
|
const ciStanza = steps.ciStanza === "removed" ? removePlannedRetiredFile(targetRepo, LEGACY_CI_STANZA_REL) : steps.ciStanza;
|
|
6517
6762
|
putPlannedMcp(targetRepo, steps.mcp);
|
|
6518
6763
|
const mcpHelper = steps.mcpHelper === "removed" ? removePlannedRetiredFile(targetRepo, RETIRED_MCP_HEADERS_HELPER_REL) : steps.mcpHelper;
|
|
6764
|
+
putPlannedCodexHooks(targetRepo, steps.codexHooks);
|
|
6765
|
+
putPlannedCodexConfig(targetRepo, steps.codexConfig);
|
|
6519
6766
|
const gitignore = putPlannedGitignore(targetRepo, steps.gitignore);
|
|
6520
6767
|
const gitignorePath = join5(targetRepo, GITIGNORE_REL);
|
|
6521
6768
|
const gitignoreAfter = existsSync3(gitignorePath) ? readFileSync5(gitignorePath, "utf-8") : null;
|
|
@@ -6528,6 +6775,8 @@ function applyInstallPlan(plan) {
|
|
|
6528
6775
|
const legacy = applyLegacyMigration(targetRepo, migrateLegacyEnvLocal(targetRepo, stored));
|
|
6529
6776
|
writeBundlePin(targetRepo, pin, []);
|
|
6530
6777
|
putPlannedFile(targetRepo, steps.crew);
|
|
6778
|
+
for (const profile of steps.crewProfiles)
|
|
6779
|
+
putPlannedFile(targetRepo, profile);
|
|
6531
6780
|
if (steps.scan.text !== null)
|
|
6532
6781
|
writeBootstrapState(targetRepo, steps.scan.text);
|
|
6533
6782
|
const { report, installManifest, manifestText } = accountInstall(steps, {
|
|
@@ -6557,9 +6806,15 @@ function applyInstallPlan(plan) {
|
|
|
6557
6806
|
replacedPaths: report.replacedPaths,
|
|
6558
6807
|
bootstrapScanRan: steps.scan.text !== null,
|
|
6559
6808
|
legacyCredentialKept: legacy.outcome === "kept" ? { keys: legacy.unbacked, missing: legacy.missingForAdoption } : null,
|
|
6560
|
-
notShared
|
|
6809
|
+
notShared,
|
|
6810
|
+
codex: { hooks: steps.codexHooks.outcome, config: appliedCodexConfig(steps.codexConfig.outcome) }
|
|
6561
6811
|
};
|
|
6562
6812
|
}
|
|
6813
|
+
function appliedCodexConfig(outcome) {
|
|
6814
|
+
if (outcome === "pending")
|
|
6815
|
+
throw new Error("[halfcycle] a preview cannot be applied: it is still waiting for the service.");
|
|
6816
|
+
return outcome;
|
|
6817
|
+
}
|
|
6563
6818
|
|
|
6564
6819
|
// dist/mcp-endpoint.js
|
|
6565
6820
|
var MCP_ENDPOINT_PATH = "/mcp";
|
|
@@ -6570,8 +6825,8 @@ function mcpEndpointUrl(origin) {
|
|
|
6570
6825
|
// dist/node-floor.js
|
|
6571
6826
|
var NODE_FLOOR_MAJOR = 18;
|
|
6572
6827
|
var NODE_FLOOR_SENTENCE_HEAD = `Halfcycle needs Node ${NODE_FLOOR_MAJOR} or newer; you have `;
|
|
6573
|
-
function nodeFloorProgram(guarded) {
|
|
6574
|
-
return `if(+process.versions.node.split('.')[0]<${NODE_FLOOR_MAJOR}){console.error('${NODE_FLOOR_SENTENCE_HEAD}'+process.versions.node)
|
|
6828
|
+
function nodeFloorProgram(guarded, belowFloor = "process.exitCode=1") {
|
|
6829
|
+
return `if(+process.versions.node.split('.')[0]<${NODE_FLOOR_MAJOR}){console.error('${NODE_FLOOR_SENTENCE_HEAD}'+process.versions.node);` + belowFloor + `}else{${guarded}}`;
|
|
6575
6830
|
}
|
|
6576
6831
|
|
|
6577
6832
|
// dist/merge-settings.js
|
|
@@ -6618,6 +6873,75 @@ function mergeSettings(existing, generated, retiredOurs, statusLine) {
|
|
|
6618
6873
|
}
|
|
6619
6874
|
return merged;
|
|
6620
6875
|
}
|
|
6876
|
+
function isPlainRecord(value) {
|
|
6877
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
6878
|
+
}
|
|
6879
|
+
function mergeCodexHooks(existing, generated, ours) {
|
|
6880
|
+
const existingHooks = isPlainRecord(existing["hooks"]) ? existing["hooks"] : {};
|
|
6881
|
+
const events = [
|
|
6882
|
+
...Object.keys(existingHooks),
|
|
6883
|
+
...Object.keys(generated.hooks).filter((event) => !(event in existingHooks))
|
|
6884
|
+
];
|
|
6885
|
+
const mergedHooks = {};
|
|
6886
|
+
for (const event of events) {
|
|
6887
|
+
const rows = generated.hooks[event] ?? [];
|
|
6888
|
+
const current = existingHooks[event];
|
|
6889
|
+
if (current === void 0) {
|
|
6890
|
+
mergedHooks[event] = rows.map(cloneGroup);
|
|
6891
|
+
continue;
|
|
6892
|
+
}
|
|
6893
|
+
if (!Array.isArray(current)) {
|
|
6894
|
+
mergedHooks[event] = current;
|
|
6895
|
+
continue;
|
|
6896
|
+
}
|
|
6897
|
+
const wanted = new Map(rows.map((row) => [row.hooks[0].command, row]));
|
|
6898
|
+
const placed = /* @__PURE__ */ new Set();
|
|
6899
|
+
const next = [];
|
|
6900
|
+
for (const group of current) {
|
|
6901
|
+
if (!isPlainRecord(group) || !Array.isArray(group["hooks"]) || !isHalfcycleEntry({ hooks: group["hooks"] }, ours)) {
|
|
6902
|
+
next.push(group);
|
|
6903
|
+
continue;
|
|
6904
|
+
}
|
|
6905
|
+
const hooks = group["hooks"];
|
|
6906
|
+
if (hooks.length === 1) {
|
|
6907
|
+
const command = hooks[0].command;
|
|
6908
|
+
const row = wanted.get(command);
|
|
6909
|
+
if (row !== void 0 && !placed.has(command)) {
|
|
6910
|
+
placed.add(command);
|
|
6911
|
+
next.push(cloneGroup(row));
|
|
6912
|
+
}
|
|
6913
|
+
continue;
|
|
6914
|
+
}
|
|
6915
|
+
const kept = [];
|
|
6916
|
+
for (const hook of hooks) {
|
|
6917
|
+
if (!isHalfcycleCommand(hook, ours)) {
|
|
6918
|
+
kept.push(hook);
|
|
6919
|
+
continue;
|
|
6920
|
+
}
|
|
6921
|
+
const command = hook.command;
|
|
6922
|
+
const row = wanted.get(command);
|
|
6923
|
+
if (row !== void 0 && !placed.has(command) && group["matcher"] === row.matcher) {
|
|
6924
|
+
placed.add(command);
|
|
6925
|
+
kept.push({ ...row.hooks[0] });
|
|
6926
|
+
}
|
|
6927
|
+
}
|
|
6928
|
+
if (kept.length > 0)
|
|
6929
|
+
next.push({ ...group, hooks: kept });
|
|
6930
|
+
}
|
|
6931
|
+
for (const row of rows) {
|
|
6932
|
+
if (!placed.has(row.hooks[0].command))
|
|
6933
|
+
next.push(cloneGroup(row));
|
|
6934
|
+
}
|
|
6935
|
+
mergedHooks[event] = next;
|
|
6936
|
+
}
|
|
6937
|
+
return { ...existing, hooks: mergedHooks };
|
|
6938
|
+
}
|
|
6939
|
+
function cloneGroup(group) {
|
|
6940
|
+
return {
|
|
6941
|
+
...group.matcher === void 0 ? {} : { matcher: group.matcher },
|
|
6942
|
+
hooks: group.hooks.map((hook) => ({ ...hook }))
|
|
6943
|
+
};
|
|
6944
|
+
}
|
|
6621
6945
|
function mergePermissions(existing, generated, retiredOurs) {
|
|
6622
6946
|
if (existing === void 0 && generated === void 0)
|
|
6623
6947
|
return void 0;
|
|
@@ -6640,6 +6964,168 @@ function mergePermissions(existing, generated, retiredOurs) {
|
|
|
6640
6964
|
return result;
|
|
6641
6965
|
}
|
|
6642
6966
|
|
|
6967
|
+
// dist/toml-lines.js
|
|
6968
|
+
var BARE_KEY_CHAR = /[A-Za-z0-9_-]/;
|
|
6969
|
+
var Unreadable = class extends Error {
|
|
6970
|
+
};
|
|
6971
|
+
function skipBlank(line, i) {
|
|
6972
|
+
while (i < line.length && (line[i] === " " || line[i] === " "))
|
|
6973
|
+
i += 1;
|
|
6974
|
+
return i;
|
|
6975
|
+
}
|
|
6976
|
+
function readKey(line, start) {
|
|
6977
|
+
const segments = [];
|
|
6978
|
+
let i = start;
|
|
6979
|
+
for (; ; ) {
|
|
6980
|
+
i = skipBlank(line, i);
|
|
6981
|
+
const c = line[i];
|
|
6982
|
+
if (c === '"') {
|
|
6983
|
+
const close = line.indexOf('"', i + 1);
|
|
6984
|
+
if (close < 0)
|
|
6985
|
+
throw new Unreadable();
|
|
6986
|
+
const segment = line.slice(i + 1, close);
|
|
6987
|
+
if (segment.includes("\\"))
|
|
6988
|
+
throw new Unreadable();
|
|
6989
|
+
segments.push(segment);
|
|
6990
|
+
i = close + 1;
|
|
6991
|
+
} else if (c === "'") {
|
|
6992
|
+
const close = line.indexOf("'", i + 1);
|
|
6993
|
+
if (close < 0)
|
|
6994
|
+
throw new Unreadable();
|
|
6995
|
+
segments.push(line.slice(i + 1, close));
|
|
6996
|
+
i = close + 1;
|
|
6997
|
+
} else {
|
|
6998
|
+
let j = i;
|
|
6999
|
+
while (j < line.length && BARE_KEY_CHAR.test(line[j]))
|
|
7000
|
+
j += 1;
|
|
7001
|
+
if (j === i)
|
|
7002
|
+
throw new Unreadable();
|
|
7003
|
+
segments.push(line.slice(i, j));
|
|
7004
|
+
i = j;
|
|
7005
|
+
}
|
|
7006
|
+
i = skipBlank(line, i);
|
|
7007
|
+
if (line[i] !== ".")
|
|
7008
|
+
return { segments, end: i };
|
|
7009
|
+
i += 1;
|
|
7010
|
+
}
|
|
7011
|
+
}
|
|
7012
|
+
function readValue(line, start, state) {
|
|
7013
|
+
let i = start;
|
|
7014
|
+
while (i < line.length) {
|
|
7015
|
+
if (state.multiline !== null) {
|
|
7016
|
+
const delimiter3 = state.multiline;
|
|
7017
|
+
const escapes = delimiter3 === '"""';
|
|
7018
|
+
while (i < line.length) {
|
|
7019
|
+
if (escapes && line[i] === "\\") {
|
|
7020
|
+
i += 2;
|
|
7021
|
+
continue;
|
|
7022
|
+
}
|
|
7023
|
+
if (line.startsWith(delimiter3, i)) {
|
|
7024
|
+
i += 3;
|
|
7025
|
+
let extra = 0;
|
|
7026
|
+
while (extra < 2 && line[i] === delimiter3[0]) {
|
|
7027
|
+
i += 1;
|
|
7028
|
+
extra += 1;
|
|
7029
|
+
}
|
|
7030
|
+
state.multiline = null;
|
|
7031
|
+
break;
|
|
7032
|
+
}
|
|
7033
|
+
i += 1;
|
|
7034
|
+
}
|
|
7035
|
+
continue;
|
|
7036
|
+
}
|
|
7037
|
+
const c = line[i];
|
|
7038
|
+
if (c === "#")
|
|
7039
|
+
return;
|
|
7040
|
+
if (c === '"' || c === "'") {
|
|
7041
|
+
const triple = c.repeat(3);
|
|
7042
|
+
if (line.startsWith(triple, i)) {
|
|
7043
|
+
state.multiline = triple;
|
|
7044
|
+
i += 3;
|
|
7045
|
+
continue;
|
|
7046
|
+
}
|
|
7047
|
+
let j = i + 1;
|
|
7048
|
+
for (; ; ) {
|
|
7049
|
+
if (j >= line.length)
|
|
7050
|
+
throw new Unreadable();
|
|
7051
|
+
if (c === '"' && line[j] === "\\") {
|
|
7052
|
+
j += 2;
|
|
7053
|
+
continue;
|
|
7054
|
+
}
|
|
7055
|
+
if (line[j] === c)
|
|
7056
|
+
break;
|
|
7057
|
+
j += 1;
|
|
7058
|
+
}
|
|
7059
|
+
i = j + 1;
|
|
7060
|
+
continue;
|
|
7061
|
+
}
|
|
7062
|
+
if (c === "[" || c === "{")
|
|
7063
|
+
state.depth += 1;
|
|
7064
|
+
else if (c === "]" || c === "}") {
|
|
7065
|
+
state.depth -= 1;
|
|
7066
|
+
if (state.depth < 0)
|
|
7067
|
+
throw new Unreadable();
|
|
7068
|
+
}
|
|
7069
|
+
i += 1;
|
|
7070
|
+
}
|
|
7071
|
+
}
|
|
7072
|
+
function scanTomlStatements(lines, skip) {
|
|
7073
|
+
const statements = [];
|
|
7074
|
+
const state = { multiline: null, depth: 0 };
|
|
7075
|
+
let table = [];
|
|
7076
|
+
let index = 0;
|
|
7077
|
+
try {
|
|
7078
|
+
for (index = 0; index < lines.length; index += 1) {
|
|
7079
|
+
const inValue = state.multiline !== null || state.depth > 0;
|
|
7080
|
+
if (skip !== void 0 && index === skip.from) {
|
|
7081
|
+
if (inValue)
|
|
7082
|
+
throw new Unreadable();
|
|
7083
|
+
table = skip.tableAfter;
|
|
7084
|
+
index = skip.to;
|
|
7085
|
+
continue;
|
|
7086
|
+
}
|
|
7087
|
+
let line = lines[index];
|
|
7088
|
+
if (index === 0 && line.startsWith("\uFEFF"))
|
|
7089
|
+
line = line.slice(1);
|
|
7090
|
+
if (inValue) {
|
|
7091
|
+
readValue(line, 0, state);
|
|
7092
|
+
continue;
|
|
7093
|
+
}
|
|
7094
|
+
let i = skipBlank(line, 0);
|
|
7095
|
+
if (i >= line.length || line[i] === "#")
|
|
7096
|
+
continue;
|
|
7097
|
+
if (line[i] === "[") {
|
|
7098
|
+
const array = line[i + 1] === "[";
|
|
7099
|
+
i += array ? 2 : 1;
|
|
7100
|
+
const key2 = readKey(line, i);
|
|
7101
|
+
i = key2.end;
|
|
7102
|
+
if (line[i] !== "]" || array && line[i + 1] !== "]")
|
|
7103
|
+
throw new Unreadable();
|
|
7104
|
+
i = skipBlank(line, i + (array ? 2 : 1));
|
|
7105
|
+
if (i < line.length && line[i] !== "#")
|
|
7106
|
+
throw new Unreadable();
|
|
7107
|
+
table = key2.segments;
|
|
7108
|
+
statements.push({ line: index, kind: array ? "array-table" : "table", path: key2.segments });
|
|
7109
|
+
continue;
|
|
7110
|
+
}
|
|
7111
|
+
const key = readKey(line, i);
|
|
7112
|
+
if (line[key.end] !== "=")
|
|
7113
|
+
throw new Unreadable();
|
|
7114
|
+
statements.push({ line: index, kind: "key", path: [...table, ...key.segments] });
|
|
7115
|
+
readValue(line, key.end + 1, state);
|
|
7116
|
+
}
|
|
7117
|
+
if (state.multiline !== null || state.depth > 0) {
|
|
7118
|
+
index = lines.length - 1;
|
|
7119
|
+
throw new Unreadable();
|
|
7120
|
+
}
|
|
7121
|
+
} catch (err) {
|
|
7122
|
+
if (err instanceof Unreadable)
|
|
7123
|
+
return { ok: false, line: index };
|
|
7124
|
+
throw err;
|
|
7125
|
+
}
|
|
7126
|
+
return { ok: true, statements };
|
|
7127
|
+
}
|
|
7128
|
+
|
|
6643
7129
|
// dist/install.js
|
|
6644
7130
|
function renderNotShared(finding) {
|
|
6645
7131
|
const rule = `rule "${finding.pattern}" in ${finding.source}, line ${finding.line}`;
|
|
@@ -6649,6 +7135,97 @@ function renderNotShared(finding) {
|
|
|
6649
7135
|
return `[halfcycle] Not shared: git ignores ${finding.path} (${rule}) and ${finding.others} other file${finding.others === 1 ? "" : "s"} Halfcycle set up here, so they will not reach anyone else who gets a copy of this project.`;
|
|
6650
7136
|
}
|
|
6651
7137
|
var ZONE_B_DIR = ".workbench";
|
|
7138
|
+
var CODEX_HOOKS_REL2 = AGENT_PROFILES.codex.settingsFiles[0];
|
|
7139
|
+
var CODEX_CONFIG_REL2 = AGENT_PROFILES.codex.mcpRegistration.file;
|
|
7140
|
+
var CLAUDE_AGENTS_DIR = ".claude/agents";
|
|
7141
|
+
var CODEX_AGENTS_DIR = ".codex/agents";
|
|
7142
|
+
var CREW_CALLSIGN_PATTERN = /^[A-Z][A-Z0-9]*$/;
|
|
7143
|
+
function crewProfileRel(agent, callsign) {
|
|
7144
|
+
if (!CREW_CALLSIGN_PATTERN.test(callsign)) {
|
|
7145
|
+
throw new Error(`[halfcycle] crew callsign "${callsign}" is not a valid profile name (must match ^[A-Z][A-Z0-9]*$).`);
|
|
7146
|
+
}
|
|
7147
|
+
return agent === "claude-code" ? `${CLAUDE_AGENTS_DIR}/${callsign}.md` : `${CODEX_AGENTS_DIR}/${callsign.toLowerCase()}.toml`;
|
|
7148
|
+
}
|
|
7149
|
+
function crewProfileTarget(targetRepo, rel) {
|
|
7150
|
+
const folder = rel.startsWith(`${CLAUDE_AGENTS_DIR}/`) ? CLAUDE_AGENTS_DIR : CODEX_AGENTS_DIR;
|
|
7151
|
+
const target = resolve(join6(targetRepo, rel));
|
|
7152
|
+
if (dirname3(target) !== resolve(targetRepo, folder)) {
|
|
7153
|
+
throw new Error(`[halfcycle] crew profile "${rel}" does not sit directly in ${folder}.`);
|
|
7154
|
+
}
|
|
7155
|
+
return target;
|
|
7156
|
+
}
|
|
7157
|
+
var CREW_ROLE_WORDS = {
|
|
7158
|
+
"business-analyst": "business analyst",
|
|
7159
|
+
architect: "architect",
|
|
7160
|
+
"spec-writer": "spec writer",
|
|
7161
|
+
builder: "builder",
|
|
7162
|
+
"context-docs": "context and docs writer",
|
|
7163
|
+
"spec-reviewer": "spec reviewer",
|
|
7164
|
+
"code-test-reviewer": "code and test reviewer",
|
|
7165
|
+
"delivery-lead": "delivery lead",
|
|
7166
|
+
"quality-lead": "quality lead"
|
|
7167
|
+
};
|
|
7168
|
+
var CREW_TIER_WORDS = {
|
|
7169
|
+
deep: "You think carefully and deeply before you act, and take the time the work needs.",
|
|
7170
|
+
standard: "You think each step through at a steady pace before you act.",
|
|
7171
|
+
fast: "You work quickly on clear, well-defined work, and say so when a task needs more thought."
|
|
7172
|
+
};
|
|
7173
|
+
var CREW_REVIEWER_WORDS = "You check the work you are given and never change it: you read it, run what you need to, and report what you find.";
|
|
7174
|
+
var CREW_REVIEWER_TOOLS = "Read, Grep, Glob, Bash";
|
|
7175
|
+
var CREW_MEMBER_TOOLS = "Read, Edit, Write, Bash, Grep, Glob";
|
|
7176
|
+
function crewProfileBody(role, group, tier) {
|
|
7177
|
+
const lines = [`You are a ${CREW_ROLE_WORDS[role]} on this project's build crew.`, CREW_TIER_WORDS[tier]];
|
|
7178
|
+
if (group === "reviewer")
|
|
7179
|
+
lines.push(CREW_REVIEWER_WORDS);
|
|
7180
|
+
return `${lines.join("\n")}
|
|
7181
|
+
`;
|
|
7182
|
+
}
|
|
7183
|
+
function crewProfileDescription(member) {
|
|
7184
|
+
return `${member.callsign}, ${CREW_ROLE_WORDS[member.role]} (${member.discipline}), a member of this project's build crew.`;
|
|
7185
|
+
}
|
|
7186
|
+
function plainYamlValue(value) {
|
|
7187
|
+
if (/[\r\n]|: | #|^[-?:,[\]{}#&*!|>'"%@`\s]|\s$/.test(value)) {
|
|
7188
|
+
throw new Error(`[halfcycle] a crew profile value cannot be written as plain text: ${JSON.stringify(value)}`);
|
|
7189
|
+
}
|
|
7190
|
+
return value;
|
|
7191
|
+
}
|
|
7192
|
+
function claudeCrewProfile(member) {
|
|
7193
|
+
const tools = member.group === "reviewer" ? CREW_REVIEWER_TOOLS : CREW_MEMBER_TOOLS;
|
|
7194
|
+
return [
|
|
7195
|
+
"---",
|
|
7196
|
+
`name: ${plainYamlValue(member.callsign)}`,
|
|
7197
|
+
`description: ${plainYamlValue(crewProfileDescription(member))}`,
|
|
7198
|
+
`model: ${plainYamlValue(AGENT_PROFILES["claude-code"].tierModels[member.tier].model)}`,
|
|
7199
|
+
`tools: ${tools}`,
|
|
7200
|
+
"---",
|
|
7201
|
+
"",
|
|
7202
|
+
crewProfileBody(member.role, member.group, member.tier)
|
|
7203
|
+
].join("\n");
|
|
7204
|
+
}
|
|
7205
|
+
function codexCrewProfile(member) {
|
|
7206
|
+
const { model, effort } = AGENT_PROFILES.codex.tierModels[member.tier];
|
|
7207
|
+
if (effort === void 0)
|
|
7208
|
+
throw new Error(`[halfcycle] Codex's ${member.tier} tier names no reasoning effort.`);
|
|
7209
|
+
const body = crewProfileBody(member.role, member.group, member.tier);
|
|
7210
|
+
if (/[\\"]|[\u0000-\u0009\u000b-\u001f\u007f]/.test(body)) {
|
|
7211
|
+
throw new Error("[halfcycle] a crew profile body holds a character a Codex role cannot carry as written.");
|
|
7212
|
+
}
|
|
7213
|
+
return [
|
|
7214
|
+
`name = ${tomlBasicString(member.callsign.toLowerCase())}`,
|
|
7215
|
+
`description = ${tomlBasicString(crewProfileDescription(member))}`,
|
|
7216
|
+
`model = ${tomlBasicString(model)}`,
|
|
7217
|
+
`model_reasoning_effort = ${tomlBasicString(effort)}`,
|
|
7218
|
+
`developer_instructions = """`,
|
|
7219
|
+
`${body}"""`,
|
|
7220
|
+
""
|
|
7221
|
+
].join("\n");
|
|
7222
|
+
}
|
|
7223
|
+
function renderCrewProfiles(roster = CREW_ROSTER) {
|
|
7224
|
+
return [
|
|
7225
|
+
...roster.map((member) => ({ rel: crewProfileRel("claude-code", member.callsign), text: claudeCrewProfile(member) })),
|
|
7226
|
+
...roster.filter((member) => member.group !== "reviewer").map((member) => ({ rel: crewProfileRel("codex", member.callsign), text: codexCrewProfile(member) }))
|
|
7227
|
+
];
|
|
7228
|
+
}
|
|
6652
7229
|
var __filename = fileURLToPath(import.meta.url);
|
|
6653
7230
|
var __dirname = dirname3(__filename);
|
|
6654
7231
|
function findBundleRoot(startDir) {
|
|
@@ -6700,13 +7277,23 @@ var WRITE_ALLOWLIST = [
|
|
|
6700
7277
|
"test/fixtures/captured",
|
|
6701
7278
|
MCP_REGISTRATION_REL,
|
|
6702
7279
|
".gitattributes",
|
|
6703
|
-
ZONE_B_DIR
|
|
7280
|
+
ZONE_B_DIR,
|
|
7281
|
+
// Codex's twin of '.claude/commands' (`codex-install-target` AC4, T-14): one
|
|
7282
|
+
// `.agents/skills/<name>/SKILL.md` per command, same collision rule.
|
|
7283
|
+
".agents/skills",
|
|
7284
|
+
// Codex's checks file, merged beside a developer's own entries.
|
|
7285
|
+
CODEX_HOOKS_REL2,
|
|
7286
|
+
// Codex's settings file: one marked block is added, and the rest of the file is left as it is.
|
|
7287
|
+
CODEX_CONFIG_REL2,
|
|
7288
|
+
// Each crew member's own profile, by its exact name; no other file in those folders is written.
|
|
7289
|
+
...renderCrewProfiles().map((profile) => profile.rel)
|
|
6704
7290
|
];
|
|
7291
|
+
var CREW_PROFILE_RELS = new Set(renderCrewProfiles().map((p) => p.rel));
|
|
6705
7292
|
function isAllowlisted(targetRelPath) {
|
|
6706
7293
|
const normalised = targetRelPath.replace(/\\/g, "/");
|
|
6707
7294
|
return WRITE_ALLOWLIST.some((prefix) => {
|
|
6708
7295
|
const p = prefix.replace(/\\/g, "/");
|
|
6709
|
-
return normalised === p || normalised.startsWith(p + "/");
|
|
7296
|
+
return normalised === p || !CREW_PROFILE_RELS.has(p) && normalised.startsWith(p + "/");
|
|
6710
7297
|
});
|
|
6711
7298
|
}
|
|
6712
7299
|
var CREW_ROSTER_REL = ".halfcycle/crew.json";
|
|
@@ -6719,6 +7306,12 @@ var GITATTRIBUTES_REL = ".gitattributes";
|
|
|
6719
7306
|
function commandStubRel(name) {
|
|
6720
7307
|
return `.claude/commands/${name}.md`;
|
|
6721
7308
|
}
|
|
7309
|
+
function skillRel(name) {
|
|
7310
|
+
if (!/^[a-z][a-z0-9-]*$/.test(name)) {
|
|
7311
|
+
throw new Error(`[halfcycle] plugin.json command "${name}" is not a valid skill name (must match ^[a-z][a-z0-9-]*$).`);
|
|
7312
|
+
}
|
|
7313
|
+
return `.agents/skills/${name}/SKILL.md`;
|
|
7314
|
+
}
|
|
6722
7315
|
var RETIRED_MCP_HEADERS_HELPER_REL = ".halfcycle/mcp-headers.sh";
|
|
6723
7316
|
var RETIRED_WRITE_SET_FILES = [RETIRED_MCP_HEADERS_HELPER_REL];
|
|
6724
7317
|
var RETIRED_DENY_PATTERNS = [
|
|
@@ -6737,6 +7330,9 @@ function closedWriteSet() {
|
|
|
6737
7330
|
const commands = readPluginManifest().commands ?? [];
|
|
6738
7331
|
const files = /* @__PURE__ */ new Set([
|
|
6739
7332
|
...commands.map((cmd) => commandStubRel(cmd.name)),
|
|
7333
|
+
// Codex's twin of the stubs (`codex-install-target` AC4, T-14), derived from the
|
|
7334
|
+
// same manifest array the stubs come from — one closed-set source for both.
|
|
7335
|
+
...commands.map((cmd) => skillRel(cmd.name)),
|
|
6740
7336
|
...Object.keys(OWNED_GENERATED_HEADERS),
|
|
6741
7337
|
VENDORED_BIN_REL,
|
|
6742
7338
|
CREW_ROSTER_REL,
|
|
@@ -6745,9 +7341,12 @@ function closedWriteSet() {
|
|
|
6745
7341
|
BOOTSTRAP_STATE_REL,
|
|
6746
7342
|
CAPTURED_INDEX_REL,
|
|
6747
7343
|
INSTALL_MANIFEST_REL,
|
|
7344
|
+
// The crew profiles (`crew-agent-profiles` AC6), from the same generator the
|
|
7345
|
+
// allowlist and the plan read, so uninstall's set and the writer's cannot differ.
|
|
7346
|
+
...renderCrewProfiles().map((profile) => profile.rel),
|
|
6748
7347
|
...RETIRED_WRITE_SET_FILES
|
|
6749
7348
|
]);
|
|
6750
|
-
const merged = /* @__PURE__ */ new Set([SETTINGS_REL, MCP_REGISTRATION_REL, GITIGNORE_REL, GITATTRIBUTES_REL]);
|
|
7349
|
+
const merged = /* @__PURE__ */ new Set([SETTINGS_REL, MCP_REGISTRATION_REL, CODEX_HOOKS_REL2, CODEX_CONFIG_REL2, GITIGNORE_REL, GITATTRIBUTES_REL]);
|
|
6751
7350
|
const dirs = /* @__PURE__ */ new Set();
|
|
6752
7351
|
for (const path of [...files, ...merged]) {
|
|
6753
7352
|
for (let dir = posix2.dirname(path); dir !== "."; dir = posix2.dirname(dir))
|
|
@@ -6760,6 +7359,7 @@ function closedWriteSet() {
|
|
|
6760
7359
|
dirs,
|
|
6761
7360
|
ownedDirs,
|
|
6762
7361
|
hookCommands: new Set(HALFCYCLE_HOOK_COMMANDS),
|
|
7362
|
+
codexHookCommands: new Set(AGENT_PROFILES.codex.commands),
|
|
6763
7363
|
denyRuleSha256: new Set([...DENY_PATTERNS, ...RETIRED_DENY_PATTERNS].map((rule) => sha256Hex(rule))),
|
|
6764
7364
|
settingsSchema: SETTINGS_SCHEMA,
|
|
6765
7365
|
gitignoreLines: /* @__PURE__ */ new Set([GITIGNORE_HEADER, ...REQUIRED_GITIGNORE_ENTRIES, LEGACY_ENV_LOCAL_REL]),
|
|
@@ -6777,10 +7377,17 @@ function sha256OfWhatThisVersionWrites(commands) {
|
|
|
6777
7377
|
if (existsSync4(src))
|
|
6778
7378
|
out.set(rel, fileSha256(readFileSync6(src)));
|
|
6779
7379
|
};
|
|
6780
|
-
for (const cmd of commands)
|
|
6781
|
-
|
|
7380
|
+
for (const cmd of commands) {
|
|
7381
|
+
const srcFile = join6(BUNDLE_ROOT, ".claude-plugin", cmd.path);
|
|
7382
|
+
fromFile(commandStubRel(cmd.name), srcFile);
|
|
7383
|
+
if (existsSync4(srcFile)) {
|
|
7384
|
+
out.set(skillRel(cmd.name), fileSha256(Buffer.from(codexSkillContent(cmd.name, readFileSync6(srcFile, "utf-8")), "utf-8")));
|
|
7385
|
+
}
|
|
7386
|
+
}
|
|
6782
7387
|
for (const [rel, content] of hookScriptsThisVersionWrites())
|
|
6783
7388
|
out.set(rel, fileSha256(content));
|
|
7389
|
+
for (const profile of renderCrewProfiles())
|
|
7390
|
+
out.set(profile.rel, fileSha256(Buffer.from(profile.text, "utf-8")));
|
|
6784
7391
|
fromFile(CAPTURED_INDEX_REL, capturedIndexScaffold());
|
|
6785
7392
|
return out;
|
|
6786
7393
|
}
|
|
@@ -6884,6 +7491,8 @@ function findNotShared(targetRepo, manifest, collidedPaths) {
|
|
|
6884
7491
|
const merged = [
|
|
6885
7492
|
SETTINGS_REL,
|
|
6886
7493
|
...manifest.mcp !== void 0 ? [MCP_REGISTRATION_REL] : [],
|
|
7494
|
+
...manifest.codexHooks !== void 0 ? [CODEX_HOOKS_REL2] : [],
|
|
7495
|
+
...manifest.codexConfig !== void 0 ? [CODEX_CONFIG_REL2] : [],
|
|
6887
7496
|
...manifest.gitignore.created || manifest.gitignore.appended.length > 0 ? [GITIGNORE_REL] : [],
|
|
6888
7497
|
...manifest.gitattributes !== void 0 && (manifest.gitattributes.created || manifest.gitattributes.appended.length > 0) ? [GITATTRIBUTES_REL] : []
|
|
6889
7498
|
];
|
|
@@ -6944,6 +7553,55 @@ function readPluginManifest() {
|
|
|
6944
7553
|
const raw = readFileSync6(manifestPath, "utf-8");
|
|
6945
7554
|
return JSON.parse(raw);
|
|
6946
7555
|
}
|
|
7556
|
+
function parseCommandStub(raw, name) {
|
|
7557
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/.exec(raw);
|
|
7558
|
+
if (match === null) {
|
|
7559
|
+
throw new Error(`[halfcycle] commands/${name}.md has no frontmatter block; cannot build its Codex skill.`);
|
|
7560
|
+
}
|
|
7561
|
+
const [, frontmatter, body] = match;
|
|
7562
|
+
const desc = /^description:\s*(.+)$/m.exec(frontmatter);
|
|
7563
|
+
if (desc === null) {
|
|
7564
|
+
throw new Error(`[halfcycle] commands/${name}.md's frontmatter has no description; cannot build its Codex skill.`);
|
|
7565
|
+
}
|
|
7566
|
+
return { description: desc[1].trim(), body };
|
|
7567
|
+
}
|
|
7568
|
+
var TRUST_SENTENCE_WORDS = "Claude Code does not trust this folder yet, open it there and accept the workspace-trust prompt";
|
|
7569
|
+
var CODEX_TRUST_SENTENCE = "Codex does not trust this folder yet, open it there and trust it when asked";
|
|
7570
|
+
function trustSentencePattern() {
|
|
7571
|
+
const words = TRUST_SENTENCE_WORDS.split(" ").map((w) => w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
7572
|
+
return new RegExp(words.join("(?: |\\n> )"));
|
|
7573
|
+
}
|
|
7574
|
+
var MCP_RECONNECT_WORDS = "run `/mcp` and reconnect it";
|
|
7575
|
+
var CODEX_MCP_FALLBACK = "start a new Codex session";
|
|
7576
|
+
function mcpReconnectPattern() {
|
|
7577
|
+
const words = MCP_RECONNECT_WORDS.split(" ").map((w) => w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
7578
|
+
return new RegExp(words.join("(?: |\\n> )"));
|
|
7579
|
+
}
|
|
7580
|
+
function codexSkillBody(name, sourceBody) {
|
|
7581
|
+
const slashForm = "`/" + name + "`";
|
|
7582
|
+
if (!sourceBody.includes(slashForm)) {
|
|
7583
|
+
throw new Error(`[halfcycle] commands/${name}.md never names itself as ${slashForm}; cannot build its Codex skill.`);
|
|
7584
|
+
}
|
|
7585
|
+
const pattern = trustSentencePattern();
|
|
7586
|
+
if (!pattern.test(sourceBody)) {
|
|
7587
|
+
throw new Error(`[halfcycle] commands/${name}.md is missing the Claude Code trust sentence; cannot build its Codex skill.`);
|
|
7588
|
+
}
|
|
7589
|
+
const bareName = "`" + name + "`";
|
|
7590
|
+
const transformed = sourceBody.split(slashForm).join(bareName).replace(trustSentencePattern(), CODEX_TRUST_SENTENCE).replace(mcpReconnectPattern(), CODEX_MCP_FALLBACK);
|
|
7591
|
+
if (transformed.includes("/mcp")) {
|
|
7592
|
+
throw new Error(`[halfcycle] commands/${name}.md's Codex skill still names /mcp; cannot build.`);
|
|
7593
|
+
}
|
|
7594
|
+
return transformed;
|
|
7595
|
+
}
|
|
7596
|
+
function codexSkillContent(name, sourceRaw) {
|
|
7597
|
+
const { description, body } = parseCommandStub(sourceRaw, name);
|
|
7598
|
+
const transformed = codexSkillBody(name, body);
|
|
7599
|
+
return `---
|
|
7600
|
+
name: ${name}
|
|
7601
|
+
description: ${description}
|
|
7602
|
+
---
|
|
7603
|
+
${transformed}`;
|
|
7604
|
+
}
|
|
6947
7605
|
function planManifestCommands(manifest, targetRepo, previous, writeSet) {
|
|
6948
7606
|
const recordedFiles = new Map((previous?.files ?? []).filter((file) => writeSet.files.has(file.path)).map((file) => [file.path, file]));
|
|
6949
7607
|
return (manifest.commands ?? []).map((cmd) => {
|
|
@@ -7122,6 +7780,16 @@ function generateSettingsJson() {
|
|
|
7122
7780
|
};
|
|
7123
7781
|
return JSON.stringify(settings, null, 2) + "\n";
|
|
7124
7782
|
}
|
|
7783
|
+
function codexHooksValue() {
|
|
7784
|
+
const hooks = {};
|
|
7785
|
+
for (const { event, matcher, timeout, command, commandWindows } of AGENT_PROFILES.codex.wiring) {
|
|
7786
|
+
(hooks[event] ??= []).push({
|
|
7787
|
+
...matcher === null ? {} : { matcher },
|
|
7788
|
+
hooks: [{ type: "command", command, timeout, ...commandWindows === void 0 ? {} : { commandWindows } }]
|
|
7789
|
+
});
|
|
7790
|
+
}
|
|
7791
|
+
return { hooks };
|
|
7792
|
+
}
|
|
7125
7793
|
var GENERATED_GUARD_RUNNER_HEADER = "# Governance hook wrapper \u2014 generated by";
|
|
7126
7794
|
function generateGuardRunnerWrapper(preFilter = resolvePreFilterShell()) {
|
|
7127
7795
|
return `#!/usr/bin/env bash
|
|
@@ -7174,9 +7842,11 @@ ${preFilter.trimEnd()}
|
|
|
7174
7842
|
# Before a tool runs, on a bash new enough for the cheap pass: read the call
|
|
7175
7843
|
# once; when no risky action can be in it, answer here (the pass writes the
|
|
7176
7844
|
# time line) and do not start Node. Otherwise hand the same input on below.
|
|
7845
|
+
# The pass is given the hook's own arguments, so it knows which coding agent's
|
|
7846
|
+
# input it reads, and hands on any agent's call it has no test for.
|
|
7177
7847
|
if [ "$1" = hook ] && [ "$2" = pre-tool-use ] && [ "\${BASH_VERSINFO[0]:-0}" -ge 5 ]; then
|
|
7178
7848
|
hc_input=$(cat 2>/dev/null) || IFS= read -r -d '' hc_input
|
|
7179
|
-
${PRE_FILTER_FUNCTION} && exit 0
|
|
7849
|
+
${PRE_FILTER_FUNCTION} "$@" && exit 0
|
|
7180
7850
|
node -e "${nodeFloorProgram("")}" || {
|
|
7181
7851
|
printf '%s' "$hc_input" | halfcycle_pre_tool_use_fallback
|
|
7182
7852
|
exit 0
|
|
@@ -7416,7 +8086,7 @@ exit 0
|
|
|
7416
8086
|
var GENERATED_USER_PROMPT_SCOPE_HEADER = "# Typed scope approval \u2014 generated by";
|
|
7417
8087
|
function generateUserPromptScopeHook() {
|
|
7418
8088
|
return `#!/usr/bin/env bash
|
|
7419
|
-
${GENERATED_USER_PROMPT_SCOPE_HEADER} the Halfcycle installer, now also for the close review.
|
|
8089
|
+
${GENERATED_USER_PROMPT_SCOPE_HEADER} the Halfcycle installer, now also for the close review and Codex.
|
|
7420
8090
|
#
|
|
7421
8091
|
# Lets the owner of this project approve a phase's scope, or send it back,
|
|
7422
8092
|
# decide one part of a phase's close review, or turn builds running on their
|
|
@@ -7424,20 +8094,25 @@ ${GENERATED_USER_PROMPT_SCOPE_HEADER} the Halfcycle installer, now also for the
|
|
|
7424
8094
|
# Code session: "approve <phase>", "send back <phase>: <note>", "accept
|
|
7425
8095
|
# <phase>/<part>", "question <phase>/<part>: <words>", "carry <phase>/<part>:
|
|
7426
8096
|
# <words>", "drop <phase>/<part>: <words>", "reject <phase>/<task>: <words>",
|
|
7427
|
-
# "autonomy on" or "autonomy off".
|
|
8097
|
+
# "autonomy on" or "autonomy off". Also lets the person who is typing let one
|
|
8098
|
+
# step Halfcycle paused run once, by typing "allow" as the whole prompt.
|
|
7428
8099
|
#
|
|
7429
8100
|
# AN ORDINARY PROMPT STARTS NOTHING. This script reads the hook input once and
|
|
7430
8101
|
# looks only at the start of the prompt's value. Unless the prompt begins, after
|
|
7431
8102
|
# any whitespace (typed, or escaped in the input), with "approve ", "send back ",
|
|
7432
|
-
# "accept ", "question ", "carry ", "drop ", "reject " or "autonomy ", in any case,
|
|
7433
|
-
#
|
|
8103
|
+
# "accept ", "question ", "carry ", "drop ", "reject " or "autonomy ", in any case,
|
|
8104
|
+
# or is "allow" and nothing else but whitespace, it exits 0 with no output, and
|
|
8105
|
+
# Node never starts. A quote inside any other field arrives
|
|
7434
8106
|
# escaped, so no other field (the folder the session is in, for one) can read as
|
|
7435
8107
|
# the start of the prompt.
|
|
7436
8108
|
#
|
|
7437
8109
|
# Otherwise it finds the project root from its own location, never from the
|
|
7438
8110
|
# folder the session has moved to, and hands the same input to the guard
|
|
7439
8111
|
# wrapper beside it. That decides whether the line counts and prints at most
|
|
7440
|
-
# one
|
|
8112
|
+
# one line. Nothing here ever blocks a prompt. Any arguments this script was
|
|
8113
|
+
# given (a Codex row passes its agent's name) are handed on after the verb; in
|
|
8114
|
+
# Codex a typed line never counts, because Codex cannot tell a person's line
|
|
8115
|
+
# from one a program sent.
|
|
7441
8116
|
#
|
|
7442
8117
|
# FAILS OPEN up to that hand-over: any error before it exits 0 with no output.
|
|
7443
8118
|
# After it, the wrapper's own exit stands.
|
|
@@ -7453,7 +8128,7 @@ IFS= read -r -d '' HC_INPUT || true
|
|
|
7453
8128
|
# \\u escape, or as the UTF-8 bytes of a Unicode space. Compared byte for byte.
|
|
7454
8129
|
HC_LEAD='[[:space:]]|\\\\[nrtf]|\\\\u(000[9a-d]|0020|00a0|1680|200[0-9a]|202[89f]|205f|3000|feff)'
|
|
7455
8130
|
HC_LEAD="$HC_LEAD|"$'\\xc2\\xa0|\\xe1\\x9a\\x80|\\xe2\\x80[\\x80-\\x8a\\xa8\\xa9\\xaf]|\\xe2\\x81\\x9f|\\xe3\\x80\\x80|\\xef\\xbb\\xbf'
|
|
7456
|
-
if ! printf '%s' "$HC_INPUT" | LC_ALL=C grep -Eiq "\\"prompt\\"[[:space:]]*:[[:space:]]*\\"($HC_LEAD)*(approve|send back|accept|question|carry|drop|reject|autonomy) "; then
|
|
8131
|
+
if ! printf '%s' "$HC_INPUT" | LC_ALL=C grep -Eiq "\\"prompt\\"[[:space:]]*:[[:space:]]*\\"($HC_LEAD)*((approve|send back|accept|question|carry|drop|reject|autonomy) |allow($HC_LEAD)*\\")"; then
|
|
7457
8132
|
exit 0
|
|
7458
8133
|
fi
|
|
7459
8134
|
|
|
@@ -7463,16 +8138,17 @@ export CLAUDE_PROJECT_DIR="$HC_PROJECT_ROOT"
|
|
|
7463
8138
|
|
|
7464
8139
|
trap - ERR
|
|
7465
8140
|
set +e
|
|
7466
|
-
exec bash "$HC_PROJECT_ROOT/.claude/hooks/guard-runner.sh" hook user-prompt-submit <<<"$HC_INPUT"
|
|
8141
|
+
exec bash "$HC_PROJECT_ROOT/.claude/hooks/guard-runner.sh" hook user-prompt-submit <<<"$HC_INPUT" "$@"
|
|
7467
8142
|
`;
|
|
7468
8143
|
}
|
|
7469
8144
|
var GENERATED_USER_PROMPT_FINDINGS_HEADER = "# Findings hand-over \u2014 generated by";
|
|
7470
8145
|
function generateUserPromptFindingsHook() {
|
|
7471
8146
|
return `#!/usr/bin/env bash
|
|
7472
|
-
${GENERATED_USER_PROMPT_FINDINGS_HEADER} the Halfcycle installer.
|
|
8147
|
+
${GENERATED_USER_PROMPT_FINDINGS_HEADER} the Halfcycle installer, now also for Codex.
|
|
7473
8148
|
#
|
|
7474
8149
|
# At the person's next message, before the agent starts, hands it the details of
|
|
7475
|
-
# whatever Halfcycle's checks found at an earlier turn end, once.
|
|
8150
|
+
# whatever Halfcycle's checks found at an earlier turn end, once. Any arguments
|
|
8151
|
+
# this script was given (a Codex row passes its agent's name) are handed on.
|
|
7476
8152
|
#
|
|
7477
8153
|
# AN ORDINARY PROMPT STARTS NOTHING. With no findings file for this session, this
|
|
7478
8154
|
# script reads its session id with a bash regex (no node) and exits 0 with no
|
|
@@ -7509,7 +8185,7 @@ export CLAUDE_PROJECT_DIR="$HC_PROJECT_ROOT"
|
|
|
7509
8185
|
|
|
7510
8186
|
trap - ERR
|
|
7511
8187
|
set +e
|
|
7512
|
-
exec bash "$HC_PROJECT_ROOT/.claude/hooks/guard-runner.sh" hook user-prompt-findings <<<"$HC_INPUT"
|
|
8188
|
+
exec bash "$HC_PROJECT_ROOT/.claude/hooks/guard-runner.sh" hook user-prompt-findings <<<"$HC_INPUT" "$@"
|
|
7513
8189
|
`;
|
|
7514
8190
|
}
|
|
7515
8191
|
var CI_STANZA_KNOWN_HEADER_HASHES = [
|
|
@@ -7588,6 +8264,160 @@ var MCP_HEADERS_HELPER_COMMAND = `node -e "${nodeFloorProgram([
|
|
|
7588
8264
|
`console.error('halfcycle: no ${VENDORED_BIN_REL} at or above '+process.cwd()+'; run npx halfcycle in the project folder');`,
|
|
7589
8265
|
`process.exit(1)`
|
|
7590
8266
|
].join(""))}"`;
|
|
8267
|
+
var CODEX_ADDRESS_PATTERN = /^https?:\/\/[A-Za-z0-9.-]+(:[0-9]{1,5})?(\/[A-Za-z0-9._-]+)*\/mcp$/;
|
|
8268
|
+
function codexAddressAllowed(address) {
|
|
8269
|
+
return CODEX_ADDRESS_PATTERN.test(address);
|
|
8270
|
+
}
|
|
8271
|
+
var CODEX_NO_CREDENTIAL_STATEMENT = `console.log(JSON.stringify({Authorization:'Bearer ${NO_CREDENTIAL_PLACEHOLDER}'}))`;
|
|
8272
|
+
function codexMcpHeadersHelperCommand(address) {
|
|
8273
|
+
if (!codexAddressAllowed(address))
|
|
8274
|
+
return null;
|
|
8275
|
+
return `node -e "${nodeFloorProgram([
|
|
8276
|
+
`let o=0;`,
|
|
8277
|
+
`const n=function(){if(o)return;o=1;${CODEX_NO_CREDENTIAL_STATEMENT}};`,
|
|
8278
|
+
`try{`,
|
|
8279
|
+
`const p=require('path'),f=require('fs'),c=require('child_process');`,
|
|
8280
|
+
`let d=process.cwd(),b='';`,
|
|
8281
|
+
`for(;;){`,
|
|
8282
|
+
`const t=p.join(d,'${VENDORED_BIN_REL}');`,
|
|
8283
|
+
`if(f.existsSync(t)){b=t;break}`,
|
|
8284
|
+
`const u=p.dirname(d);`,
|
|
8285
|
+
`if(u===d)break;`,
|
|
8286
|
+
`d=u}`,
|
|
8287
|
+
`if(b===''){`,
|
|
8288
|
+
`console.error('halfcycle: no ${VENDORED_BIN_REL} at or above '+process.cwd()+'; run npx halfcycle in the project folder');`,
|
|
8289
|
+
`n()}else{`,
|
|
8290
|
+
`const r=c.spawnSync(process.execPath,[b,'mcp-headers','--url','${address}'],{stdio:['ignore','pipe','inherit']});`,
|
|
8291
|
+
`if(r.status===0&&r.stdout&&r.stdout.length){o=1;process.stdout.write(r.stdout)}else{n()}}`,
|
|
8292
|
+
`}catch(e){n()}`
|
|
8293
|
+
].join(""), CODEX_NO_CREDENTIAL_STATEMENT)}"`;
|
|
8294
|
+
}
|
|
8295
|
+
var CODEX_CONFIG_OPEN_MARKER = "# >>> Halfcycle \u2014 generated by the Halfcycle installer; replaced on every install";
|
|
8296
|
+
var CODEX_CONFIG_CLOSE_MARKER = "# <<< Halfcycle";
|
|
8297
|
+
function tomlBasicString(value) {
|
|
8298
|
+
if (/[\u0000-\u001f\u007f]/.test(value)) {
|
|
8299
|
+
throw new Error("[halfcycle] a value for Codex's settings holds a control character.");
|
|
8300
|
+
}
|
|
8301
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
8302
|
+
}
|
|
8303
|
+
function codexServerPath() {
|
|
8304
|
+
const { serversKey, serverKey } = AGENT_PROFILES.codex.mcpRegistration;
|
|
8305
|
+
return [serversKey, serverKey];
|
|
8306
|
+
}
|
|
8307
|
+
function generateCodexConfigBlock(address) {
|
|
8308
|
+
return codexConfigBlockWith(address, codexMcpHeadersHelperCommand);
|
|
8309
|
+
}
|
|
8310
|
+
function codexConfigBlockWith(address, helperFor) {
|
|
8311
|
+
if (!codexAddressAllowed(address))
|
|
8312
|
+
return null;
|
|
8313
|
+
const helper = helperFor(address);
|
|
8314
|
+
if (helper === null)
|
|
8315
|
+
return null;
|
|
8316
|
+
const [serversKey, serverKey] = codexServerPath();
|
|
8317
|
+
return [
|
|
8318
|
+
CODEX_CONFIG_OPEN_MARKER,
|
|
8319
|
+
`[${serversKey}.${serverKey}]`,
|
|
8320
|
+
`url = ${tomlBasicString(address)}`,
|
|
8321
|
+
`http_headers_helper = ${tomlBasicString(helper)}`,
|
|
8322
|
+
CODEX_CONFIG_CLOSE_MARKER
|
|
8323
|
+
].join("\n");
|
|
8324
|
+
}
|
|
8325
|
+
var RETIRED_CODEX_MCP_HEADERS_HELPERS = [];
|
|
8326
|
+
function regeneratesCodexBlock(block, retired = RETIRED_CODEX_MCP_HEADERS_HELPERS) {
|
|
8327
|
+
const lines = linesOf(block);
|
|
8328
|
+
while (lines.length > 0 && lines[lines.length - 1] === "")
|
|
8329
|
+
lines.pop();
|
|
8330
|
+
const url = /^url = "((?:[^"\\]|\\.)*)"$/.exec(lines[2] ?? "");
|
|
8331
|
+
if (url === null)
|
|
8332
|
+
return false;
|
|
8333
|
+
const address = url[1].replace(/\\(["\\])/g, "$1");
|
|
8334
|
+
const text = lines.join("\n");
|
|
8335
|
+
return [codexMcpHeadersHelperCommand, ...retired].some((helperFor) => codexConfigBlockWith(address, helperFor) === text);
|
|
8336
|
+
}
|
|
8337
|
+
function locateCodexBlock(lines) {
|
|
8338
|
+
const opens = [];
|
|
8339
|
+
const closes = [];
|
|
8340
|
+
for (const [index, line] of lines.entries()) {
|
|
8341
|
+
const trimmed = line.trim();
|
|
8342
|
+
if (trimmed.startsWith("# >>> Halfcycle")) {
|
|
8343
|
+
if (line !== CODEX_CONFIG_OPEN_MARKER)
|
|
8344
|
+
return { kind: "broken" };
|
|
8345
|
+
opens.push(index);
|
|
8346
|
+
} else if (trimmed.startsWith("# <<< Halfcycle")) {
|
|
8347
|
+
if (line !== CODEX_CONFIG_CLOSE_MARKER)
|
|
8348
|
+
return { kind: "broken" };
|
|
8349
|
+
closes.push(index);
|
|
8350
|
+
}
|
|
8351
|
+
}
|
|
8352
|
+
if (opens.length === 0 && closes.length === 0)
|
|
8353
|
+
return { kind: "none" };
|
|
8354
|
+
if (opens.length === 1 && closes.length === 1 && opens[0] < closes[0]) {
|
|
8355
|
+
return { kind: "one", open: opens[0], close: closes[0] };
|
|
8356
|
+
}
|
|
8357
|
+
return { kind: "broken" };
|
|
8358
|
+
}
|
|
8359
|
+
function definesCodexServer(statement, servers, server) {
|
|
8360
|
+
const [first, second] = statement.path;
|
|
8361
|
+
if (first !== servers)
|
|
8362
|
+
return false;
|
|
8363
|
+
if (statement.kind === "table")
|
|
8364
|
+
return second === server;
|
|
8365
|
+
return statement.path.length === 1 || second === server;
|
|
8366
|
+
}
|
|
8367
|
+
function linesOf(text) {
|
|
8368
|
+
return text.split("\n").map((line) => line.endsWith("\r") ? line.slice(0, -1) : line);
|
|
8369
|
+
}
|
|
8370
|
+
function codexConfigConflict(existing) {
|
|
8371
|
+
const lines = linesOf(existing);
|
|
8372
|
+
const at = locateCodexBlock(lines);
|
|
8373
|
+
if (at.kind === "broken")
|
|
8374
|
+
return "broken-markers";
|
|
8375
|
+
const [servers, server] = codexServerPath();
|
|
8376
|
+
const scan = scanTomlStatements(lines, at.kind === "one" ? { from: at.open, to: at.close, tableAfter: [servers, server] } : void 0);
|
|
8377
|
+
if (!scan.ok)
|
|
8378
|
+
return "unreadable";
|
|
8379
|
+
return scan.statements.some((statement) => definesCodexServer(statement, servers, server)) ? "server-named" : null;
|
|
8380
|
+
}
|
|
8381
|
+
function holdsCodexBlock(existing) {
|
|
8382
|
+
return locateCodexBlock(linesOf(existing)).kind === "one";
|
|
8383
|
+
}
|
|
8384
|
+
function codexBlockSpan(text) {
|
|
8385
|
+
const at = locateCodexBlock(linesOf(text));
|
|
8386
|
+
if (at.kind !== "one")
|
|
8387
|
+
return at.kind;
|
|
8388
|
+
const raw = text.split("\n");
|
|
8389
|
+
let start = 0;
|
|
8390
|
+
for (let i = 0; i < at.open; i += 1)
|
|
8391
|
+
start += raw[i].length + 1;
|
|
8392
|
+
let end = start;
|
|
8393
|
+
for (let i = at.open; i <= at.close; i += 1)
|
|
8394
|
+
end += raw[i].length + 1;
|
|
8395
|
+
return { start, end: Math.min(end, text.length) };
|
|
8396
|
+
}
|
|
8397
|
+
function reconcileCodexConfig(existing, block) {
|
|
8398
|
+
if (existing === null || existing === "") {
|
|
8399
|
+
const text2 = `${block}
|
|
8400
|
+
`;
|
|
8401
|
+
return { kind: "new", text: text2, appended: existing === null ? null : text2, block: text2 };
|
|
8402
|
+
}
|
|
8403
|
+
const conflict = codexConfigConflict(existing);
|
|
8404
|
+
if (conflict !== null)
|
|
8405
|
+
return { kind: "conflict", reason: conflict };
|
|
8406
|
+
const eol = lineEndingOf(existing);
|
|
8407
|
+
const blockText = block.split("\n").join(eol);
|
|
8408
|
+
const span = codexBlockSpan(existing);
|
|
8409
|
+
if (span === "broken")
|
|
8410
|
+
return { kind: "conflict", reason: "broken-markers" };
|
|
8411
|
+
if (span === "none") {
|
|
8412
|
+
const appended = `${existing.endsWith("\n") ? "" : eol}${eol}${blockText}${eol}`;
|
|
8413
|
+
return { kind: "new", text: existing + appended, appended, block: `${blockText}${eol}` };
|
|
8414
|
+
}
|
|
8415
|
+
const old = existing.slice(span.start, span.end);
|
|
8416
|
+
const ending = old.endsWith("\r\n") ? "\r\n" : old.endsWith("\n") ? "\n" : "";
|
|
8417
|
+
const inFile = `${blockText}${ending}`;
|
|
8418
|
+
const text = existing.slice(0, span.start) + inFile + existing.slice(span.end);
|
|
8419
|
+
return text === existing ? { kind: "unchanged", block: inFile } : { kind: "new", text, appended: null, block: inFile };
|
|
8420
|
+
}
|
|
7591
8421
|
var MCP_HEADERS_KNOWN_HEADER_HASHES = [
|
|
7592
8422
|
"37a733ea588b3361b2bbfd86c44e0c95da30d7af6acffd1243ece83c811f0af2",
|
|
7593
8423
|
"6732ec6d2d2bf7cb33017749a3d021b59af653dcd07c3811693b5a242d44b1a1"
|
|
@@ -8002,6 +8832,21 @@ function planCommandStubs(targetRepo, previous, writeSet) {
|
|
|
8002
8832
|
const manifest = readPluginManifest();
|
|
8003
8833
|
return { version: manifest.version, commands: planManifestCommands(manifest, targetRepo, previous, writeSet) };
|
|
8004
8834
|
}
|
|
8835
|
+
function planCodexSkills(targetRepo, previous, writeSet) {
|
|
8836
|
+
const manifest = readPluginManifest();
|
|
8837
|
+
const recordedFiles = new Map((previous?.files ?? []).filter((file) => writeSet.files.has(file.path)).map((file) => [file.path, file]));
|
|
8838
|
+
return (manifest.commands ?? []).map((cmd) => {
|
|
8839
|
+
const srcFile = join6(BUNDLE_ROOT, ".claude-plugin", cmd.path);
|
|
8840
|
+
if (!existsSync4(srcFile)) {
|
|
8841
|
+
throw new Error(`[halfcycle] plugin.json declares command "${cmd.name}" at "${cmd.path}", but no file exists there (${srcFile}). The manifest and commands/ must agree.`);
|
|
8842
|
+
}
|
|
8843
|
+
const rel = skillRel(cmd.name);
|
|
8844
|
+
const content = codexSkillContent(cmd.name, readFileSync6(srcFile, "utf-8"));
|
|
8845
|
+
const recorded = recordedFiles.get(rel);
|
|
8846
|
+
const planned = decideCollisionSafe(join6(targetRepo, rel), targetRepo, content, (current) => bytesAreOurs(rel, current, recorded, writeSet));
|
|
8847
|
+
return { ...planned, editedCollided: planned.outcome === "collided" && recorded !== void 0 };
|
|
8848
|
+
});
|
|
8849
|
+
}
|
|
8005
8850
|
function planVendoredBinary(targetRepo) {
|
|
8006
8851
|
return decideOwned(join6(targetRepo, ".halfcycle", "bin", "bin.bundle.mjs"), targetRepo, readFileSync6(resolveVendoredBinary(), "utf-8"));
|
|
8007
8852
|
}
|
|
@@ -8049,9 +8894,156 @@ function putPlannedMcp(targetRepo, planned) {
|
|
|
8049
8894
|
return;
|
|
8050
8895
|
writeAllowlisted(join6(targetRepo, MCP_REGISTRATION_REL), targetRepo, planned.text, []);
|
|
8051
8896
|
}
|
|
8897
|
+
function fileInFolderPlace(root, rel) {
|
|
8898
|
+
for (let dir = posix2.dirname(rel); dir !== "."; dir = posix2.dirname(dir)) {
|
|
8899
|
+
const found = lstatOrNull(join6(root, dir));
|
|
8900
|
+
if (found !== null && !found.isDirectory())
|
|
8901
|
+
return true;
|
|
8902
|
+
}
|
|
8903
|
+
return false;
|
|
8904
|
+
}
|
|
8905
|
+
function symlinkOnPath(root, rel) {
|
|
8906
|
+
let current = root;
|
|
8907
|
+
for (const segment of rel.split("/")) {
|
|
8908
|
+
current = join6(current, segment);
|
|
8909
|
+
try {
|
|
8910
|
+
if (lstatSync(current).isSymbolicLink())
|
|
8911
|
+
return true;
|
|
8912
|
+
} catch {
|
|
8913
|
+
return false;
|
|
8914
|
+
}
|
|
8915
|
+
}
|
|
8916
|
+
return false;
|
|
8917
|
+
}
|
|
8918
|
+
function planCodexHooks(targetRepo) {
|
|
8919
|
+
const generated = codexHooksValue();
|
|
8920
|
+
const leave = (outcome, before2) => ({
|
|
8921
|
+
outcome,
|
|
8922
|
+
before: before2,
|
|
8923
|
+
existing: null,
|
|
8924
|
+
generated,
|
|
8925
|
+
text: null
|
|
8926
|
+
});
|
|
8927
|
+
if (symlinkOnPath(targetRepo, CODEX_HOOKS_REL2))
|
|
8928
|
+
return leave("link", null);
|
|
8929
|
+
const folder = lstatOrNull(join6(targetRepo, posix2.dirname(CODEX_HOOKS_REL2)));
|
|
8930
|
+
if (folder !== null && !folder.isDirectory())
|
|
8931
|
+
return leave("unreadable", null);
|
|
8932
|
+
const path = join6(targetRepo, CODEX_HOOKS_REL2);
|
|
8933
|
+
let before = null;
|
|
8934
|
+
if (lstatOrNull(path) !== null) {
|
|
8935
|
+
try {
|
|
8936
|
+
before = readFileSync6(path, "utf-8");
|
|
8937
|
+
} catch {
|
|
8938
|
+
return leave("unreadable", null);
|
|
8939
|
+
}
|
|
8940
|
+
}
|
|
8941
|
+
let existing = {};
|
|
8942
|
+
if (before !== null) {
|
|
8943
|
+
let value;
|
|
8944
|
+
try {
|
|
8945
|
+
value = JSON.parse(before);
|
|
8946
|
+
} catch {
|
|
8947
|
+
return leave("unreadable", before);
|
|
8948
|
+
}
|
|
8949
|
+
if (!isPlainObject2(value))
|
|
8950
|
+
return leave("unreadable", before);
|
|
8951
|
+
const hooks = value["hooks"];
|
|
8952
|
+
if (hooks !== void 0 && !isPlainObject2(hooks))
|
|
8953
|
+
return leave("unreadable", before);
|
|
8954
|
+
const ourEvents = Object.keys(generated.hooks);
|
|
8955
|
+
if (hooks !== void 0 && ourEvents.some((event) => event in hooks && !Array.isArray(hooks[event]))) {
|
|
8956
|
+
return leave("unreadable", before);
|
|
8957
|
+
}
|
|
8958
|
+
existing = value;
|
|
8959
|
+
}
|
|
8960
|
+
const merged = mergeCodexHooks(existing, generated, AGENT_PROFILES.codex.commands);
|
|
8961
|
+
if (before !== null && JSON.stringify(merged) === JSON.stringify(existing)) {
|
|
8962
|
+
return { outcome: "current", before, existing, generated, text: null };
|
|
8963
|
+
}
|
|
8964
|
+
const text = inLineEndingOf(before, JSON.stringify(merged, null, 2) + "\n");
|
|
8965
|
+
return { outcome: "written", before, existing: before === null ? null : existing, generated, text };
|
|
8966
|
+
}
|
|
8967
|
+
function putPlannedCodexHooks(targetRepo, planned) {
|
|
8968
|
+
if (planned.outcome !== "written" || planned.text === null)
|
|
8969
|
+
return;
|
|
8970
|
+
writeAllowlisted(join6(targetRepo, CODEX_HOOKS_REL2), targetRepo, planned.text, []);
|
|
8971
|
+
}
|
|
8972
|
+
function readCodexConfig(targetRepo) {
|
|
8973
|
+
const folder = lstatOrNull(join6(targetRepo, posix2.dirname(CODEX_CONFIG_REL2)));
|
|
8974
|
+
if (folder !== null && !folder.isDirectory())
|
|
8975
|
+
return "unreadable";
|
|
8976
|
+
const path = join6(targetRepo, CODEX_CONFIG_REL2);
|
|
8977
|
+
const stat = lstatOrNull(path);
|
|
8978
|
+
if (stat === null)
|
|
8979
|
+
return { text: null };
|
|
8980
|
+
if (!stat.isFile())
|
|
8981
|
+
return "unreadable";
|
|
8982
|
+
let bytes;
|
|
8983
|
+
try {
|
|
8984
|
+
bytes = readFileSync6(path);
|
|
8985
|
+
} catch {
|
|
8986
|
+
return "unreadable";
|
|
8987
|
+
}
|
|
8988
|
+
const text = bytes.toString("utf-8");
|
|
8989
|
+
return Buffer.from(text, "utf-8").equals(bytes) ? { text } : "unreadable";
|
|
8990
|
+
}
|
|
8991
|
+
function planCodexConfig(targetRepo, credential) {
|
|
8992
|
+
const plan = (outcome, extra = {}) => ({
|
|
8993
|
+
outcome,
|
|
8994
|
+
before: null,
|
|
8995
|
+
text: null,
|
|
8996
|
+
appended: null,
|
|
8997
|
+
inFile: null,
|
|
8998
|
+
conflict: null,
|
|
8999
|
+
blockKept: false,
|
|
9000
|
+
...extra
|
|
9001
|
+
});
|
|
9002
|
+
if (credential === void 0)
|
|
9003
|
+
return plan("no-credential");
|
|
9004
|
+
if (symlinkOnPath(targetRepo, CODEX_CONFIG_REL2))
|
|
9005
|
+
return plan("link");
|
|
9006
|
+
const read = readCodexConfig(targetRepo);
|
|
9007
|
+
if (read === "unreadable")
|
|
9008
|
+
return plan("conflict", { conflict: "unreadable" });
|
|
9009
|
+
const before = read.text;
|
|
9010
|
+
if (credential === "pending") {
|
|
9011
|
+
const conflict = before === null ? null : codexConfigConflict(before);
|
|
9012
|
+
return conflict === null ? plan("pending", { before }) : plan("conflict", { before, conflict });
|
|
9013
|
+
}
|
|
9014
|
+
const block = generateCodexConfigBlock(mcpEndpointUrl(credential.mcpUrl));
|
|
9015
|
+
if (block === null)
|
|
9016
|
+
return plan("refused-address", { before, blockKept: before !== null && holdsCodexBlock(before) });
|
|
9017
|
+
const reconciled = reconcileCodexConfig(before, block);
|
|
9018
|
+
switch (reconciled.kind) {
|
|
9019
|
+
case "conflict":
|
|
9020
|
+
return plan("conflict", { before, conflict: reconciled.reason });
|
|
9021
|
+
case "unchanged":
|
|
9022
|
+
return plan("current", { before, inFile: reconciled.block });
|
|
9023
|
+
case "new":
|
|
9024
|
+
return plan("written", { before, text: reconciled.text, appended: reconciled.appended, inFile: reconciled.block });
|
|
9025
|
+
}
|
|
9026
|
+
}
|
|
9027
|
+
function putPlannedCodexConfig(targetRepo, planned) {
|
|
9028
|
+
if (planned.outcome !== "written" || planned.text === null)
|
|
9029
|
+
return;
|
|
9030
|
+
writeAllowlisted(join6(targetRepo, CODEX_CONFIG_REL2), targetRepo, planned.text, []);
|
|
9031
|
+
}
|
|
8052
9032
|
function planCrewRoster(targetRepo) {
|
|
8053
9033
|
return decideOwned(join6(targetRepo, CREW_ROSTER_REL), targetRepo, renderCrewRoster());
|
|
8054
9034
|
}
|
|
9035
|
+
function planCrewProfiles(targetRepo, previous, writeSet) {
|
|
9036
|
+
const recordedFiles = new Map((previous?.files ?? []).filter((file) => writeSet.files.has(file.path)).map((file) => [file.path, file]));
|
|
9037
|
+
return renderCrewProfiles().map(({ rel, text }) => {
|
|
9038
|
+
const target = crewProfileTarget(targetRepo, rel);
|
|
9039
|
+
if (symlinkOnPath(targetRepo, rel) || fileInFolderPlace(targetRepo, rel)) {
|
|
9040
|
+
return { rel, text, outcome: "collided", replacedExisting: false, editedCollided: false };
|
|
9041
|
+
}
|
|
9042
|
+
const recorded = recordedFiles.get(rel);
|
|
9043
|
+
const planned = decideCollisionSafe(target, targetRepo, text, (current) => bytesAreOurs(rel, current, recorded, writeSet));
|
|
9044
|
+
return { ...planned, editedCollided: planned.outcome === "collided" && recorded !== void 0 };
|
|
9045
|
+
});
|
|
9046
|
+
}
|
|
8055
9047
|
function planInstallRecord(targetRepo, text) {
|
|
8056
9048
|
return decideOwned(join6(targetRepo, INSTALL_MANIFEST_REL), targetRepo, text);
|
|
8057
9049
|
}
|
|
@@ -8075,6 +9067,12 @@ function accountInstall(steps, actual) {
|
|
|
8075
9067
|
report.editedCollidedPaths.push(command.rel);
|
|
8076
9068
|
noteFile(draft, command.rel, command.outcome, actual.bytesAfter);
|
|
8077
9069
|
}
|
|
9070
|
+
for (const skill of steps.codexSkills) {
|
|
9071
|
+
recordOwnedGenerated(report, skill);
|
|
9072
|
+
if (skill.editedCollided)
|
|
9073
|
+
report.editedCollidedPaths.push(skill.rel);
|
|
9074
|
+
noteFile(draft, skill.rel, skill.outcome, actual.bytesAfter);
|
|
9075
|
+
}
|
|
8078
9076
|
noteFile(draft, VENDORED_BIN_REL, recordOwnedGenerated(report, steps.vendored), actual.bytesAfter);
|
|
8079
9077
|
const settings = steps.settings;
|
|
8080
9078
|
recordSettingsMerge(draft, settings.existing, settings.generated, settings.preexisted, settings.merged);
|
|
@@ -8132,6 +9130,8 @@ function accountInstall(steps, actual) {
|
|
|
8132
9130
|
if (mcp.existing !== null)
|
|
8133
9131
|
recordMcpAdoptionOnly(draft, mcp.existing);
|
|
8134
9132
|
}
|
|
9133
|
+
recordCodexHooksStep(report, draft, steps.codexHooks);
|
|
9134
|
+
recordCodexConfigStep(report, draft, steps.codexConfig);
|
|
8135
9135
|
recordGitignoreReconcile(draft, steps.gitignore.before, actual.gitignoreAfter, writeSet);
|
|
8136
9136
|
if (actual.gitignore === "failed") {
|
|
8137
9137
|
report.skippedPaths.push(".gitignore (write failed \u2014 see credential refusal)");
|
|
@@ -8174,6 +9174,12 @@ function accountInstall(steps, actual) {
|
|
|
8174
9174
|
report.writtenPaths.push(BUNDLE_PIN_REL);
|
|
8175
9175
|
noteFile(draft, BUNDLE_PIN_REL, "written", actual.bytesAfter);
|
|
8176
9176
|
noteFile(draft, CREW_ROSTER_REL, recordOwnedGenerated(report, steps.crew), actual.bytesAfter);
|
|
9177
|
+
for (const profile of steps.crewProfiles) {
|
|
9178
|
+
recordOwnedGenerated(report, profile);
|
|
9179
|
+
if (profile.editedCollided)
|
|
9180
|
+
report.editedCollidedPaths.push(profile.rel);
|
|
9181
|
+
noteFile(draft, profile.rel, profile.outcome, actual.bytesAfter);
|
|
9182
|
+
}
|
|
8177
9183
|
if (actual.stateExistsAfter)
|
|
8178
9184
|
recordPerMachineFile(draft, BOOTSTRAP_STATE_REL);
|
|
8179
9185
|
for (const dir of writeSet.dirs) {
|
|
@@ -8184,11 +9190,79 @@ function accountInstall(steps, actual) {
|
|
|
8184
9190
|
assertManifestInWriteSet(installManifest, writeSet);
|
|
8185
9191
|
return { report, installManifest, manifestText: serializeManifest(installManifest) };
|
|
8186
9192
|
}
|
|
9193
|
+
function recordCodexHooksStep(report, draft, planned) {
|
|
9194
|
+
switch (planned.outcome) {
|
|
9195
|
+
case "unreadable":
|
|
9196
|
+
report.skippedPaths.push(`${CODEX_HOOKS_REL2} (left as it is \u2014 it could not be read, so Codex's checks were not added)`);
|
|
9197
|
+
return;
|
|
9198
|
+
case "link":
|
|
9199
|
+
report.skippedPaths.push(`${CODEX_HOOKS_REL2} (left as it is \u2014 it is a link Halfcycle does not follow)`);
|
|
9200
|
+
return;
|
|
9201
|
+
case "written":
|
|
9202
|
+
(planned.before === null ? report.writtenPaths : report.mergedPaths).push(CODEX_HOOKS_REL2);
|
|
9203
|
+
break;
|
|
9204
|
+
case "current":
|
|
9205
|
+
report.skippedPaths.push(CODEX_HOOKS_REL2);
|
|
9206
|
+
break;
|
|
9207
|
+
}
|
|
9208
|
+
const existingHooks = planned.existing !== null && isPlainObject2(planned.existing["hooks"]) ? planned.existing["hooks"] : void 0;
|
|
9209
|
+
const ours = AGENT_PROFILES.codex.commands;
|
|
9210
|
+
const heldOurs = Object.values(existingHooks ?? {}).some((groups) => Array.isArray(groups) && groups.some((group) => isPlainObject2(group) && Array.isArray(group["hooks"]) && isHalfcycleEntry({ hooks: group["hooks"] }, ours)));
|
|
9211
|
+
recordCodexHooks(draft, {
|
|
9212
|
+
created: planned.before === null,
|
|
9213
|
+
adopted: draft.previous === null && heldOurs,
|
|
9214
|
+
hookCommands: Object.values(planned.generated.hooks).flatMap((groups) => groups.flatMap((group) => group.hooks.map((h) => h.command))),
|
|
9215
|
+
eventsCreated: Object.keys(planned.generated.hooks).filter((event) => !(existingHooks && event in existingHooks)),
|
|
9216
|
+
hooksCreated: planned.existing === null || planned.existing["hooks"] === void 0
|
|
9217
|
+
});
|
|
9218
|
+
}
|
|
9219
|
+
function recordCodexConfigStep(report, draft, planned) {
|
|
9220
|
+
const prev = draft.codexConfig;
|
|
9221
|
+
const separatorOf = (block) => {
|
|
9222
|
+
const at = block.indexOf(CODEX_CONFIG_OPEN_MARKER);
|
|
9223
|
+
return at > 0 ? block.slice(0, at) : "";
|
|
9224
|
+
};
|
|
9225
|
+
switch (planned.outcome) {
|
|
9226
|
+
case "written": {
|
|
9227
|
+
(planned.before === null ? report.writtenPaths : report.mergedPaths).push(CODEX_CONFIG_REL2);
|
|
9228
|
+
const block = planned.appended !== null ? planned.appended : planned.before === null ? planned.text : `${prev === void 0 ? "" : separatorOf(prev.block)}${planned.inFile}`;
|
|
9229
|
+
recordCodexConfig(draft, { created: planned.before === null, block });
|
|
9230
|
+
return;
|
|
9231
|
+
}
|
|
9232
|
+
case "current":
|
|
9233
|
+
report.skippedPaths.push(CODEX_CONFIG_REL2);
|
|
9234
|
+
if (prev === void 0)
|
|
9235
|
+
recordCodexConfig(draft, { created: false, block: planned.inFile });
|
|
9236
|
+
return;
|
|
9237
|
+
case "no-credential":
|
|
9238
|
+
report.skippedPaths.push(`${CODEX_CONFIG_REL2} (no MCP origin \u2014 no credential supplied)`);
|
|
9239
|
+
return;
|
|
9240
|
+
case "link":
|
|
9241
|
+
report.skippedPaths.push(`${CODEX_CONFIG_REL2} (left as it is \u2014 it is a link Halfcycle does not follow)`);
|
|
9242
|
+
return;
|
|
9243
|
+
case "conflict":
|
|
9244
|
+
report.skippedPaths.push(planned.conflict === "server-named" ? `${CODEX_CONFIG_REL2} (left as it is \u2014 it already names a "halfcycle" server, so Halfcycle did not add its own)` : planned.conflict === "broken-markers" ? `${CODEX_CONFIG_REL2} (left as it is \u2014 the lines marking Halfcycle's part of it are incomplete or repeated)` : `${CODEX_CONFIG_REL2} (left as it is \u2014 it could not be read, so Halfcycle did not add its entry)`);
|
|
9245
|
+
return;
|
|
9246
|
+
case "refused-address":
|
|
9247
|
+
report.skippedPaths.push(planned.blockKept ? `${CODEX_CONFIG_REL2} (left as it is \u2014 the Halfcycle address cannot be passed to Codex safely; Halfcycle's existing entry in it was kept unchanged)` : `${CODEX_CONFIG_REL2} (not written \u2014 the Halfcycle address cannot be passed to Codex safely)`);
|
|
9248
|
+
return;
|
|
9249
|
+
case "pending":
|
|
9250
|
+
throw new Error("[halfcycle] a plan still waiting for the service cannot be applied or recorded.");
|
|
9251
|
+
}
|
|
9252
|
+
}
|
|
8187
9253
|
function recordInstallRecord(report, planned) {
|
|
8188
9254
|
recordOwnedGenerated(report, planned);
|
|
8189
9255
|
}
|
|
8190
9256
|
var SELF_BUILD_GITIGNORE_REL = ".halfcycle/.gitignore";
|
|
8191
9257
|
var SELF_BUILD_GITIGNORE_TEXT = "bin/\n";
|
|
9258
|
+
var SELF_BUILD_WRITE_SET = [
|
|
9259
|
+
BUNDLE_PIN_REL,
|
|
9260
|
+
VENDORED_BIN_REL,
|
|
9261
|
+
SELF_BUILD_GITIGNORE_REL,
|
|
9262
|
+
CREW_ROSTER_REL,
|
|
9263
|
+
MCP_REGISTRATION_REL,
|
|
9264
|
+
...renderCrewProfiles().map((profile) => profile.rel)
|
|
9265
|
+
];
|
|
8192
9266
|
function mcpHasHalfcycleEntry(text) {
|
|
8193
9267
|
const parsed = JSON.parse(text);
|
|
8194
9268
|
if (parsed === null || typeof parsed !== "object")
|
|
@@ -8196,6 +9270,14 @@ function mcpHasHalfcycleEntry(text) {
|
|
|
8196
9270
|
const servers = parsed.mcpServers;
|
|
8197
9271
|
return servers !== null && typeof servers === "object" && MCP_SERVER_KEY in servers;
|
|
8198
9272
|
}
|
|
9273
|
+
function writeMissingSelfBuildCrewProfiles(targetRepoRoot, report) {
|
|
9274
|
+
for (const { rel, text } of renderCrewProfiles()) {
|
|
9275
|
+
const target = crewProfileTarget(targetRepoRoot, rel);
|
|
9276
|
+
if (existsSync4(target))
|
|
9277
|
+
continue;
|
|
9278
|
+
writeAllowlisted(target, targetRepoRoot, text, report.writtenPaths);
|
|
9279
|
+
}
|
|
9280
|
+
}
|
|
8199
9281
|
function installSelfBuild(options) {
|
|
8200
9282
|
const { targetRepo, engagementId, credential, freshlyMinted, home } = options;
|
|
8201
9283
|
assertEngagementId(engagementId);
|
|
@@ -8230,6 +9312,7 @@ function installSelfBuild(options) {
|
|
|
8230
9312
|
if (!existsSync4(at(CREW_ROSTER_REL))) {
|
|
8231
9313
|
writeCrewRoster(targetRepo, report);
|
|
8232
9314
|
}
|
|
9315
|
+
writeMissingSelfBuildCrewProfiles(targetRepo, report);
|
|
8233
9316
|
if (!existsSync4(at(BUNDLE_PIN_REL))) {
|
|
8234
9317
|
writeBundlePin(targetRepo, planBundlePin(targetRepo, version, engagementId, "self-build", void 0, credential.methodVersion), report.writtenPaths);
|
|
8235
9318
|
}
|
|
@@ -9543,7 +10626,7 @@ function refuseInsideProject(targetRepo) {
|
|
|
9543
10626
|
let folder = realOrLiteral(targetRepo);
|
|
9544
10627
|
for (let parent = dirname4(folder); parent !== folder; folder = parent, parent = dirname4(parent)) {
|
|
9545
10628
|
if (existsSync5(join7(parent, BUNDLE_PIN_REL))) {
|
|
9546
|
-
throw new PreCreateRefused(`${targetRepo} is inside the Halfcycle project at ${parent} \u2014 open that folder in
|
|
10629
|
+
throw new PreCreateRefused(`${targetRepo} is inside the Halfcycle project at ${parent} \u2014 open that folder in your coding agent instead, or run "npx halfcycle" in a folder outside it.`);
|
|
9547
10630
|
}
|
|
9548
10631
|
}
|
|
9549
10632
|
}
|
|
@@ -9789,7 +10872,8 @@ function renderPreview(plan, missingGitForWindows, claudeMdWords) {
|
|
|
9789
10872
|
|
|
9790
10873
|
// dist/doctor.js
|
|
9791
10874
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
9792
|
-
import { existsSync as existsSync7, readFileSync as readFileSync10 } from "node:fs";
|
|
10875
|
+
import { existsSync as existsSync7, readFileSync as readFileSync10, realpathSync as realpathSync3 } from "node:fs";
|
|
10876
|
+
import { homedir as homedir4 } from "node:os";
|
|
9793
10877
|
import { join as join10 } from "node:path";
|
|
9794
10878
|
var GUARD_RUNNER_REL2 = ".claude/hooks/guard-runner.sh";
|
|
9795
10879
|
var CONNECTION_VERB = "check-connection";
|
|
@@ -9813,8 +10897,10 @@ function runDoctor(root, options) {
|
|
|
9813
10897
|
const lines = [setUp.line, connection.signedIn, connection.connected, checksRunning, version];
|
|
9814
10898
|
const failing = lines.filter((line) => !line.ok && !line.cascading).length;
|
|
9815
10899
|
const closing = failing === 0 ? "Everything is working." : `${failing} of these ${failing === 1 ? "needs" : "need"} fixing; each line above says what to do.`;
|
|
10900
|
+
const codex = codexLine(root, options);
|
|
10901
|
+
const printed = codex === void 0 ? lines : [...lines.slice(0, 4), codex, ...lines.slice(4)];
|
|
9816
10902
|
return {
|
|
9817
|
-
text: [...
|
|
10903
|
+
text: [...printed.map((line) => line.text), closing].join("\n"),
|
|
9818
10904
|
exitCode: failing === 0 ? 0 : 1
|
|
9819
10905
|
};
|
|
9820
10906
|
}
|
|
@@ -10004,6 +11090,88 @@ function versionNoLine(installed, current) {
|
|
|
10004
11090
|
}
|
|
10005
11091
|
return `Up to date: no. This folder has Halfcycle ${installed}; the newest is ${current}. To update, run ${HALFCYCLE_COMMAND} here.`;
|
|
10006
11092
|
}
|
|
11093
|
+
var CODEX_EVENT_SNAKE = {
|
|
11094
|
+
SessionStart: "session_start",
|
|
11095
|
+
UserPromptSubmit: "user_prompt_submit",
|
|
11096
|
+
PreToolUse: "pre_tool_use",
|
|
11097
|
+
PostToolUse: "post_tool_use",
|
|
11098
|
+
Stop: "stop",
|
|
11099
|
+
SubagentStop: "subagent_stop",
|
|
11100
|
+
SessionEnd: "session_end"
|
|
11101
|
+
};
|
|
11102
|
+
function codexLine(root, options) {
|
|
11103
|
+
const hooksPath = join10(root, CODEX_HOOKS_REL2);
|
|
11104
|
+
if (!existsSync7(hooksPath))
|
|
11105
|
+
return void 0;
|
|
11106
|
+
const positions = codexEntryPositions(hooksPath);
|
|
11107
|
+
if (positions === void 0) {
|
|
11108
|
+
return { ok: false, text: `Codex: not complete. To repair it, run ${HALFCYCLE_COMMAND} here.` };
|
|
11109
|
+
}
|
|
11110
|
+
return codexApprovalStatus(root, positions, options) === "approved" ? { ok: true, text: "Codex: set up, and its checks are approved." } : {
|
|
11111
|
+
ok: false,
|
|
11112
|
+
text: "Codex: set up, but its checks are not approved yet. Open this folder in Codex and type /hooks to approve each Halfcycle entry."
|
|
11113
|
+
};
|
|
11114
|
+
}
|
|
11115
|
+
function codexEntryPositions(hooksPath) {
|
|
11116
|
+
const parsed = readJson(hooksPath);
|
|
11117
|
+
if (!isObject(parsed) || !isObject(parsed["hooks"]))
|
|
11118
|
+
return void 0;
|
|
11119
|
+
const positions = [];
|
|
11120
|
+
for (const entry of AGENT_PROFILES.codex.wiring) {
|
|
11121
|
+
const groups = parsed["hooks"][entry.event];
|
|
11122
|
+
if (!Array.isArray(groups))
|
|
11123
|
+
return void 0;
|
|
11124
|
+
let found;
|
|
11125
|
+
for (let g = 0; g < groups.length && found === void 0; g += 1) {
|
|
11126
|
+
const group = groups[g];
|
|
11127
|
+
if (!isObject(group) || !Array.isArray(group["hooks"]))
|
|
11128
|
+
continue;
|
|
11129
|
+
for (let h = 0; h < group["hooks"].length; h += 1) {
|
|
11130
|
+
const hook = group["hooks"][h];
|
|
11131
|
+
if (isObject(hook) && hook["command"] === entry.command) {
|
|
11132
|
+
found = { group: g, hook: h };
|
|
11133
|
+
break;
|
|
11134
|
+
}
|
|
11135
|
+
}
|
|
11136
|
+
}
|
|
11137
|
+
if (found === void 0)
|
|
11138
|
+
return void 0;
|
|
11139
|
+
positions.push({ event: entry.event, group: found.group, hook: found.hook });
|
|
11140
|
+
}
|
|
11141
|
+
return positions;
|
|
11142
|
+
}
|
|
11143
|
+
function codexApprovalStatus(root, positions, options) {
|
|
11144
|
+
const codexHome = options.env["CODEX_HOME"];
|
|
11145
|
+
const configPath = join10(codexHome !== void 0 && codexHome !== "" ? codexHome : join10(homedir4(), ".codex"), "config.toml");
|
|
11146
|
+
let text;
|
|
11147
|
+
try {
|
|
11148
|
+
text = readFileSync10(configPath, "utf-8");
|
|
11149
|
+
} catch {
|
|
11150
|
+
return "not-approved";
|
|
11151
|
+
}
|
|
11152
|
+
let hooksPath;
|
|
11153
|
+
try {
|
|
11154
|
+
hooksPath = join10(realpathSync3(root), CODEX_HOOKS_REL2);
|
|
11155
|
+
} catch {
|
|
11156
|
+
return "not-approved";
|
|
11157
|
+
}
|
|
11158
|
+
const fileLines = text.split("\n");
|
|
11159
|
+
for (const { event, group, hook } of positions) {
|
|
11160
|
+
const key = `${hooksPath}:${CODEX_EVENT_SNAKE[event] ?? event}:${group}:${hook}`;
|
|
11161
|
+
const headers = [`[hooks.state."${key}"]`, `[hooks.state.'${key}']`];
|
|
11162
|
+
const start = fileLines.findIndex((line) => headers.includes(line.trim()));
|
|
11163
|
+
if (start === -1)
|
|
11164
|
+
return "not-approved";
|
|
11165
|
+
for (let i = start + 1; i < fileLines.length; i += 1) {
|
|
11166
|
+
const trimmed = fileLines[i].trim();
|
|
11167
|
+
if (trimmed.startsWith("["))
|
|
11168
|
+
break;
|
|
11169
|
+
if (trimmed === "enabled = false")
|
|
11170
|
+
return "not-approved";
|
|
11171
|
+
}
|
|
11172
|
+
}
|
|
11173
|
+
return "approved";
|
|
11174
|
+
}
|
|
10007
11175
|
function notChecked(check, earlier) {
|
|
10008
11176
|
return { ok: false, cascading: true, text: `${check}: not checked, because ${earlier} is not working yet.` };
|
|
10009
11177
|
}
|
|
@@ -10178,19 +11346,18 @@ async function probeMcpOrigin(mcpUrl, token) {
|
|
|
10178
11346
|
|
|
10179
11347
|
// dist/claude-version-probe.js
|
|
10180
11348
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
10181
|
-
import { existsSync as existsSync8, realpathSync as
|
|
11349
|
+
import { existsSync as existsSync8, realpathSync as realpathSync4 } from "node:fs";
|
|
10182
11350
|
import { delimiter as delimiter2, join as join12 } from "node:path";
|
|
10183
|
-
function
|
|
11351
|
+
function execVersionCommand(command, timeoutMs, deps) {
|
|
10184
11352
|
const platform6 = deps.platform ?? process.platform;
|
|
10185
11353
|
const exec = deps.exec ?? execFileSync3;
|
|
10186
|
-
let raw;
|
|
10187
11354
|
try {
|
|
10188
|
-
|
|
11355
|
+
return platform6 === "win32" ? exec("cmd.exe", ["/d", "/s", "/c", `"${command} --version"`], {
|
|
10189
11356
|
encoding: "utf-8",
|
|
10190
11357
|
timeout: timeoutMs,
|
|
10191
11358
|
stdio: ["ignore", "pipe", "ignore"],
|
|
10192
11359
|
windowsVerbatimArguments: true
|
|
10193
|
-
}) : exec(
|
|
11360
|
+
}) : exec(command, ["--version"], {
|
|
10194
11361
|
encoding: "utf-8",
|
|
10195
11362
|
timeout: timeoutMs,
|
|
10196
11363
|
stdio: ["ignore", "pipe", "ignore"]
|
|
@@ -10198,7 +11365,14 @@ function probeClaudeCodeVersion(timeoutMs, deps = {}) {
|
|
|
10198
11365
|
} catch {
|
|
10199
11366
|
return void 0;
|
|
10200
11367
|
}
|
|
10201
|
-
|
|
11368
|
+
}
|
|
11369
|
+
function probeClaudeCodeVersion(timeoutMs, deps = {}) {
|
|
11370
|
+
const raw = execVersionCommand("claude", timeoutMs, deps);
|
|
11371
|
+
return raw === void 0 ? void 0 : /(\d+\.\d+\.\d+)/.exec(raw)?.[1];
|
|
11372
|
+
}
|
|
11373
|
+
function probeCodexVersion(timeoutMs, deps = {}) {
|
|
11374
|
+
const raw = execVersionCommand("codex", timeoutMs, deps);
|
|
11375
|
+
return raw === void 0 ? void 0 : /^codex-cli (\d+\.\d+\.\d+)/.exec(raw)?.[1];
|
|
10202
11376
|
}
|
|
10203
11377
|
var CLAUDE_CODE_INSTALL_PAGE = "https://claude.com/claude-code";
|
|
10204
11378
|
var UPGRADE_COMMANDS = {
|
|
@@ -10219,7 +11393,7 @@ function probeClaudeCodeInstallMethod(deps = {}) {
|
|
|
10219
11393
|
const exists = deps.exists ?? existsSync8;
|
|
10220
11394
|
const realpath = deps.realpath ?? ((file) => {
|
|
10221
11395
|
try {
|
|
10222
|
-
return
|
|
11396
|
+
return realpathSync4(file);
|
|
10223
11397
|
} catch {
|
|
10224
11398
|
return file;
|
|
10225
11399
|
}
|
|
@@ -10238,6 +11412,30 @@ function probeClaudeCodeInstallMethod(deps = {}) {
|
|
|
10238
11412
|
return void 0;
|
|
10239
11413
|
}
|
|
10240
11414
|
|
|
11415
|
+
// dist/codex-install-lines.js
|
|
11416
|
+
var CODEX_APPROVAL_LINE = "[halfcycle] Using Codex? Open this folder in Codex and trust it when asked. Then type /hooks and approve each Halfcycle entry \u2014 until you do, Codex works without Halfcycle checking its edits. Then type $halfcycle-setup and answer what it asks about your project.";
|
|
11417
|
+
var CODEX_LINK_LINE = "[halfcycle] Codex: this project's Codex settings are a link Halfcycle does not follow, so Codex is not set up here. Codex sessions here will not reach Halfcycle.";
|
|
11418
|
+
var CODEX_CONFLICT_LINE = `[halfcycle] Codex: this project's Codex settings already name a "halfcycle" server, so Halfcycle did not add its own. Codex sessions here will not reach Halfcycle until that entry is removed and this is run again.`;
|
|
11419
|
+
var CODEX_REFUSED_ADDRESS_LINE = "[halfcycle] Codex: this project's Halfcycle address cannot be passed to Codex safely, so Codex sessions here will not reach Halfcycle.";
|
|
11420
|
+
var CODEX_UNREADABLE_HOOKS_LINE = "[halfcycle] Codex: this project's Codex checks file could not be read, so Halfcycle did not add its checks. Codex sessions here will not be checked until it is fixed and this is run again.";
|
|
11421
|
+
var CODEX_MIN_VERSION = "0.166.0";
|
|
11422
|
+
function codexVersionWarningLine(version) {
|
|
11423
|
+
return `[halfcycle] WARNING: Codex ${version} is older than ${CODEX_MIN_VERSION}, which Halfcycle needs. Update Codex, then reopen this folder.`;
|
|
11424
|
+
}
|
|
11425
|
+
function codexInstallLine(codex) {
|
|
11426
|
+
if (codex.hooks === "link" || codex.config === "link")
|
|
11427
|
+
return CODEX_LINK_LINE;
|
|
11428
|
+
if (codex.hooks === "unreadable")
|
|
11429
|
+
return CODEX_UNREADABLE_HOOKS_LINE;
|
|
11430
|
+
if (codex.config === "conflict")
|
|
11431
|
+
return CODEX_CONFLICT_LINE;
|
|
11432
|
+
if (codex.config === "refused-address")
|
|
11433
|
+
return CODEX_REFUSED_ADDRESS_LINE;
|
|
11434
|
+
if (codex.config === "no-credential")
|
|
11435
|
+
return void 0;
|
|
11436
|
+
return CODEX_APPROVAL_LINE;
|
|
11437
|
+
}
|
|
11438
|
+
|
|
10241
11439
|
// dist/build-record/write.js
|
|
10242
11440
|
import { existsSync as existsSync9, mkdirSync as mkdirSync7, rmSync as rmSync4 } from "node:fs";
|
|
10243
11441
|
import { basename as basename2, join as join13, resolve as resolve3, relative as relative2, isAbsolute as isAbsolute3 } from "node:path";
|
|
@@ -10980,7 +12178,7 @@ import { existsSync as existsSync11, readFileSync as readFileSync16 } from "node
|
|
|
10980
12178
|
import { join as join19 } from "node:path";
|
|
10981
12179
|
|
|
10982
12180
|
// dist/draw/write.js
|
|
10983
|
-
import { closeSync as closeSync2, lstatSync as lstatSync2, mkdirSync as mkdirSync8, openSync as openSync2, readFileSync as readFileSync13, readSync, realpathSync as
|
|
12181
|
+
import { closeSync as closeSync2, lstatSync as lstatSync2, mkdirSync as mkdirSync8, openSync as openSync2, readFileSync as readFileSync13, readSync, realpathSync as realpathSync5, rmSync as rmSync5 } from "node:fs";
|
|
10984
12182
|
import { dirname as dirname6, isAbsolute as isAbsolute4, join as join14, relative as relative3, resolve as resolve4, sep } from "node:path";
|
|
10985
12183
|
var DOCS_DIR = "docs";
|
|
10986
12184
|
var DIAGRAMS_DIR = "diagrams";
|
|
@@ -11068,7 +12266,7 @@ function isInside(outer, inner) {
|
|
|
11068
12266
|
function checkDiagramPlace(paths) {
|
|
11069
12267
|
let root;
|
|
11070
12268
|
try {
|
|
11071
|
-
root =
|
|
12269
|
+
root = realpathSync5(paths.repoRoot);
|
|
11072
12270
|
} catch {
|
|
11073
12271
|
return { ok: false, why: `the folder ${paths.repoRoot} could not be opened. Check the path, then run this again.` };
|
|
11074
12272
|
}
|
|
@@ -11081,7 +12279,7 @@ function checkDiagramPlace(paths) {
|
|
|
11081
12279
|
}
|
|
11082
12280
|
let real;
|
|
11083
12281
|
try {
|
|
11084
|
-
real =
|
|
12282
|
+
real = realpathSync5(ancestor);
|
|
11085
12283
|
} catch {
|
|
11086
12284
|
return {
|
|
11087
12285
|
ok: false,
|
|
@@ -12706,7 +13904,7 @@ function approvalLine2(phase, number, typedApprovalOffered, pinnedVersion, shown
|
|
|
12706
13904
|
if (!clientMeetsChatApprovalFloor(pinnedVersion)) {
|
|
12707
13905
|
return then(CLIENT_UPDATE_SENTENCE, "Or the project's owner approves it in the System view now:");
|
|
12708
13906
|
}
|
|
12709
|
-
return then(`To approve it, the project's owner types \`approve ${phase}\` in their own
|
|
13907
|
+
return then(`To approve it, the project's owner types \`approve ${phase}\` in their own coding agent's session. Only the owner can.`, "They can also approve it in the System view:");
|
|
12710
13908
|
}
|
|
12711
13909
|
function changeLines(plan, description, changed, carriedFrom = []) {
|
|
12712
13910
|
const names = new Map(description.parts.map((part) => [part.id, part.name]));
|
|
@@ -12954,7 +14152,7 @@ function rv7Lines(view, claims) {
|
|
|
12954
14152
|
if (decidable.length === 0)
|
|
12955
14153
|
return ["No part waits for your decision now."];
|
|
12956
14154
|
}
|
|
12957
|
-
const lines = ["To decide, type one of these lines in your own
|
|
14155
|
+
const lines = ["To decide, type one of these lines in your own coding agent's session:"];
|
|
12958
14156
|
for (const part of decidable) {
|
|
12959
14157
|
for (const act of reviewActsFor(part.built))
|
|
12960
14158
|
lines.push(` ${typedLine(act, reference, part.part)}`);
|
|
@@ -13000,7 +14198,7 @@ var CHECKS_SUBHEADING = "An agent says these pass and Halfcycle never ran them.
|
|
|
13000
14198
|
function uncheckedClaimLine(test) {
|
|
13001
14199
|
return `Unchecked: ${test.claimedBy} says "${test.label}" passes. Halfcycle never ran it.`;
|
|
13002
14200
|
}
|
|
13003
|
-
var ANSWER_CLAIMS_BY_TYPING_LINE = "To answer them, type the reject or accept line listed above in your own
|
|
14201
|
+
var ANSWER_CLAIMS_BY_TYPING_LINE = "To answer them, type the reject or accept line listed above in your own coding agent's session.";
|
|
13004
14202
|
function checksSectionLines(work) {
|
|
13005
14203
|
const unchecked = work.tasks.flatMap((task) => task.tests.filter((t) => t.state === "unchecked"));
|
|
13006
14204
|
if (unchecked.length === 0)
|
|
@@ -13421,7 +14619,7 @@ function typedReviewHintLine(offered, phase) {
|
|
|
13421
14619
|
return `[halfcycle] ${TYPED_REVIEW_OFF_SENTENCE}`;
|
|
13422
14620
|
const accept = typedAcceptLine(phase);
|
|
13423
14621
|
const lines = accept === null ? "then type one decision per line, as the whole message." : `then type ${accept} as the whole message to accept every built part, or one line per part, such as ${accept}/<part>; the list gives each part's lines.`;
|
|
13424
|
-
return `[halfcycle] The owner reviews each part in their own
|
|
14622
|
+
return `[halfcycle] The owner reviews each part in their own coding agent's session: they say "review what was built" to see the list, ${lines}`;
|
|
13425
14623
|
}
|
|
13426
14624
|
function systemViewReviewLine(offered, engagementId, phase) {
|
|
13427
14625
|
const address = systemViewUrl(engagementId, phase);
|
|
@@ -13430,7 +14628,7 @@ function systemViewReviewLine(offered, engagementId, phase) {
|
|
|
13430
14628
|
if (!offered)
|
|
13431
14629
|
return `[halfcycle] The owner reviews it in the System view: ${address}`;
|
|
13432
14630
|
const accept = typedAcceptLine(phase);
|
|
13433
|
-
const instead = accept === null ? "typing in their own
|
|
14631
|
+
const instead = accept === null ? "typing in their own coding agent's session" : `typing ${accept} in their own coding agent's session`;
|
|
13434
14632
|
return `[halfcycle] Or, instead of ${instead}, the owner reviews it in the System view: ${address}`;
|
|
13435
14633
|
}
|
|
13436
14634
|
function tasksOpenLine(phase, open, more) {
|
|
@@ -14255,6 +15453,35 @@ function readJsonFile(repoRoot, rel, parse) {
|
|
|
14255
15453
|
}
|
|
14256
15454
|
return { kind: "ok", value: parsed.data };
|
|
14257
15455
|
}
|
|
15456
|
+
function readSystemParts(repoRoot) {
|
|
15457
|
+
const file = readDescriptionFile(repoRoot);
|
|
15458
|
+
if (file.kind === "missing")
|
|
15459
|
+
return { kind: "none" };
|
|
15460
|
+
if (file.kind === "unreadable") {
|
|
15461
|
+
return {
|
|
15462
|
+
kind: "bad",
|
|
15463
|
+
line: `${SYSTEM_DESCRIPTION_REL} could not be read. Check that it is a file you can open, then run this again.
|
|
15464
|
+
`
|
|
15465
|
+
};
|
|
15466
|
+
}
|
|
15467
|
+
if (file.kind === "not-json") {
|
|
15468
|
+
return {
|
|
15469
|
+
kind: "bad",
|
|
15470
|
+
line: `${SYSTEM_DESCRIPTION_REL} is not valid JSON (${file.error}). Fix it, then run this again.
|
|
15471
|
+
`
|
|
15472
|
+
};
|
|
15473
|
+
}
|
|
15474
|
+
const checked = checkSystemDescription(file.value);
|
|
15475
|
+
if (!checked.ok) {
|
|
15476
|
+
const n = checked.problems.length;
|
|
15477
|
+
return {
|
|
15478
|
+
kind: "bad",
|
|
15479
|
+
line: `${SYSTEM_DESCRIPTION_REL} has ${n} ${n === 1 ? "problem" : "problems"}, so the plan could not be checked against it:
|
|
15480
|
+
` + problemLines(file.value, checked.problems)
|
|
15481
|
+
};
|
|
15482
|
+
}
|
|
15483
|
+
return { kind: "ok", partIds: checked.description.parts.map((part) => part.id) };
|
|
15484
|
+
}
|
|
14258
15485
|
function runCheckTasks(repoRoot, phase, withTaskList) {
|
|
14259
15486
|
const p = "halfcycle check-tasks:";
|
|
14260
15487
|
const planRead = readJsonFile(repoRoot, TASK_PLAN_FILE, (raw) => taskPlanSchema.safeParse(raw));
|
|
@@ -14280,7 +15507,15 @@ function runCheckTasks(repoRoot, phase, withTaskList) {
|
|
|
14280
15507
|
}
|
|
14281
15508
|
taskList = { file: listRead.kind === "ok" ? listRead.value : null };
|
|
14282
15509
|
}
|
|
14283
|
-
const
|
|
15510
|
+
const system = readSystemParts(repoRoot);
|
|
15511
|
+
if (system.kind === "bad") {
|
|
15512
|
+
return { stdout: "", stderr: `${p} ${system.line}`, exitCode: 2 };
|
|
15513
|
+
}
|
|
15514
|
+
const result = checkTaskPlan(plan, {
|
|
15515
|
+
phase,
|
|
15516
|
+
taskList,
|
|
15517
|
+
...system.kind === "ok" ? { system: { file: SYSTEM_DESCRIPTION_REL, partIds: system.partIds } } : {}
|
|
15518
|
+
});
|
|
14284
15519
|
const stdout = result.lines.map((line) => `${line.text}
|
|
14285
15520
|
`).join("");
|
|
14286
15521
|
return { stdout, stderr: "", exitCode: result.refused ? 1 : 0 };
|
|
@@ -14339,7 +15574,7 @@ function runProveChild(binary, credential, repoRoot, words, deps = {}) {
|
|
|
14339
15574
|
|
|
14340
15575
|
// dist/uninstall.js
|
|
14341
15576
|
import { lstatSync as lstatSync3, readFileSync as readFileSync19, readdirSync, rmSync as rmSync6, rmdirSync, writeFileSync as writeFileSync8 } from "node:fs";
|
|
14342
|
-
import { homedir as
|
|
15577
|
+
import { homedir as homedir5 } from "node:os";
|
|
14343
15578
|
import { dirname as dirname7, join as join22, relative as relative6, sep as sep2 } from "node:path";
|
|
14344
15579
|
var REMOVE_CREDENTIAL_FLAG = "--remove-credential";
|
|
14345
15580
|
function parseUninstallArgs(args2) {
|
|
@@ -14363,19 +15598,6 @@ function lexists(path) {
|
|
|
14363
15598
|
return false;
|
|
14364
15599
|
}
|
|
14365
15600
|
}
|
|
14366
|
-
function symlinkOnPath(root, rel) {
|
|
14367
|
-
let current = root;
|
|
14368
|
-
for (const segment of rel.split("/")) {
|
|
14369
|
-
current = join22(current, segment);
|
|
14370
|
-
try {
|
|
14371
|
-
if (lstatSync3(current).isSymbolicLink())
|
|
14372
|
-
return true;
|
|
14373
|
-
} catch {
|
|
14374
|
-
return false;
|
|
14375
|
-
}
|
|
14376
|
-
}
|
|
14377
|
-
return false;
|
|
14378
|
-
}
|
|
14379
15601
|
function isPlainObject3(value) {
|
|
14380
15602
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
14381
15603
|
}
|
|
@@ -14453,7 +15675,7 @@ function validate(options, rules) {
|
|
|
14453
15675
|
throw new Refusal(`the project id in ${BUNDLE_PIN_REL} is not a valid Halfcycle project id, so it cannot name a folder on this machine. Nothing was removed, here or under ~/.halfcycle.`);
|
|
14454
15676
|
}
|
|
14455
15677
|
if (engagementId !== void 0) {
|
|
14456
|
-
const expectedParent = join22(home ??
|
|
15678
|
+
const expectedParent = join22(home ?? homedir5(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME);
|
|
14457
15679
|
if (dirname7(engagementStateDir(engagementId, home)) !== expectedParent) {
|
|
14458
15680
|
throw new Refusal(`the project id in ${BUNDLE_PIN_REL} does not name a folder under ~/.halfcycle/engagements. Nothing was removed.`);
|
|
14459
15681
|
}
|
|
@@ -14464,14 +15686,16 @@ function validate(options, rules) {
|
|
|
14464
15686
|
engagementId,
|
|
14465
15687
|
idValid,
|
|
14466
15688
|
settings: readMergedJson(targetRepo, rules.settings),
|
|
14467
|
-
mcp: readMergedJson(targetRepo, rules.mcp)
|
|
15689
|
+
mcp: readMergedJson(targetRepo, rules.mcp),
|
|
15690
|
+
codexHooks: readMergedJson(targetRepo, rules.codexHooks)
|
|
14468
15691
|
};
|
|
14469
15692
|
}
|
|
14470
15693
|
function mergedFileRules(writeSet) {
|
|
14471
15694
|
const spelledIn = (spellings) => (text) => spellings.some((spelling) => text.includes(spelling));
|
|
14472
15695
|
return {
|
|
14473
15696
|
settings: { rel: SETTINGS_REL, holdsOurs: spelledIn([...writeSet.hookCommands].map((c) => JSON.stringify(c))) },
|
|
14474
|
-
mcp: { rel: MCP_REGISTRATION_REL, holdsOurs: spelledIn([JSON.stringify(MCP_SERVER_KEY)]) }
|
|
15697
|
+
mcp: { rel: MCP_REGISTRATION_REL, holdsOurs: spelledIn([JSON.stringify(MCP_SERVER_KEY)]) },
|
|
15698
|
+
codexHooks: { rel: CODEX_HOOKS_REL2, holdsOurs: spelledIn([...writeSet.codexHookCommands].map((c) => JSON.stringify(c))) }
|
|
14475
15699
|
};
|
|
14476
15700
|
}
|
|
14477
15701
|
function writeBack(targetRepo, rel, current, next, lists, name) {
|
|
@@ -14560,48 +15784,12 @@ function ourMcpServers(value) {
|
|
|
14560
15784
|
return isPlainObject3(servers) && MCP_SERVER_KEY in servers ? servers : null;
|
|
14561
15785
|
}
|
|
14562
15786
|
function unmergeSettings(value, rec, writeSet) {
|
|
14563
|
-
let changed = false;
|
|
14564
15787
|
const refused3 = [];
|
|
14565
|
-
const commands = new Set(writeSet.hookCommands);
|
|
14566
15788
|
const denyHashes = rec.denyAddedSha256.filter((h) => writeSet.denyRuleSha256.has(h));
|
|
14567
15789
|
if (denyHashes.length < rec.denyAddedSha256.length) {
|
|
14568
15790
|
refused3.push(`${SETTINGS_REL}: a deny rule the install record names but Halfcycle never adds (it was kept)`);
|
|
14569
15791
|
}
|
|
14570
|
-
|
|
14571
|
-
if (isPlainObject3(hooks)) {
|
|
14572
|
-
const ours = new Set(ourHookEntries(value, commands).map(({ entry }) => entry));
|
|
14573
|
-
let emptiedAnEvent = false;
|
|
14574
|
-
for (const event of Object.keys(hooks)) {
|
|
14575
|
-
const entries = hooks[event];
|
|
14576
|
-
if (!Array.isArray(entries))
|
|
14577
|
-
continue;
|
|
14578
|
-
let touched = false;
|
|
14579
|
-
const next = [];
|
|
14580
|
-
for (const entry of entries) {
|
|
14581
|
-
if (!ours.has(entry)) {
|
|
14582
|
-
next.push(entry);
|
|
14583
|
-
continue;
|
|
14584
|
-
}
|
|
14585
|
-
touched = true;
|
|
14586
|
-
const remaining = withoutHalfcycleCommands(entry, commands);
|
|
14587
|
-
if (remaining !== null)
|
|
14588
|
-
next.push(remaining);
|
|
14589
|
-
}
|
|
14590
|
-
if (touched)
|
|
14591
|
-
changed = true;
|
|
14592
|
-
if (next.length === 0 && (rec.eventsCreated.includes(event) || rec.adopted && touched)) {
|
|
14593
|
-
delete hooks[event];
|
|
14594
|
-
changed = true;
|
|
14595
|
-
emptiedAnEvent = true;
|
|
14596
|
-
} else if (touched) {
|
|
14597
|
-
hooks[event] = next;
|
|
14598
|
-
}
|
|
14599
|
-
}
|
|
14600
|
-
if (Object.keys(hooks).length === 0 && (rec.hooksCreated || rec.adopted && emptiedAnEvent)) {
|
|
14601
|
-
delete value["hooks"];
|
|
14602
|
-
changed = true;
|
|
14603
|
-
}
|
|
14604
|
-
}
|
|
15792
|
+
let changed = unmergeHooks(value, rec, new Set(writeSet.hookCommands));
|
|
14605
15793
|
const permissions = value["permissions"];
|
|
14606
15794
|
if (isPlainObject3(permissions) && Array.isArray(permissions["deny"])) {
|
|
14607
15795
|
const deny = permissions["deny"];
|
|
@@ -14637,6 +15825,52 @@ function unmergeSettings(value, rec, writeSet) {
|
|
|
14637
15825
|
}
|
|
14638
15826
|
return { changed, refused: refused3 };
|
|
14639
15827
|
}
|
|
15828
|
+
function unmergeHooks(value, rec, commands) {
|
|
15829
|
+
const hooks = value["hooks"];
|
|
15830
|
+
if (!isPlainObject3(hooks))
|
|
15831
|
+
return false;
|
|
15832
|
+
let changed = false;
|
|
15833
|
+
const ours = new Set(ourHookEntries(value, commands).map(({ entry }) => entry));
|
|
15834
|
+
let emptiedAnEvent = false;
|
|
15835
|
+
for (const event of Object.keys(hooks)) {
|
|
15836
|
+
const entries = hooks[event];
|
|
15837
|
+
if (!Array.isArray(entries))
|
|
15838
|
+
continue;
|
|
15839
|
+
let touched = false;
|
|
15840
|
+
const next = [];
|
|
15841
|
+
for (const entry of entries) {
|
|
15842
|
+
if (!ours.has(entry)) {
|
|
15843
|
+
next.push(entry);
|
|
15844
|
+
continue;
|
|
15845
|
+
}
|
|
15846
|
+
touched = true;
|
|
15847
|
+
const remaining = withoutHalfcycleCommands(entry, commands);
|
|
15848
|
+
if (remaining !== null)
|
|
15849
|
+
next.push(remaining);
|
|
15850
|
+
}
|
|
15851
|
+
if (touched)
|
|
15852
|
+
changed = true;
|
|
15853
|
+
if (next.length === 0 && (rec.eventsCreated.includes(event) || rec.adopted && touched)) {
|
|
15854
|
+
delete hooks[event];
|
|
15855
|
+
changed = true;
|
|
15856
|
+
emptiedAnEvent = true;
|
|
15857
|
+
} else if (touched) {
|
|
15858
|
+
hooks[event] = next;
|
|
15859
|
+
}
|
|
15860
|
+
}
|
|
15861
|
+
if (Object.keys(hooks).length === 0 && (rec.hooksCreated || rec.adopted && emptiedAnEvent)) {
|
|
15862
|
+
delete value["hooks"];
|
|
15863
|
+
changed = true;
|
|
15864
|
+
}
|
|
15865
|
+
return changed;
|
|
15866
|
+
}
|
|
15867
|
+
var BEFORE_UNKNOWN_CODEX_HOOKS = {
|
|
15868
|
+
created: false,
|
|
15869
|
+
adopted: true,
|
|
15870
|
+
hookCommands: [],
|
|
15871
|
+
eventsCreated: [],
|
|
15872
|
+
hooksCreated: false
|
|
15873
|
+
};
|
|
14640
15874
|
function unmergeMcp(value, rec) {
|
|
14641
15875
|
const servers = value["mcpServers"];
|
|
14642
15876
|
if (!isPlainObject3(servers))
|
|
@@ -14744,7 +15978,8 @@ function coveredPathExists(targetRepo, line) {
|
|
|
14744
15978
|
function selfBuildMergedFileRules() {
|
|
14745
15979
|
return {
|
|
14746
15980
|
settings: { rel: SETTINGS_REL, holdsOurs: () => false },
|
|
14747
|
-
mcp: { rel: MCP_REGISTRATION_REL, holdsOurs: (text) => text.includes(JSON.stringify(MCP_SERVER_KEY)) }
|
|
15981
|
+
mcp: { rel: MCP_REGISTRATION_REL, holdsOurs: (text) => text.includes(JSON.stringify(MCP_SERVER_KEY)) },
|
|
15982
|
+
codexHooks: { rel: CODEX_HOOKS_REL2, holdsOurs: () => false }
|
|
14748
15983
|
};
|
|
14749
15984
|
}
|
|
14750
15985
|
function removeSelfBuildOwnedFile(targetRepo, rel, lists, name) {
|
|
@@ -14798,6 +16033,33 @@ function removeSelfBuildCrewRoster(targetRepo, lists, name) {
|
|
|
14798
16033
|
name(lists.failed, rel, rel);
|
|
14799
16034
|
}
|
|
14800
16035
|
}
|
|
16036
|
+
function removeSelfBuildCrewProfile(targetRepo, rel, text, lists, name) {
|
|
16037
|
+
if (symlinkOnPath(targetRepo, rel)) {
|
|
16038
|
+
name(lists.leftAlone, `${rel} (a symbolic link, or inside one \u2014 not followed)`, rel);
|
|
16039
|
+
return;
|
|
16040
|
+
}
|
|
16041
|
+
const abs = join22(targetRepo, rel);
|
|
16042
|
+
let bytes;
|
|
16043
|
+
try {
|
|
16044
|
+
bytes = readFileSync19(abs);
|
|
16045
|
+
} catch {
|
|
16046
|
+
return;
|
|
16047
|
+
}
|
|
16048
|
+
if (!lstatSync3(abs).isFile()) {
|
|
16049
|
+
name(lists.leftAlone, `${rel} (not a file)`, rel);
|
|
16050
|
+
return;
|
|
16051
|
+
}
|
|
16052
|
+
if (bytes.toString("utf-8") !== text) {
|
|
16053
|
+
name(lists.kept, rel, rel);
|
|
16054
|
+
return;
|
|
16055
|
+
}
|
|
16056
|
+
try {
|
|
16057
|
+
rmSync6(abs);
|
|
16058
|
+
name(lists.removed, rel, rel);
|
|
16059
|
+
} catch {
|
|
16060
|
+
name(lists.failed, rel, rel);
|
|
16061
|
+
}
|
|
16062
|
+
}
|
|
14801
16063
|
function removeSelfBuildEmptyDir(targetRepo, dir, lists, name) {
|
|
14802
16064
|
if (symlinkOnPath(targetRepo, dir))
|
|
14803
16065
|
return;
|
|
@@ -14847,8 +16109,13 @@ function uninstallSelfBuild(options) {
|
|
|
14847
16109
|
removeSelfBuildOwnedFile(targetRepo, rel, result, name);
|
|
14848
16110
|
}
|
|
14849
16111
|
removeSelfBuildCrewRoster(targetRepo, result, name);
|
|
16112
|
+
for (const { rel, text } of renderCrewProfiles()) {
|
|
16113
|
+
removeSelfBuildCrewProfile(targetRepo, rel, text, result, name);
|
|
16114
|
+
}
|
|
14850
16115
|
removeSelfBuildEmptyDir(targetRepo, ".halfcycle/bin", result, name);
|
|
14851
16116
|
removeSelfBuildEmptyDir(targetRepo, ".halfcycle", result, name);
|
|
16117
|
+
removeSelfBuildEmptyDir(targetRepo, ".codex/agents", result, name);
|
|
16118
|
+
removeSelfBuildEmptyDir(targetRepo, ".codex", result, name);
|
|
14852
16119
|
const foundNothing = result.removed.length + result.restored.length + result.kept.length + result.failed.length === 0;
|
|
14853
16120
|
if (!pinExisted && foundNothing) {
|
|
14854
16121
|
return { outcome: "not-installed", exitCode: 0, ...result, credential: nothingToSay };
|
|
@@ -14884,6 +16151,7 @@ function uninstall(options) {
|
|
|
14884
16151
|
const { targetRepo } = options;
|
|
14885
16152
|
const manifest = checked.manifest ?? noRecord(targetRepo, writeSet);
|
|
14886
16153
|
const deleteWhenEmpty = (created) => created || checked.manifest === null;
|
|
16154
|
+
const deleteCodexWhenEmpty = (record2) => record2 === void 0 || deleteWhenEmpty(record2.created);
|
|
14887
16155
|
const named = /* @__PURE__ */ new Set();
|
|
14888
16156
|
const name = (list2, text, path) => {
|
|
14889
16157
|
list2.push(text);
|
|
@@ -14902,6 +16170,13 @@ function uninstall(options) {
|
|
|
14902
16170
|
changed: unmergeMcp(value, mcpRecord).changed,
|
|
14903
16171
|
adopted: false
|
|
14904
16172
|
}));
|
|
16173
|
+
const codexRecord = manifest.codexHooks ?? BEFORE_UNKNOWN_CODEX_HOOKS;
|
|
16174
|
+
const codexCommands = new Set(writeSet.codexHookCommands);
|
|
16175
|
+
applyMergedJson(targetRepo, rules.codexHooks, checked.codexHooks, deleteCodexWhenEmpty(manifest.codexHooks), result, name, (value) => ({
|
|
16176
|
+
changed: unmergeHooks(value, codexRecord, codexCommands),
|
|
16177
|
+
adopted: false
|
|
16178
|
+
}));
|
|
16179
|
+
removeCodexConfigBlock(targetRepo, manifest.codexConfig, deleteCodexWhenEmpty(manifest.codexConfig), result, name);
|
|
14905
16180
|
const deferred = /* @__PURE__ */ new Set([BUNDLE_PIN_REL, INSTALL_MANIFEST_REL]);
|
|
14906
16181
|
const recorded = new Map(manifest.files.map((entry) => [entry.path, entry]));
|
|
14907
16182
|
for (const path of [.../* @__PURE__ */ new Set([...recorded.keys(), ...writeSet.files])].sort()) {
|
|
@@ -14985,6 +16260,44 @@ function applyMergedJson(targetRepo, rule, file, deleteWhenEmpty, lists, name, e
|
|
|
14985
16260
|
name(lists.leftAlone, `${rel} (Halfcycle's hooks were taken out. There is no record of this file from before the install, so a permission rule or "$schema" Halfcycle added may still be there \u2014 it cannot be told apart from your own)`, rel);
|
|
14986
16261
|
}
|
|
14987
16262
|
}
|
|
16263
|
+
function removeCodexConfigBlock(targetRepo, record2, deleteWhenEmpty, lists, name) {
|
|
16264
|
+
const rel = CODEX_CONFIG_REL2;
|
|
16265
|
+
const path = join22(targetRepo, rel);
|
|
16266
|
+
if (symlinkOnPath(targetRepo, rel)) {
|
|
16267
|
+
if (lexists(path))
|
|
16268
|
+
name(lists.leftAlone, `${rel} (a symbolic link, or inside one \u2014 not followed)`, rel);
|
|
16269
|
+
return;
|
|
16270
|
+
}
|
|
16271
|
+
let text;
|
|
16272
|
+
try {
|
|
16273
|
+
if (!lstatSync3(path).isFile())
|
|
16274
|
+
return;
|
|
16275
|
+
text = readFileSync19(path, "utf-8");
|
|
16276
|
+
} catch {
|
|
16277
|
+
return;
|
|
16278
|
+
}
|
|
16279
|
+
const span = codexBlockSpan(text);
|
|
16280
|
+
if (span === "none")
|
|
16281
|
+
return;
|
|
16282
|
+
const changed = () => name(lists.kept, `${rel} (Halfcycle's part of it was changed after it was written, so it was kept)`, rel);
|
|
16283
|
+
if (span === "broken")
|
|
16284
|
+
return changed();
|
|
16285
|
+
let cut = null;
|
|
16286
|
+
const at = record2 === void 0 ? -1 : record2.block.indexOf(CODEX_CONFIG_OPEN_MARKER);
|
|
16287
|
+
if (record2 !== void 0 && at >= 0) {
|
|
16288
|
+
const found = regeneratesCodexBlock(record2.block.slice(at)) ? lastOccurrence(text, record2.block) : null;
|
|
16289
|
+
if (found !== null)
|
|
16290
|
+
cut = found;
|
|
16291
|
+
} else if (regeneratesCodexBlock(text.slice(span.start, span.end))) {
|
|
16292
|
+
cut = span;
|
|
16293
|
+
}
|
|
16294
|
+
if (cut === null)
|
|
16295
|
+
return changed();
|
|
16296
|
+
const head = text.slice(0, cut.start);
|
|
16297
|
+
const tail = text.slice(cut.end);
|
|
16298
|
+
const next = head + (head !== "" && tail !== "" && !head.endsWith("\n") ? lineEndingOf(text) : "") + tail;
|
|
16299
|
+
writeBack(targetRepo, rel, text, next === "" && deleteWhenEmpty ? null : next, lists, name);
|
|
16300
|
+
}
|
|
14988
16301
|
function removeInstalledFile(targetRepo, path, entry, writeSet, lists, name) {
|
|
14989
16302
|
if (!writeSet.files.has(path)) {
|
|
14990
16303
|
name(lists.leftAlone, `${path} (Halfcycle never writes this path, so it was not removed)`, path);
|
|
@@ -15116,7 +16429,7 @@ function filesUnder(root, rel) {
|
|
|
15116
16429
|
return out;
|
|
15117
16430
|
}
|
|
15118
16431
|
function shownPath(path, home) {
|
|
15119
|
-
const base = home ??
|
|
16432
|
+
const base = home ?? homedir5();
|
|
15120
16433
|
const rel = relative6(base, path);
|
|
15121
16434
|
const under = rel !== "" && !rel.startsWith("..") && !rel.startsWith(sep2);
|
|
15122
16435
|
return `${under ? `~/${rel.split(sep2).join("/")}` : path}/`;
|
|
@@ -15170,7 +16483,7 @@ function renderUninstallReport(result, home) {
|
|
|
15170
16483
|
case "nothing-to-say":
|
|
15171
16484
|
break;
|
|
15172
16485
|
}
|
|
15173
|
-
out.push("[halfcycle] Your project on Halfcycle is unchanged. Restart any
|
|
16486
|
+
out.push("[halfcycle] Your project on Halfcycle is unchanged. Restart any coding agent session open in this project.");
|
|
15174
16487
|
return { stdout: out.join("\n") + "\n", stderr: "" };
|
|
15175
16488
|
}
|
|
15176
16489
|
|
|
@@ -15198,9 +16511,13 @@ function bundleVersion() {
|
|
|
15198
16511
|
function claudeCodeVersion(deps = {}) {
|
|
15199
16512
|
return probeClaudeCodeVersion(3e3, deps);
|
|
15200
16513
|
}
|
|
16514
|
+
function codexVersion(deps = {}) {
|
|
16515
|
+
return probeCodexVersion(3e3, deps);
|
|
16516
|
+
}
|
|
15201
16517
|
var REAL_PROBES = {
|
|
15202
16518
|
bundleVersion,
|
|
15203
16519
|
claudeCodeVersion,
|
|
16520
|
+
codexVersion,
|
|
15204
16521
|
projectName: (dir) => {
|
|
15205
16522
|
try {
|
|
15206
16523
|
return deriveProjectName(dir);
|
|
@@ -15248,6 +16565,9 @@ function openingBannerContent(projectDir, env = process.env, probes = REAL_PROBE
|
|
|
15248
16565
|
const facts = [
|
|
15249
16566
|
fact("Version", version, version),
|
|
15250
16567
|
fact("Claude Code", probes.claudeCodeVersion()),
|
|
16568
|
+
// Beside the `Claude Code` row, same rule: a probe that cannot answer omits the
|
|
16569
|
+
// row rather than guessing (`codex-install-target`, *The Codex version*).
|
|
16570
|
+
fact("Codex", probes.codexVersion()),
|
|
15251
16571
|
fact("Project", project, name),
|
|
15252
16572
|
fact("Status", status, installed === void 0 ? "new" : "installed"),
|
|
15253
16573
|
// `Website`, NOT `Plane` — see this function's docblock. (No issue number here:
|
|
@@ -15420,8 +16740,8 @@ async function runPace(repoRoot, start, deps) {
|
|
|
15420
16740
|
|
|
15421
16741
|
// dist/restore.js
|
|
15422
16742
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
15423
|
-
import { closeSync as closeSync3, fchmodSync, lstatSync as lstatSync4, mkdirSync as mkdirSync11, openSync as openSync3, readdirSync as readdirSync2, readFileSync as readFileSync21, readlinkSync, realpathSync as
|
|
15424
|
-
import { homedir as
|
|
16743
|
+
import { closeSync as closeSync3, fchmodSync, lstatSync as lstatSync4, mkdirSync as mkdirSync11, openSync as openSync3, readdirSync as readdirSync2, readFileSync as readFileSync21, readlinkSync, realpathSync as realpathSync6, rmSync as rmSync7, symlinkSync, writeSync as writeSync2 } from "node:fs";
|
|
16744
|
+
import { homedir as homedir6 } from "node:os";
|
|
15425
16745
|
import { join as join24, resolve as resolve7 } from "node:path";
|
|
15426
16746
|
var RESTORE_USAGE = `${HALFCYCLE_COMMAND} restore [<id>]`;
|
|
15427
16747
|
function parseRestoreArgs(rest) {
|
|
@@ -15444,7 +16764,7 @@ function runRestore(cwd, id, options = {}) {
|
|
|
15444
16764
|
if (engagementId === null) {
|
|
15445
16765
|
return refused2(`Halfcycle is not set up in this project, so it has saved nothing here. To set it up, run: ${HALFCYCLE_COMMAND}`);
|
|
15446
16766
|
}
|
|
15447
|
-
const folder = join24(options.home ??
|
|
16767
|
+
const folder = join24(options.home ?? homedir6(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME, engagementId, SAVED_DIR_NAME);
|
|
15448
16768
|
const refs = savedRefs(root);
|
|
15449
16769
|
if (refs === null)
|
|
15450
16770
|
return refused2("Halfcycle could not read the saved copies in this project, so nothing was put back.");
|
|
@@ -15830,7 +17150,7 @@ function lstatOrNull3(p) {
|
|
|
15830
17150
|
}
|
|
15831
17151
|
function realOrSelf(p) {
|
|
15832
17152
|
try {
|
|
15833
|
-
return
|
|
17153
|
+
return realpathSync6(p);
|
|
15834
17154
|
} catch {
|
|
15835
17155
|
return p;
|
|
15836
17156
|
}
|
|
@@ -15877,6 +17197,15 @@ function reportClaudeCodeVersion(verbose2, deps = {}) {
|
|
|
15877
17197
|
process.stdout.write(`[halfcycle] WARNING: Claude Code ${version} is older than ${MIN_HEADERS_HELPER_VERSION}, which ignores the credential helper this install wrote. The Halfcycle server will connect with no credential and every call will fail \u2014 ${upgradeInstruction}, then re-open this folder.
|
|
15878
17198
|
`);
|
|
15879
17199
|
}
|
|
17200
|
+
function reportCodexVersion(deps = {}) {
|
|
17201
|
+
const version = probeCodexVersion(5e3, deps);
|
|
17202
|
+
if (version === void 0)
|
|
17203
|
+
return;
|
|
17204
|
+
if (compareVersions(version, CODEX_MIN_VERSION) < 0) {
|
|
17205
|
+
process.stdout.write(`${codexVersionWarningLine(version)}
|
|
17206
|
+
`);
|
|
17207
|
+
}
|
|
17208
|
+
}
|
|
15880
17209
|
function shortId(id) {
|
|
15881
17210
|
return id.length > 10 ? `${id.slice(0, 8)}\u2026` : id;
|
|
15882
17211
|
}
|
|
@@ -16310,6 +17639,7 @@ ${PROJECT_RESET_LINE}
|
|
|
16310
17639
|
`);
|
|
16311
17640
|
}
|
|
16312
17641
|
reportClaudeCodeVersion(verbose);
|
|
17642
|
+
reportCodexVersion();
|
|
16313
17643
|
const identity = actingIdentity ?? heldAnswer?.identity ?? (credential && heldAnswer === void 0 ? await describeAccount(credential.serviceUrl, credential.token) : void 0);
|
|
16314
17644
|
const written = result.writtenPaths.length;
|
|
16315
17645
|
const merged = result.mergedPaths.length;
|
|
@@ -16336,6 +17666,10 @@ ${PROJECT_RESET_LINE}
|
|
|
16336
17666
|
process.stdout.write(`[halfcycle] Next: open this folder in Claude Code \u2014 accept the workspace-trust prompt, it is expected \u2014 then say "set up this project" and answer what it asks about your project
|
|
16337
17667
|
`);
|
|
16338
17668
|
process.stdout.write(`[halfcycle] Claude Code will then ask you to approve the new "halfcycle" MCP server. If it is the only one listed, the cursor starts on "Continue without using this MCP server" \u2014 choose "Use this MCP server" instead. If several are listed together, halfcycle is already checked \u2014 leave it that way. Either way, without it Halfcycle will not connect.
|
|
17669
|
+
`);
|
|
17670
|
+
const codexLine2 = codexInstallLine(result.codex);
|
|
17671
|
+
if (codexLine2 !== void 0)
|
|
17672
|
+
process.stdout.write(`${codexLine2}
|
|
16339
17673
|
`);
|
|
16340
17674
|
}
|
|
16341
17675
|
const consoleUrl = credential?.consoleUrl ?? (reusedWithoutFreshAnswer ? readBundlePin(targetRepo)?.projectPage : void 0);
|