halfcycle 0.4.17 → 0.5.0
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 +1620 -632
- 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 +1475 -120
- package/dist/cli.js.map +3 -3
- 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 +1078 -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 +19 -4
- 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
|
|
@@ -4851,7 +4962,8 @@ var chatReviewAcceptRequestSchema = z30.object({
|
|
|
4851
4962
|
act: z30.literal("accept"),
|
|
4852
4963
|
reference: phaseIdentitySchema,
|
|
4853
4964
|
reviewVersion: z30.number().int().min(0),
|
|
4854
|
-
parts: z30.array(systemPartIdSchema).max(REVIEW_PARTS_MAX)
|
|
4965
|
+
parts: z30.array(systemPartIdSchema).max(REVIEW_PARTS_MAX),
|
|
4966
|
+
waitingClaims: z30.literal(true).optional()
|
|
4855
4967
|
}).strict();
|
|
4856
4968
|
function notedRequest(act) {
|
|
4857
4969
|
return z30.object({
|
|
@@ -4866,12 +4978,20 @@ var chatReviewQuestionRequestSchema = notedRequest("question");
|
|
|
4866
4978
|
var chatReviewSendBackRequestSchema = notedRequest("send-back");
|
|
4867
4979
|
var chatReviewCarryRequestSchema = notedRequest("carry");
|
|
4868
4980
|
var chatReviewDropRequestSchema = notedRequest("drop");
|
|
4981
|
+
var chatReviewRejectRequestSchema = z30.object({
|
|
4982
|
+
act: z30.literal("reject"),
|
|
4983
|
+
reference: phaseIdentitySchema,
|
|
4984
|
+
reviewVersion: z30.number().int().min(0),
|
|
4985
|
+
task: buildTaskIdSchema,
|
|
4986
|
+
note: reviewNoteWireSchema
|
|
4987
|
+
}).strict();
|
|
4869
4988
|
var chatReviewRequestSchema = z30.discriminatedUnion("act", [
|
|
4870
4989
|
chatReviewAcceptRequestSchema,
|
|
4871
4990
|
chatReviewQuestionRequestSchema,
|
|
4872
4991
|
chatReviewSendBackRequestSchema,
|
|
4873
4992
|
chatReviewCarryRequestSchema,
|
|
4874
|
-
chatReviewDropRequestSchema
|
|
4993
|
+
chatReviewDropRequestSchema,
|
|
4994
|
+
chatReviewRejectRequestSchema
|
|
4875
4995
|
]);
|
|
4876
4996
|
function bareOutcome2(outcome) {
|
|
4877
4997
|
return z30.object({ outcome: z30.literal(outcome) }).strict();
|
|
@@ -4884,7 +5004,8 @@ var chatReviewResultSchema = z30.discriminatedUnion("outcome", [
|
|
|
4884
5004
|
...reviewDecisionResultSchema.options.filter(notTheConsoleNotDecidable),
|
|
4885
5005
|
bareOutcome2("show-first"),
|
|
4886
5006
|
bareOutcome2("not-offered"),
|
|
4887
|
-
chatAmbiguousSchema
|
|
5007
|
+
chatAmbiguousSchema,
|
|
5008
|
+
bareOutcome2("nothing-waiting")
|
|
4888
5009
|
]);
|
|
4889
5010
|
var reviewAnswerRequestSchema = z30.object({
|
|
4890
5011
|
part: systemPartIdSchema,
|
|
@@ -5348,8 +5469,8 @@ function autonomyA13(accepted, carried) {
|
|
|
5348
5469
|
return `Your autonomy setting accepted the ${String(accepted)} built parts and carried ${String(carried)} unbuilt parts forward. The phase is closed.`;
|
|
5349
5470
|
}
|
|
5350
5471
|
function autonomyA14(count, parts, acceptLine) {
|
|
5351
|
-
const typed = acceptLine === null ? 'say "review what was built" and type the lines it gives.' : `say "review what was built", then type ${acceptLine}.`;
|
|
5352
|
-
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
|
|
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.`;
|
|
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 Claude Code session, ` + typed;
|
|
5353
5474
|
}
|
|
5354
5475
|
function autonomyA15(count) {
|
|
5355
5476
|
return `The phase waits for you: ${String(count)} parts were built without an approved scope, and only you can decide them.`;
|
|
@@ -5853,7 +5974,14 @@ function serializeManifest(manifest) {
|
|
|
5853
5974
|
files: [...manifest.files].sort((a, b) => byString(a.path, b.path)),
|
|
5854
5975
|
createdDirs: [...manifest.createdDirs].sort(byString),
|
|
5855
5976
|
leftAlone: [...manifest.leftAlone].sort(byString),
|
|
5856
|
-
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
|
+
} : {}
|
|
5857
5985
|
};
|
|
5858
5986
|
return JSON.stringify(canonicalJson2(ordered), null, 2) + "\n";
|
|
5859
5987
|
}
|
|
@@ -5907,7 +6035,7 @@ function parseManifest(text) {
|
|
|
5907
6035
|
}
|
|
5908
6036
|
if (!isRecord2(raw))
|
|
5909
6037
|
throw new UnreadableManifestError("it is not a JSON object");
|
|
5910
|
-
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");
|
|
5911
6039
|
if (raw["format"] !== INSTALL_MANIFEST_FORMAT) {
|
|
5912
6040
|
throw new UnreadableManifestError(`its format is not ${INSTALL_MANIFEST_FORMAT}`);
|
|
5913
6041
|
}
|
|
@@ -5973,6 +6101,30 @@ function parseManifest(text) {
|
|
|
5973
6101
|
...typeof m["entrySha256"] === "string" ? { entrySha256: m["entrySha256"] } : {}
|
|
5974
6102
|
};
|
|
5975
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
|
+
}
|
|
5976
6128
|
if (raw["gitignore"] === void 0)
|
|
5977
6129
|
throw new UnreadableManifestError('"gitignore" is missing');
|
|
5978
6130
|
const gitignore = appendedTextRecord(raw["gitignore"], "gitignore");
|
|
@@ -5991,6 +6143,8 @@ function parseManifest(text) {
|
|
|
5991
6143
|
leftAlone,
|
|
5992
6144
|
settings,
|
|
5993
6145
|
...mcp !== void 0 ? { mcp } : {},
|
|
6146
|
+
...codexHooks !== void 0 ? { codexHooks } : {},
|
|
6147
|
+
...codexConfig !== void 0 ? { codexConfig } : {},
|
|
5994
6148
|
gitignore,
|
|
5995
6149
|
...gitattributes !== void 0 ? { gitattributes } : {}
|
|
5996
6150
|
};
|
|
@@ -6008,6 +6162,8 @@ function startManifest(previous, isMember, isMemberDir) {
|
|
|
6008
6162
|
createdDirs: new Set((previous?.createdDirs ?? []).filter(isMemberDir)),
|
|
6009
6163
|
settings: previous?.settings,
|
|
6010
6164
|
mcp: previous?.mcp,
|
|
6165
|
+
codexHooks: previous?.codexHooks,
|
|
6166
|
+
codexConfig: previous?.codexConfig,
|
|
6011
6167
|
gitignore: previous?.gitignore,
|
|
6012
6168
|
gitattributes: previous?.gitattributes
|
|
6013
6169
|
};
|
|
@@ -6068,6 +6224,19 @@ function recordMcp(draft, observed) {
|
|
|
6068
6224
|
...entrySha256 !== void 0 ? { entrySha256 } : {}
|
|
6069
6225
|
};
|
|
6070
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
|
+
}
|
|
6071
6240
|
function recordGitignore(draft, created, appended) {
|
|
6072
6241
|
draft.gitignore = carryAppended(draft.gitignore, created, appended);
|
|
6073
6242
|
}
|
|
@@ -6101,6 +6270,8 @@ function finishManifest(draft) {
|
|
|
6101
6270
|
denyCreated: false
|
|
6102
6271
|
},
|
|
6103
6272
|
...draft.mcp !== void 0 ? { mcp: draft.mcp } : {},
|
|
6273
|
+
...draft.codexHooks !== void 0 ? { codexHooks: draft.codexHooks } : {},
|
|
6274
|
+
...draft.codexConfig !== void 0 ? { codexConfig: draft.codexConfig } : {},
|
|
6104
6275
|
gitignore: draft.gitignore ?? { created: false, appended: [] },
|
|
6105
6276
|
...draft.gitattributes !== void 0 ? { gitattributes: draft.gitattributes } : {}
|
|
6106
6277
|
};
|
|
@@ -6225,6 +6396,30 @@ var PREVIEW_WORDS = Object.freeze({
|
|
|
6225
6396
|
change: "takes out an older Halfcycle sign-in; your own lines stay as they are",
|
|
6226
6397
|
leave: "an older Halfcycle sign-in that cannot be moved yet; left as it is",
|
|
6227
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
|
|
6228
6423
|
}
|
|
6229
6424
|
});
|
|
6230
6425
|
var kindTable;
|
|
@@ -6249,13 +6444,21 @@ function kinds() {
|
|
|
6249
6444
|
[RETIRED_MCP_HEADERS_HELPER_REL, "halfcycle-folder"],
|
|
6250
6445
|
[LEGACY_CI_STANZA_REL, "halfcycle-folder"],
|
|
6251
6446
|
[CAPTURED_INDEX_REL, "captured-index"],
|
|
6252
|
-
[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"]
|
|
6253
6450
|
]);
|
|
6254
6451
|
return kindTable;
|
|
6255
6452
|
}
|
|
6256
6453
|
function previewKindOf(rel) {
|
|
6257
6454
|
if (/^\.claude\/commands\/[^/]+\.md$/.test(rel))
|
|
6258
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";
|
|
6259
6462
|
return kinds().get(rel);
|
|
6260
6463
|
}
|
|
6261
6464
|
function kindOf(rel) {
|
|
@@ -6281,6 +6484,7 @@ function planInstall(options) {
|
|
|
6281
6484
|
const previous = readPreviousManifest(targetRepo);
|
|
6282
6485
|
const dirsBefore = new Set([...writeSet.dirs].filter((dir) => existsSync3(join5(targetRepo, dir))));
|
|
6283
6486
|
const { version, commands } = planCommandStubs(targetRepo, previous, writeSet);
|
|
6487
|
+
const codexSkills = planCodexSkills(targetRepo, previous, writeSet);
|
|
6284
6488
|
const vendored = planVendoredBinary(targetRepo);
|
|
6285
6489
|
let settings;
|
|
6286
6490
|
try {
|
|
@@ -6294,6 +6498,8 @@ function planInstall(options) {
|
|
|
6294
6498
|
const ciStanza = planLegacyCiStanzaRemoval(targetRepo);
|
|
6295
6499
|
const mcp = planMcpRegistration(targetRepo, preview ? "pending" : credential);
|
|
6296
6500
|
const mcpHelper = mcp.kind === "none" ? null : planMcpHeadersHelperRemoval(targetRepo);
|
|
6501
|
+
const codexHooks = planCodexHooks(targetRepo);
|
|
6502
|
+
const codexConfig = planCodexConfig(targetRepo, preview ? "pending" : credential);
|
|
6297
6503
|
const gitignore = planGitignore(targetRepo);
|
|
6298
6504
|
const gitattributes = planGitattributes(targetRepo);
|
|
6299
6505
|
const identity = planProjectIdentity(targetRepo);
|
|
@@ -6305,6 +6511,7 @@ function planInstall(options) {
|
|
|
6305
6511
|
const legacy = migrateLegacyEnvLocal(targetRepo, storedAfter);
|
|
6306
6512
|
const pin = preview ? planBundlePinPreview(targetRepo, version, home) : planBundlePin(targetRepo, version, engagementId, engagementType, createdByAccountId, credential?.methodVersion);
|
|
6307
6513
|
const crew = planCrewRoster(targetRepo);
|
|
6514
|
+
const crewProfiles = planCrewProfiles(targetRepo, previous, writeSet);
|
|
6308
6515
|
const scan = { text: planBootstrapScan(targetRepo).text };
|
|
6309
6516
|
const steps = {
|
|
6310
6517
|
targetRepo,
|
|
@@ -6317,12 +6524,15 @@ function planInstall(options) {
|
|
|
6317
6524
|
previous,
|
|
6318
6525
|
dirsBefore,
|
|
6319
6526
|
commands,
|
|
6527
|
+
codexSkills,
|
|
6320
6528
|
vendored,
|
|
6321
6529
|
settings,
|
|
6322
6530
|
hooks,
|
|
6323
6531
|
ciStanza,
|
|
6324
6532
|
mcp,
|
|
6325
6533
|
mcpHelper,
|
|
6534
|
+
codexHooks,
|
|
6535
|
+
codexConfig,
|
|
6326
6536
|
gitignore,
|
|
6327
6537
|
gitattributes,
|
|
6328
6538
|
identity,
|
|
@@ -6330,6 +6540,7 @@ function planInstall(options) {
|
|
|
6330
6540
|
legacy,
|
|
6331
6541
|
pin,
|
|
6332
6542
|
crew,
|
|
6543
|
+
crewProfiles,
|
|
6333
6544
|
scan
|
|
6334
6545
|
};
|
|
6335
6546
|
const record2 = preview ? null : planInstallRecord(targetRepo, accountInstall(steps, predicted(steps)).manifestText);
|
|
@@ -6355,16 +6566,24 @@ function predicted(steps) {
|
|
|
6355
6566
|
lands(planned.rel, planned.text, planned.outcome === "written");
|
|
6356
6567
|
};
|
|
6357
6568
|
steps.commands.forEach(file);
|
|
6569
|
+
steps.codexSkills.forEach(file);
|
|
6358
6570
|
file(steps.vendored);
|
|
6359
6571
|
file(steps.settings.file);
|
|
6360
6572
|
steps.hooks.forEach(file);
|
|
6361
6573
|
if (steps.mcp.kind === "write")
|
|
6362
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
|
+
}
|
|
6363
6581
|
if (steps.identity.after !== null)
|
|
6364
6582
|
lands(PROJECT_IDENTITY_REL, steps.identity.after, steps.identity.action === "add");
|
|
6365
6583
|
if (steps.pin !== null)
|
|
6366
6584
|
file(steps.pin);
|
|
6367
6585
|
file(steps.crew);
|
|
6586
|
+
steps.crewProfiles.forEach(file);
|
|
6368
6587
|
if (steps.scan.text !== null)
|
|
6369
6588
|
lands(BOOTSTRAP_STATE_REL, steps.scan.text, true);
|
|
6370
6589
|
const mergeAfter = (planned) => planned.outcome === "written" ? planned.after : planned.before;
|
|
@@ -6422,6 +6641,7 @@ function entriesOf(steps, record2) {
|
|
|
6422
6641
|
entries.push({ path: rel, action: there ? "change" : "add", kind: kindOf(rel), bytes: PENDING });
|
|
6423
6642
|
};
|
|
6424
6643
|
steps.commands.forEach(fromFile);
|
|
6644
|
+
steps.codexSkills.forEach(fromFile);
|
|
6425
6645
|
fromFile(steps.vendored);
|
|
6426
6646
|
const settings = steps.settings;
|
|
6427
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 });
|
|
@@ -6437,6 +6657,38 @@ function entriesOf(steps, record2) {
|
|
|
6437
6657
|
entries.push({ path: MCP_REGISTRATION_REL, action: "leave", kind: "server-entry", bytes: null });
|
|
6438
6658
|
}
|
|
6439
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
|
+
}
|
|
6440
6692
|
merge(GITIGNORE_REL, steps.gitignore);
|
|
6441
6693
|
merge(GITATTRIBUTES_REL, steps.gitattributes);
|
|
6442
6694
|
const identity = steps.identity;
|
|
@@ -6459,6 +6711,7 @@ function entriesOf(steps, record2) {
|
|
|
6459
6711
|
else
|
|
6460
6712
|
fromFile(steps.pin);
|
|
6461
6713
|
fromFile(steps.crew);
|
|
6714
|
+
steps.crewProfiles.forEach(fromFile);
|
|
6462
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 });
|
|
6463
6716
|
if (record2 === null)
|
|
6464
6717
|
pending(INSTALL_MANIFEST_REL);
|
|
@@ -6499,6 +6752,8 @@ function applyInstallPlan(plan) {
|
|
|
6499
6752
|
}
|
|
6500
6753
|
for (const command of steps.commands)
|
|
6501
6754
|
putPlannedFile(targetRepo, command);
|
|
6755
|
+
for (const skill of steps.codexSkills)
|
|
6756
|
+
putPlannedFile(targetRepo, skill);
|
|
6502
6757
|
putPlannedFile(targetRepo, steps.vendored);
|
|
6503
6758
|
putPlannedFile(targetRepo, steps.settings.file);
|
|
6504
6759
|
for (const hook of steps.hooks)
|
|
@@ -6506,6 +6761,8 @@ function applyInstallPlan(plan) {
|
|
|
6506
6761
|
const ciStanza = steps.ciStanza === "removed" ? removePlannedRetiredFile(targetRepo, LEGACY_CI_STANZA_REL) : steps.ciStanza;
|
|
6507
6762
|
putPlannedMcp(targetRepo, steps.mcp);
|
|
6508
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);
|
|
6509
6766
|
const gitignore = putPlannedGitignore(targetRepo, steps.gitignore);
|
|
6510
6767
|
const gitignorePath = join5(targetRepo, GITIGNORE_REL);
|
|
6511
6768
|
const gitignoreAfter = existsSync3(gitignorePath) ? readFileSync5(gitignorePath, "utf-8") : null;
|
|
@@ -6518,6 +6775,8 @@ function applyInstallPlan(plan) {
|
|
|
6518
6775
|
const legacy = applyLegacyMigration(targetRepo, migrateLegacyEnvLocal(targetRepo, stored));
|
|
6519
6776
|
writeBundlePin(targetRepo, pin, []);
|
|
6520
6777
|
putPlannedFile(targetRepo, steps.crew);
|
|
6778
|
+
for (const profile of steps.crewProfiles)
|
|
6779
|
+
putPlannedFile(targetRepo, profile);
|
|
6521
6780
|
if (steps.scan.text !== null)
|
|
6522
6781
|
writeBootstrapState(targetRepo, steps.scan.text);
|
|
6523
6782
|
const { report, installManifest, manifestText } = accountInstall(steps, {
|
|
@@ -6547,9 +6806,15 @@ function applyInstallPlan(plan) {
|
|
|
6547
6806
|
replacedPaths: report.replacedPaths,
|
|
6548
6807
|
bootstrapScanRan: steps.scan.text !== null,
|
|
6549
6808
|
legacyCredentialKept: legacy.outcome === "kept" ? { keys: legacy.unbacked, missing: legacy.missingForAdoption } : null,
|
|
6550
|
-
notShared
|
|
6809
|
+
notShared,
|
|
6810
|
+
codex: { hooks: steps.codexHooks.outcome, config: appliedCodexConfig(steps.codexConfig.outcome) }
|
|
6551
6811
|
};
|
|
6552
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
|
+
}
|
|
6553
6818
|
|
|
6554
6819
|
// dist/mcp-endpoint.js
|
|
6555
6820
|
var MCP_ENDPOINT_PATH = "/mcp";
|
|
@@ -6560,8 +6825,8 @@ function mcpEndpointUrl(origin) {
|
|
|
6560
6825
|
// dist/node-floor.js
|
|
6561
6826
|
var NODE_FLOOR_MAJOR = 18;
|
|
6562
6827
|
var NODE_FLOOR_SENTENCE_HEAD = `Halfcycle needs Node ${NODE_FLOOR_MAJOR} or newer; you have `;
|
|
6563
|
-
function nodeFloorProgram(guarded) {
|
|
6564
|
-
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}}`;
|
|
6565
6830
|
}
|
|
6566
6831
|
|
|
6567
6832
|
// dist/merge-settings.js
|
|
@@ -6608,6 +6873,75 @@ function mergeSettings(existing, generated, retiredOurs, statusLine) {
|
|
|
6608
6873
|
}
|
|
6609
6874
|
return merged;
|
|
6610
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
|
+
}
|
|
6611
6945
|
function mergePermissions(existing, generated, retiredOurs) {
|
|
6612
6946
|
if (existing === void 0 && generated === void 0)
|
|
6613
6947
|
return void 0;
|
|
@@ -6630,6 +6964,168 @@ function mergePermissions(existing, generated, retiredOurs) {
|
|
|
6630
6964
|
return result;
|
|
6631
6965
|
}
|
|
6632
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
|
+
|
|
6633
7129
|
// dist/install.js
|
|
6634
7130
|
function renderNotShared(finding) {
|
|
6635
7131
|
const rule = `rule "${finding.pattern}" in ${finding.source}, line ${finding.line}`;
|
|
@@ -6639,6 +7135,97 @@ function renderNotShared(finding) {
|
|
|
6639
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.`;
|
|
6640
7136
|
}
|
|
6641
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
|
+
}
|
|
6642
7229
|
var __filename = fileURLToPath(import.meta.url);
|
|
6643
7230
|
var __dirname = dirname3(__filename);
|
|
6644
7231
|
function findBundleRoot(startDir) {
|
|
@@ -6690,13 +7277,23 @@ var WRITE_ALLOWLIST = [
|
|
|
6690
7277
|
"test/fixtures/captured",
|
|
6691
7278
|
MCP_REGISTRATION_REL,
|
|
6692
7279
|
".gitattributes",
|
|
6693
|
-
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)
|
|
6694
7290
|
];
|
|
7291
|
+
var CREW_PROFILE_RELS = new Set(renderCrewProfiles().map((p) => p.rel));
|
|
6695
7292
|
function isAllowlisted(targetRelPath) {
|
|
6696
7293
|
const normalised = targetRelPath.replace(/\\/g, "/");
|
|
6697
7294
|
return WRITE_ALLOWLIST.some((prefix) => {
|
|
6698
7295
|
const p = prefix.replace(/\\/g, "/");
|
|
6699
|
-
return normalised === p || normalised.startsWith(p + "/");
|
|
7296
|
+
return normalised === p || !CREW_PROFILE_RELS.has(p) && normalised.startsWith(p + "/");
|
|
6700
7297
|
});
|
|
6701
7298
|
}
|
|
6702
7299
|
var CREW_ROSTER_REL = ".halfcycle/crew.json";
|
|
@@ -6709,6 +7306,12 @@ var GITATTRIBUTES_REL = ".gitattributes";
|
|
|
6709
7306
|
function commandStubRel(name) {
|
|
6710
7307
|
return `.claude/commands/${name}.md`;
|
|
6711
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
|
+
}
|
|
6712
7315
|
var RETIRED_MCP_HEADERS_HELPER_REL = ".halfcycle/mcp-headers.sh";
|
|
6713
7316
|
var RETIRED_WRITE_SET_FILES = [RETIRED_MCP_HEADERS_HELPER_REL];
|
|
6714
7317
|
var RETIRED_DENY_PATTERNS = [
|
|
@@ -6727,6 +7330,9 @@ function closedWriteSet() {
|
|
|
6727
7330
|
const commands = readPluginManifest().commands ?? [];
|
|
6728
7331
|
const files = /* @__PURE__ */ new Set([
|
|
6729
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)),
|
|
6730
7336
|
...Object.keys(OWNED_GENERATED_HEADERS),
|
|
6731
7337
|
VENDORED_BIN_REL,
|
|
6732
7338
|
CREW_ROSTER_REL,
|
|
@@ -6735,9 +7341,12 @@ function closedWriteSet() {
|
|
|
6735
7341
|
BOOTSTRAP_STATE_REL,
|
|
6736
7342
|
CAPTURED_INDEX_REL,
|
|
6737
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),
|
|
6738
7347
|
...RETIRED_WRITE_SET_FILES
|
|
6739
7348
|
]);
|
|
6740
|
-
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]);
|
|
6741
7350
|
const dirs = /* @__PURE__ */ new Set();
|
|
6742
7351
|
for (const path of [...files, ...merged]) {
|
|
6743
7352
|
for (let dir = posix2.dirname(path); dir !== "."; dir = posix2.dirname(dir))
|
|
@@ -6750,6 +7359,7 @@ function closedWriteSet() {
|
|
|
6750
7359
|
dirs,
|
|
6751
7360
|
ownedDirs,
|
|
6752
7361
|
hookCommands: new Set(HALFCYCLE_HOOK_COMMANDS),
|
|
7362
|
+
codexHookCommands: new Set(AGENT_PROFILES.codex.commands),
|
|
6753
7363
|
denyRuleSha256: new Set([...DENY_PATTERNS, ...RETIRED_DENY_PATTERNS].map((rule) => sha256Hex(rule))),
|
|
6754
7364
|
settingsSchema: SETTINGS_SCHEMA,
|
|
6755
7365
|
gitignoreLines: /* @__PURE__ */ new Set([GITIGNORE_HEADER, ...REQUIRED_GITIGNORE_ENTRIES, LEGACY_ENV_LOCAL_REL]),
|
|
@@ -6767,10 +7377,17 @@ function sha256OfWhatThisVersionWrites(commands) {
|
|
|
6767
7377
|
if (existsSync4(src))
|
|
6768
7378
|
out.set(rel, fileSha256(readFileSync6(src)));
|
|
6769
7379
|
};
|
|
6770
|
-
for (const cmd of commands)
|
|
6771
|
-
|
|
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
|
+
}
|
|
6772
7387
|
for (const [rel, content] of hookScriptsThisVersionWrites())
|
|
6773
7388
|
out.set(rel, fileSha256(content));
|
|
7389
|
+
for (const profile of renderCrewProfiles())
|
|
7390
|
+
out.set(profile.rel, fileSha256(Buffer.from(profile.text, "utf-8")));
|
|
6774
7391
|
fromFile(CAPTURED_INDEX_REL, capturedIndexScaffold());
|
|
6775
7392
|
return out;
|
|
6776
7393
|
}
|
|
@@ -6874,6 +7491,8 @@ function findNotShared(targetRepo, manifest, collidedPaths) {
|
|
|
6874
7491
|
const merged = [
|
|
6875
7492
|
SETTINGS_REL,
|
|
6876
7493
|
...manifest.mcp !== void 0 ? [MCP_REGISTRATION_REL] : [],
|
|
7494
|
+
...manifest.codexHooks !== void 0 ? [CODEX_HOOKS_REL2] : [],
|
|
7495
|
+
...manifest.codexConfig !== void 0 ? [CODEX_CONFIG_REL2] : [],
|
|
6877
7496
|
...manifest.gitignore.created || manifest.gitignore.appended.length > 0 ? [GITIGNORE_REL] : [],
|
|
6878
7497
|
...manifest.gitattributes !== void 0 && (manifest.gitattributes.created || manifest.gitattributes.appended.length > 0) ? [GITATTRIBUTES_REL] : []
|
|
6879
7498
|
];
|
|
@@ -6934,6 +7553,55 @@ function readPluginManifest() {
|
|
|
6934
7553
|
const raw = readFileSync6(manifestPath, "utf-8");
|
|
6935
7554
|
return JSON.parse(raw);
|
|
6936
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
|
+
}
|
|
6937
7605
|
function planManifestCommands(manifest, targetRepo, previous, writeSet) {
|
|
6938
7606
|
const recordedFiles = new Map((previous?.files ?? []).filter((file) => writeSet.files.has(file.path)).map((file) => [file.path, file]));
|
|
6939
7607
|
return (manifest.commands ?? []).map((cmd) => {
|
|
@@ -7112,6 +7780,16 @@ function generateSettingsJson() {
|
|
|
7112
7780
|
};
|
|
7113
7781
|
return JSON.stringify(settings, null, 2) + "\n";
|
|
7114
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
|
+
}
|
|
7115
7793
|
var GENERATED_GUARD_RUNNER_HEADER = "# Governance hook wrapper \u2014 generated by";
|
|
7116
7794
|
function generateGuardRunnerWrapper(preFilter = resolvePreFilterShell()) {
|
|
7117
7795
|
return `#!/usr/bin/env bash
|
|
@@ -7164,9 +7842,11 @@ ${preFilter.trimEnd()}
|
|
|
7164
7842
|
# Before a tool runs, on a bash new enough for the cheap pass: read the call
|
|
7165
7843
|
# once; when no risky action can be in it, answer here (the pass writes the
|
|
7166
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.
|
|
7167
7847
|
if [ "$1" = hook ] && [ "$2" = pre-tool-use ] && [ "\${BASH_VERSINFO[0]:-0}" -ge 5 ]; then
|
|
7168
7848
|
hc_input=$(cat 2>/dev/null) || IFS= read -r -d '' hc_input
|
|
7169
|
-
${PRE_FILTER_FUNCTION} && exit 0
|
|
7849
|
+
${PRE_FILTER_FUNCTION} "$@" && exit 0
|
|
7170
7850
|
node -e "${nodeFloorProgram("")}" || {
|
|
7171
7851
|
printf '%s' "$hc_input" | halfcycle_pre_tool_use_fallback
|
|
7172
7852
|
exit 0
|
|
@@ -7406,27 +8086,33 @@ exit 0
|
|
|
7406
8086
|
var GENERATED_USER_PROMPT_SCOPE_HEADER = "# Typed scope approval \u2014 generated by";
|
|
7407
8087
|
function generateUserPromptScopeHook() {
|
|
7408
8088
|
return `#!/usr/bin/env bash
|
|
7409
|
-
${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.
|
|
7410
8090
|
#
|
|
7411
8091
|
# Lets the owner of this project approve a phase's scope, or send it back,
|
|
7412
8092
|
# decide one part of a phase's close review, or turn builds running on their
|
|
7413
8093
|
# own on or off, by typing one line as the whole prompt in their own Claude
|
|
7414
8094
|
# Code session: "approve <phase>", "send back <phase>: <note>", "accept
|
|
7415
8095
|
# <phase>/<part>", "question <phase>/<part>: <words>", "carry <phase>/<part>:
|
|
7416
|
-
# <words>", "drop <phase>/<part>: <words>", "
|
|
8096
|
+
# <words>", "drop <phase>/<part>: <words>", "reject <phase>/<task>: <words>",
|
|
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.
|
|
7417
8099
|
#
|
|
7418
8100
|
# AN ORDINARY PROMPT STARTS NOTHING. This script reads the hook input once and
|
|
7419
8101
|
# looks only at the start of the prompt's value. Unless the prompt begins, after
|
|
7420
8102
|
# any whitespace (typed, or escaped in the input), with "approve ", "send back ",
|
|
7421
|
-
# "accept ", "question ", "carry ", "drop " or "autonomy ", in any case,
|
|
7422
|
-
#
|
|
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
|
|
7423
8106
|
# escaped, so no other field (the folder the session is in, for one) can read as
|
|
7424
8107
|
# the start of the prompt.
|
|
7425
8108
|
#
|
|
7426
8109
|
# Otherwise it finds the project root from its own location, never from the
|
|
7427
8110
|
# folder the session has moved to, and hands the same input to the guard
|
|
7428
8111
|
# wrapper beside it. That decides whether the line counts and prints at most
|
|
7429
|
-
# 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.
|
|
7430
8116
|
#
|
|
7431
8117
|
# FAILS OPEN up to that hand-over: any error before it exits 0 with no output.
|
|
7432
8118
|
# After it, the wrapper's own exit stands.
|
|
@@ -7442,7 +8128,7 @@ IFS= read -r -d '' HC_INPUT || true
|
|
|
7442
8128
|
# \\u escape, or as the UTF-8 bytes of a Unicode space. Compared byte for byte.
|
|
7443
8129
|
HC_LEAD='[[:space:]]|\\\\[nrtf]|\\\\u(000[9a-d]|0020|00a0|1680|200[0-9a]|202[89f]|205f|3000|feff)'
|
|
7444
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'
|
|
7445
|
-
if ! printf '%s' "$HC_INPUT" | LC_ALL=C grep -Eiq "\\"prompt\\"[[:space:]]*:[[:space:]]*\\"($HC_LEAD)*(approve|send back|accept|question|carry|drop|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
|
|
7446
8132
|
exit 0
|
|
7447
8133
|
fi
|
|
7448
8134
|
|
|
@@ -7452,16 +8138,17 @@ export CLAUDE_PROJECT_DIR="$HC_PROJECT_ROOT"
|
|
|
7452
8138
|
|
|
7453
8139
|
trap - ERR
|
|
7454
8140
|
set +e
|
|
7455
|
-
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" "$@"
|
|
7456
8142
|
`;
|
|
7457
8143
|
}
|
|
7458
8144
|
var GENERATED_USER_PROMPT_FINDINGS_HEADER = "# Findings hand-over \u2014 generated by";
|
|
7459
8145
|
function generateUserPromptFindingsHook() {
|
|
7460
8146
|
return `#!/usr/bin/env bash
|
|
7461
|
-
${GENERATED_USER_PROMPT_FINDINGS_HEADER} the Halfcycle installer.
|
|
8147
|
+
${GENERATED_USER_PROMPT_FINDINGS_HEADER} the Halfcycle installer, now also for Codex.
|
|
7462
8148
|
#
|
|
7463
8149
|
# At the person's next message, before the agent starts, hands it the details of
|
|
7464
|
-
# 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.
|
|
7465
8152
|
#
|
|
7466
8153
|
# AN ORDINARY PROMPT STARTS NOTHING. With no findings file for this session, this
|
|
7467
8154
|
# script reads its session id with a bash regex (no node) and exits 0 with no
|
|
@@ -7498,7 +8185,7 @@ export CLAUDE_PROJECT_DIR="$HC_PROJECT_ROOT"
|
|
|
7498
8185
|
|
|
7499
8186
|
trap - ERR
|
|
7500
8187
|
set +e
|
|
7501
|
-
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" "$@"
|
|
7502
8189
|
`;
|
|
7503
8190
|
}
|
|
7504
8191
|
var CI_STANZA_KNOWN_HEADER_HASHES = [
|
|
@@ -7577,6 +8264,160 @@ var MCP_HEADERS_HELPER_COMMAND = `node -e "${nodeFloorProgram([
|
|
|
7577
8264
|
`console.error('halfcycle: no ${VENDORED_BIN_REL} at or above '+process.cwd()+'; run npx halfcycle in the project folder');`,
|
|
7578
8265
|
`process.exit(1)`
|
|
7579
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
|
+
}
|
|
7580
8421
|
var MCP_HEADERS_KNOWN_HEADER_HASHES = [
|
|
7581
8422
|
"37a733ea588b3361b2bbfd86c44e0c95da30d7af6acffd1243ece83c811f0af2",
|
|
7582
8423
|
"6732ec6d2d2bf7cb33017749a3d021b59af653dcd07c3811693b5a242d44b1a1"
|
|
@@ -7991,6 +8832,21 @@ function planCommandStubs(targetRepo, previous, writeSet) {
|
|
|
7991
8832
|
const manifest = readPluginManifest();
|
|
7992
8833
|
return { version: manifest.version, commands: planManifestCommands(manifest, targetRepo, previous, writeSet) };
|
|
7993
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
|
+
}
|
|
7994
8850
|
function planVendoredBinary(targetRepo) {
|
|
7995
8851
|
return decideOwned(join6(targetRepo, ".halfcycle", "bin", "bin.bundle.mjs"), targetRepo, readFileSync6(resolveVendoredBinary(), "utf-8"));
|
|
7996
8852
|
}
|
|
@@ -8038,9 +8894,156 @@ function putPlannedMcp(targetRepo, planned) {
|
|
|
8038
8894
|
return;
|
|
8039
8895
|
writeAllowlisted(join6(targetRepo, MCP_REGISTRATION_REL), targetRepo, planned.text, []);
|
|
8040
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
|
+
}
|
|
8041
9032
|
function planCrewRoster(targetRepo) {
|
|
8042
9033
|
return decideOwned(join6(targetRepo, CREW_ROSTER_REL), targetRepo, renderCrewRoster());
|
|
8043
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
|
+
}
|
|
8044
9047
|
function planInstallRecord(targetRepo, text) {
|
|
8045
9048
|
return decideOwned(join6(targetRepo, INSTALL_MANIFEST_REL), targetRepo, text);
|
|
8046
9049
|
}
|
|
@@ -8064,6 +9067,12 @@ function accountInstall(steps, actual) {
|
|
|
8064
9067
|
report.editedCollidedPaths.push(command.rel);
|
|
8065
9068
|
noteFile(draft, command.rel, command.outcome, actual.bytesAfter);
|
|
8066
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
|
+
}
|
|
8067
9076
|
noteFile(draft, VENDORED_BIN_REL, recordOwnedGenerated(report, steps.vendored), actual.bytesAfter);
|
|
8068
9077
|
const settings = steps.settings;
|
|
8069
9078
|
recordSettingsMerge(draft, settings.existing, settings.generated, settings.preexisted, settings.merged);
|
|
@@ -8121,6 +9130,8 @@ function accountInstall(steps, actual) {
|
|
|
8121
9130
|
if (mcp.existing !== null)
|
|
8122
9131
|
recordMcpAdoptionOnly(draft, mcp.existing);
|
|
8123
9132
|
}
|
|
9133
|
+
recordCodexHooksStep(report, draft, steps.codexHooks);
|
|
9134
|
+
recordCodexConfigStep(report, draft, steps.codexConfig);
|
|
8124
9135
|
recordGitignoreReconcile(draft, steps.gitignore.before, actual.gitignoreAfter, writeSet);
|
|
8125
9136
|
if (actual.gitignore === "failed") {
|
|
8126
9137
|
report.skippedPaths.push(".gitignore (write failed \u2014 see credential refusal)");
|
|
@@ -8163,6 +9174,12 @@ function accountInstall(steps, actual) {
|
|
|
8163
9174
|
report.writtenPaths.push(BUNDLE_PIN_REL);
|
|
8164
9175
|
noteFile(draft, BUNDLE_PIN_REL, "written", actual.bytesAfter);
|
|
8165
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
|
+
}
|
|
8166
9183
|
if (actual.stateExistsAfter)
|
|
8167
9184
|
recordPerMachineFile(draft, BOOTSTRAP_STATE_REL);
|
|
8168
9185
|
for (const dir of writeSet.dirs) {
|
|
@@ -8173,11 +9190,79 @@ function accountInstall(steps, actual) {
|
|
|
8173
9190
|
assertManifestInWriteSet(installManifest, writeSet);
|
|
8174
9191
|
return { report, installManifest, manifestText: serializeManifest(installManifest) };
|
|
8175
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
|
+
}
|
|
8176
9253
|
function recordInstallRecord(report, planned) {
|
|
8177
9254
|
recordOwnedGenerated(report, planned);
|
|
8178
9255
|
}
|
|
8179
9256
|
var SELF_BUILD_GITIGNORE_REL = ".halfcycle/.gitignore";
|
|
8180
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
|
+
];
|
|
8181
9266
|
function mcpHasHalfcycleEntry(text) {
|
|
8182
9267
|
const parsed = JSON.parse(text);
|
|
8183
9268
|
if (parsed === null || typeof parsed !== "object")
|
|
@@ -8185,6 +9270,14 @@ function mcpHasHalfcycleEntry(text) {
|
|
|
8185
9270
|
const servers = parsed.mcpServers;
|
|
8186
9271
|
return servers !== null && typeof servers === "object" && MCP_SERVER_KEY in servers;
|
|
8187
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
|
+
}
|
|
8188
9281
|
function installSelfBuild(options) {
|
|
8189
9282
|
const { targetRepo, engagementId, credential, freshlyMinted, home } = options;
|
|
8190
9283
|
assertEngagementId(engagementId);
|
|
@@ -8219,6 +9312,7 @@ function installSelfBuild(options) {
|
|
|
8219
9312
|
if (!existsSync4(at(CREW_ROSTER_REL))) {
|
|
8220
9313
|
writeCrewRoster(targetRepo, report);
|
|
8221
9314
|
}
|
|
9315
|
+
writeMissingSelfBuildCrewProfiles(targetRepo, report);
|
|
8222
9316
|
if (!existsSync4(at(BUNDLE_PIN_REL))) {
|
|
8223
9317
|
writeBundlePin(targetRepo, planBundlePin(targetRepo, version, engagementId, "self-build", void 0, credential.methodVersion), report.writtenPaths);
|
|
8224
9318
|
}
|
|
@@ -9778,7 +10872,8 @@ function renderPreview(plan, missingGitForWindows, claudeMdWords) {
|
|
|
9778
10872
|
|
|
9779
10873
|
// dist/doctor.js
|
|
9780
10874
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
9781
|
-
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";
|
|
9782
10877
|
import { join as join10 } from "node:path";
|
|
9783
10878
|
var GUARD_RUNNER_REL2 = ".claude/hooks/guard-runner.sh";
|
|
9784
10879
|
var CONNECTION_VERB = "check-connection";
|
|
@@ -9802,8 +10897,10 @@ function runDoctor(root, options) {
|
|
|
9802
10897
|
const lines = [setUp.line, connection.signedIn, connection.connected, checksRunning, version];
|
|
9803
10898
|
const failing = lines.filter((line) => !line.ok && !line.cascading).length;
|
|
9804
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)];
|
|
9805
10902
|
return {
|
|
9806
|
-
text: [...
|
|
10903
|
+
text: [...printed.map((line) => line.text), closing].join("\n"),
|
|
9807
10904
|
exitCode: failing === 0 ? 0 : 1
|
|
9808
10905
|
};
|
|
9809
10906
|
}
|
|
@@ -9993,6 +11090,88 @@ function versionNoLine(installed, current) {
|
|
|
9993
11090
|
}
|
|
9994
11091
|
return `Up to date: no. This folder has Halfcycle ${installed}; the newest is ${current}. To update, run ${HALFCYCLE_COMMAND} here.`;
|
|
9995
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
|
+
}
|
|
9996
11175
|
function notChecked(check, earlier) {
|
|
9997
11176
|
return { ok: false, cascading: true, text: `${check}: not checked, because ${earlier} is not working yet.` };
|
|
9998
11177
|
}
|
|
@@ -10167,19 +11346,18 @@ async function probeMcpOrigin(mcpUrl, token) {
|
|
|
10167
11346
|
|
|
10168
11347
|
// dist/claude-version-probe.js
|
|
10169
11348
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
10170
|
-
import { existsSync as existsSync8, realpathSync as
|
|
11349
|
+
import { existsSync as existsSync8, realpathSync as realpathSync4 } from "node:fs";
|
|
10171
11350
|
import { delimiter as delimiter2, join as join12 } from "node:path";
|
|
10172
|
-
function
|
|
11351
|
+
function execVersionCommand(command, timeoutMs, deps) {
|
|
10173
11352
|
const platform6 = deps.platform ?? process.platform;
|
|
10174
11353
|
const exec = deps.exec ?? execFileSync3;
|
|
10175
|
-
let raw;
|
|
10176
11354
|
try {
|
|
10177
|
-
|
|
11355
|
+
return platform6 === "win32" ? exec("cmd.exe", ["/d", "/s", "/c", `"${command} --version"`], {
|
|
10178
11356
|
encoding: "utf-8",
|
|
10179
11357
|
timeout: timeoutMs,
|
|
10180
11358
|
stdio: ["ignore", "pipe", "ignore"],
|
|
10181
11359
|
windowsVerbatimArguments: true
|
|
10182
|
-
}) : exec(
|
|
11360
|
+
}) : exec(command, ["--version"], {
|
|
10183
11361
|
encoding: "utf-8",
|
|
10184
11362
|
timeout: timeoutMs,
|
|
10185
11363
|
stdio: ["ignore", "pipe", "ignore"]
|
|
@@ -10187,7 +11365,14 @@ function probeClaudeCodeVersion(timeoutMs, deps = {}) {
|
|
|
10187
11365
|
} catch {
|
|
10188
11366
|
return void 0;
|
|
10189
11367
|
}
|
|
10190
|
-
|
|
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];
|
|
10191
11376
|
}
|
|
10192
11377
|
var CLAUDE_CODE_INSTALL_PAGE = "https://claude.com/claude-code";
|
|
10193
11378
|
var UPGRADE_COMMANDS = {
|
|
@@ -10208,7 +11393,7 @@ function probeClaudeCodeInstallMethod(deps = {}) {
|
|
|
10208
11393
|
const exists = deps.exists ?? existsSync8;
|
|
10209
11394
|
const realpath = deps.realpath ?? ((file) => {
|
|
10210
11395
|
try {
|
|
10211
|
-
return
|
|
11396
|
+
return realpathSync4(file);
|
|
10212
11397
|
} catch {
|
|
10213
11398
|
return file;
|
|
10214
11399
|
}
|
|
@@ -10227,6 +11412,30 @@ function probeClaudeCodeInstallMethod(deps = {}) {
|
|
|
10227
11412
|
return void 0;
|
|
10228
11413
|
}
|
|
10229
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
|
+
|
|
10230
11439
|
// dist/build-record/write.js
|
|
10231
11440
|
import { existsSync as existsSync9, mkdirSync as mkdirSync7, rmSync as rmSync4 } from "node:fs";
|
|
10232
11441
|
import { basename as basename2, join as join13, resolve as resolve3, relative as relative2, isAbsolute as isAbsolute3 } from "node:path";
|
|
@@ -10969,7 +12178,7 @@ import { existsSync as existsSync11, readFileSync as readFileSync16 } from "node
|
|
|
10969
12178
|
import { join as join19 } from "node:path";
|
|
10970
12179
|
|
|
10971
12180
|
// dist/draw/write.js
|
|
10972
|
-
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";
|
|
10973
12182
|
import { dirname as dirname6, isAbsolute as isAbsolute4, join as join14, relative as relative3, resolve as resolve4, sep } from "node:path";
|
|
10974
12183
|
var DOCS_DIR = "docs";
|
|
10975
12184
|
var DIAGRAMS_DIR = "diagrams";
|
|
@@ -11057,7 +12266,7 @@ function isInside(outer, inner) {
|
|
|
11057
12266
|
function checkDiagramPlace(paths) {
|
|
11058
12267
|
let root;
|
|
11059
12268
|
try {
|
|
11060
|
-
root =
|
|
12269
|
+
root = realpathSync5(paths.repoRoot);
|
|
11061
12270
|
} catch {
|
|
11062
12271
|
return { ok: false, why: `the folder ${paths.repoRoot} could not be opened. Check the path, then run this again.` };
|
|
11063
12272
|
}
|
|
@@ -11070,7 +12279,7 @@ function checkDiagramPlace(paths) {
|
|
|
11070
12279
|
}
|
|
11071
12280
|
let real;
|
|
11072
12281
|
try {
|
|
11073
|
-
real =
|
|
12282
|
+
real = realpathSync5(ancestor);
|
|
11074
12283
|
} catch {
|
|
11075
12284
|
return {
|
|
11076
12285
|
ok: false,
|
|
@@ -12927,23 +14136,36 @@ function typedLine(act, reference, part) {
|
|
|
12927
14136
|
return `drop ${reference}/${part}: <why>`;
|
|
12928
14137
|
}
|
|
12929
14138
|
}
|
|
12930
|
-
function
|
|
14139
|
+
function rv7aLine(reference) {
|
|
14140
|
+
return `The line accept ${reference} also accepts every check nobody ran that is listed below. Reject a task's checks first if you do not accept them.`;
|
|
14141
|
+
}
|
|
14142
|
+
function tasksWithWaitingClaims(claims) {
|
|
14143
|
+
return [...new Set(claims.filter((c) => c.answer === null).map((c) => c.task))];
|
|
14144
|
+
}
|
|
14145
|
+
function rv7Lines(view, claims) {
|
|
12931
14146
|
const reference = typedPhaseReference(view.phase, view.number);
|
|
12932
|
-
|
|
12933
|
-
return [`This phase has no parts to review. To close it, type: accept ${reference}`];
|
|
14147
|
+
const claimTasks = tasksWithWaitingClaims(claims);
|
|
12934
14148
|
const decidable = view.parts.filter((p) => p.decidable);
|
|
12935
|
-
if (
|
|
12936
|
-
|
|
14149
|
+
if (claimTasks.length === 0) {
|
|
14150
|
+
if (view.parts.length === 0)
|
|
14151
|
+
return [`This phase has no parts to review. To close it, type: accept ${reference}`];
|
|
14152
|
+
if (decidable.length === 0)
|
|
14153
|
+
return ["No part waits for your decision now."];
|
|
14154
|
+
}
|
|
12937
14155
|
const lines = ["To decide, type one of these lines in your own Claude Code session:"];
|
|
12938
14156
|
for (const part of decidable) {
|
|
12939
14157
|
for (const act of reviewActsFor(part.built))
|
|
12940
14158
|
lines.push(` ${typedLine(act, reference, part.part)}`);
|
|
12941
14159
|
}
|
|
12942
|
-
|
|
14160
|
+
for (const task of claimTasks)
|
|
14161
|
+
lines.push(` reject ${reference}/${task}: <why>`);
|
|
14162
|
+
if (decidable.some((p) => p.built) || claimTasks.length > 0)
|
|
12943
14163
|
lines.push(` accept ${reference}`);
|
|
14164
|
+
if (claimTasks.length > 0)
|
|
14165
|
+
lines.push(rv7aLine(reference));
|
|
12944
14166
|
return lines;
|
|
12945
14167
|
}
|
|
12946
|
-
function reviewListingLines(view) {
|
|
14168
|
+
function reviewListingLines(view, claims = []) {
|
|
12947
14169
|
const lines = [rv1Line(view)];
|
|
12948
14170
|
if (!view.counted)
|
|
12949
14171
|
lines.push(RV1A_LINE);
|
|
@@ -12951,7 +14173,7 @@ function reviewListingLines(view) {
|
|
|
12951
14173
|
lines.push(...observedPhaseLines(view.observed));
|
|
12952
14174
|
for (const part of view.parts)
|
|
12953
14175
|
lines.push(...partLines(part, view));
|
|
12954
|
-
lines.push(...rv7Lines(view));
|
|
14176
|
+
lines.push(...rv7Lines(view, claims));
|
|
12955
14177
|
return lines;
|
|
12956
14178
|
}
|
|
12957
14179
|
var CALLS_HEADING2 = "Calls made without asking you";
|
|
@@ -12976,21 +14198,17 @@ var CHECKS_SUBHEADING = "An agent says these pass and Halfcycle never ran them.
|
|
|
12976
14198
|
function uncheckedClaimLine(test) {
|
|
12977
14199
|
return `Unchecked: ${test.claimedBy} says "${test.label}" passes. Halfcycle never ran it.`;
|
|
12978
14200
|
}
|
|
12979
|
-
|
|
12980
|
-
|
|
12981
|
-
const only = "Accept or reject each one in the System view; this cannot be done by typing in chat";
|
|
12982
|
-
return address === void 0 ? `${only}.` : `${only}: ${address}`;
|
|
12983
|
-
}
|
|
12984
|
-
function checksSectionLines(work, engagementId) {
|
|
14201
|
+
var ANSWER_CLAIMS_BY_TYPING_LINE = "To answer them, type the reject or accept line listed above in your own Claude Code session.";
|
|
14202
|
+
function checksSectionLines(work) {
|
|
12985
14203
|
const unchecked = work.tasks.flatMap((task) => task.tests.filter((t) => t.state === "unchecked"));
|
|
12986
14204
|
if (unchecked.length === 0)
|
|
12987
14205
|
return [];
|
|
12988
|
-
return [CHECKS_HEADING, CHECKS_SUBHEADING, ...unchecked.map(uncheckedClaimLine),
|
|
14206
|
+
return [CHECKS_HEADING, CHECKS_SUBHEADING, ...unchecked.map(uncheckedClaimLine), ANSWER_CLAIMS_BY_TYPING_LINE];
|
|
12989
14207
|
}
|
|
12990
|
-
function workSectionLines(work,
|
|
14208
|
+
function workSectionLines(work, _engagementId) {
|
|
12991
14209
|
if (work === null)
|
|
12992
14210
|
return [];
|
|
12993
|
-
return [...callsSectionLines(work.calls), ...checksSectionLines(work
|
|
14211
|
+
return [...callsSectionLines(work.calls), ...checksSectionLines(work)];
|
|
12994
14212
|
}
|
|
12995
14213
|
function rv8Line(view) {
|
|
12996
14214
|
if (view.state === "closed") {
|
|
@@ -13132,7 +14350,8 @@ async function runReviewShow(repoRoot, phase, deps) {
|
|
|
13132
14350
|
if (view.state !== "open")
|
|
13133
14351
|
return refused(rv8Line(view));
|
|
13134
14352
|
const work = await deps.readWork(credential, phase);
|
|
13135
|
-
const
|
|
14353
|
+
const phaseWork = work.kind === "work" ? work.work : null;
|
|
14354
|
+
const stdout = `${[...reviewListingLines(view, phaseWork?.uncheckedClaims ?? []), ...workSectionLines(phaseWork, credential.engagementId)].join("\n")}
|
|
13136
14355
|
`;
|
|
13137
14356
|
const written = deps.recordShown(credential.engagementId, shownReviewEntry(view, deps.now().toISOString()));
|
|
13138
14357
|
if (!written.ok)
|
|
@@ -14234,6 +15453,35 @@ function readJsonFile(repoRoot, rel, parse) {
|
|
|
14234
15453
|
}
|
|
14235
15454
|
return { kind: "ok", value: parsed.data };
|
|
14236
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
|
+
}
|
|
14237
15485
|
function runCheckTasks(repoRoot, phase, withTaskList) {
|
|
14238
15486
|
const p = "halfcycle check-tasks:";
|
|
14239
15487
|
const planRead = readJsonFile(repoRoot, TASK_PLAN_FILE, (raw) => taskPlanSchema.safeParse(raw));
|
|
@@ -14259,7 +15507,15 @@ function runCheckTasks(repoRoot, phase, withTaskList) {
|
|
|
14259
15507
|
}
|
|
14260
15508
|
taskList = { file: listRead.kind === "ok" ? listRead.value : null };
|
|
14261
15509
|
}
|
|
14262
|
-
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
|
+
});
|
|
14263
15519
|
const stdout = result.lines.map((line) => `${line.text}
|
|
14264
15520
|
`).join("");
|
|
14265
15521
|
return { stdout, stderr: "", exitCode: result.refused ? 1 : 0 };
|
|
@@ -14318,7 +15574,7 @@ function runProveChild(binary, credential, repoRoot, words, deps = {}) {
|
|
|
14318
15574
|
|
|
14319
15575
|
// dist/uninstall.js
|
|
14320
15576
|
import { lstatSync as lstatSync3, readFileSync as readFileSync19, readdirSync, rmSync as rmSync6, rmdirSync, writeFileSync as writeFileSync8 } from "node:fs";
|
|
14321
|
-
import { homedir as
|
|
15577
|
+
import { homedir as homedir5 } from "node:os";
|
|
14322
15578
|
import { dirname as dirname7, join as join22, relative as relative6, sep as sep2 } from "node:path";
|
|
14323
15579
|
var REMOVE_CREDENTIAL_FLAG = "--remove-credential";
|
|
14324
15580
|
function parseUninstallArgs(args2) {
|
|
@@ -14342,19 +15598,6 @@ function lexists(path) {
|
|
|
14342
15598
|
return false;
|
|
14343
15599
|
}
|
|
14344
15600
|
}
|
|
14345
|
-
function symlinkOnPath(root, rel) {
|
|
14346
|
-
let current = root;
|
|
14347
|
-
for (const segment of rel.split("/")) {
|
|
14348
|
-
current = join22(current, segment);
|
|
14349
|
-
try {
|
|
14350
|
-
if (lstatSync3(current).isSymbolicLink())
|
|
14351
|
-
return true;
|
|
14352
|
-
} catch {
|
|
14353
|
-
return false;
|
|
14354
|
-
}
|
|
14355
|
-
}
|
|
14356
|
-
return false;
|
|
14357
|
-
}
|
|
14358
15601
|
function isPlainObject3(value) {
|
|
14359
15602
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
14360
15603
|
}
|
|
@@ -14432,7 +15675,7 @@ function validate(options, rules) {
|
|
|
14432
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.`);
|
|
14433
15676
|
}
|
|
14434
15677
|
if (engagementId !== void 0) {
|
|
14435
|
-
const expectedParent = join22(home ??
|
|
15678
|
+
const expectedParent = join22(home ?? homedir5(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME);
|
|
14436
15679
|
if (dirname7(engagementStateDir(engagementId, home)) !== expectedParent) {
|
|
14437
15680
|
throw new Refusal(`the project id in ${BUNDLE_PIN_REL} does not name a folder under ~/.halfcycle/engagements. Nothing was removed.`);
|
|
14438
15681
|
}
|
|
@@ -14443,14 +15686,16 @@ function validate(options, rules) {
|
|
|
14443
15686
|
engagementId,
|
|
14444
15687
|
idValid,
|
|
14445
15688
|
settings: readMergedJson(targetRepo, rules.settings),
|
|
14446
|
-
mcp: readMergedJson(targetRepo, rules.mcp)
|
|
15689
|
+
mcp: readMergedJson(targetRepo, rules.mcp),
|
|
15690
|
+
codexHooks: readMergedJson(targetRepo, rules.codexHooks)
|
|
14447
15691
|
};
|
|
14448
15692
|
}
|
|
14449
15693
|
function mergedFileRules(writeSet) {
|
|
14450
15694
|
const spelledIn = (spellings) => (text) => spellings.some((spelling) => text.includes(spelling));
|
|
14451
15695
|
return {
|
|
14452
15696
|
settings: { rel: SETTINGS_REL, holdsOurs: spelledIn([...writeSet.hookCommands].map((c) => JSON.stringify(c))) },
|
|
14453
|
-
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))) }
|
|
14454
15699
|
};
|
|
14455
15700
|
}
|
|
14456
15701
|
function writeBack(targetRepo, rel, current, next, lists, name) {
|
|
@@ -14539,48 +15784,12 @@ function ourMcpServers(value) {
|
|
|
14539
15784
|
return isPlainObject3(servers) && MCP_SERVER_KEY in servers ? servers : null;
|
|
14540
15785
|
}
|
|
14541
15786
|
function unmergeSettings(value, rec, writeSet) {
|
|
14542
|
-
let changed = false;
|
|
14543
15787
|
const refused3 = [];
|
|
14544
|
-
const commands = new Set(writeSet.hookCommands);
|
|
14545
15788
|
const denyHashes = rec.denyAddedSha256.filter((h) => writeSet.denyRuleSha256.has(h));
|
|
14546
15789
|
if (denyHashes.length < rec.denyAddedSha256.length) {
|
|
14547
15790
|
refused3.push(`${SETTINGS_REL}: a deny rule the install record names but Halfcycle never adds (it was kept)`);
|
|
14548
15791
|
}
|
|
14549
|
-
|
|
14550
|
-
if (isPlainObject3(hooks)) {
|
|
14551
|
-
const ours = new Set(ourHookEntries(value, commands).map(({ entry }) => entry));
|
|
14552
|
-
let emptiedAnEvent = false;
|
|
14553
|
-
for (const event of Object.keys(hooks)) {
|
|
14554
|
-
const entries = hooks[event];
|
|
14555
|
-
if (!Array.isArray(entries))
|
|
14556
|
-
continue;
|
|
14557
|
-
let touched = false;
|
|
14558
|
-
const next = [];
|
|
14559
|
-
for (const entry of entries) {
|
|
14560
|
-
if (!ours.has(entry)) {
|
|
14561
|
-
next.push(entry);
|
|
14562
|
-
continue;
|
|
14563
|
-
}
|
|
14564
|
-
touched = true;
|
|
14565
|
-
const remaining = withoutHalfcycleCommands(entry, commands);
|
|
14566
|
-
if (remaining !== null)
|
|
14567
|
-
next.push(remaining);
|
|
14568
|
-
}
|
|
14569
|
-
if (touched)
|
|
14570
|
-
changed = true;
|
|
14571
|
-
if (next.length === 0 && (rec.eventsCreated.includes(event) || rec.adopted && touched)) {
|
|
14572
|
-
delete hooks[event];
|
|
14573
|
-
changed = true;
|
|
14574
|
-
emptiedAnEvent = true;
|
|
14575
|
-
} else if (touched) {
|
|
14576
|
-
hooks[event] = next;
|
|
14577
|
-
}
|
|
14578
|
-
}
|
|
14579
|
-
if (Object.keys(hooks).length === 0 && (rec.hooksCreated || rec.adopted && emptiedAnEvent)) {
|
|
14580
|
-
delete value["hooks"];
|
|
14581
|
-
changed = true;
|
|
14582
|
-
}
|
|
14583
|
-
}
|
|
15792
|
+
let changed = unmergeHooks(value, rec, new Set(writeSet.hookCommands));
|
|
14584
15793
|
const permissions = value["permissions"];
|
|
14585
15794
|
if (isPlainObject3(permissions) && Array.isArray(permissions["deny"])) {
|
|
14586
15795
|
const deny = permissions["deny"];
|
|
@@ -14616,6 +15825,52 @@ function unmergeSettings(value, rec, writeSet) {
|
|
|
14616
15825
|
}
|
|
14617
15826
|
return { changed, refused: refused3 };
|
|
14618
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
|
+
};
|
|
14619
15874
|
function unmergeMcp(value, rec) {
|
|
14620
15875
|
const servers = value["mcpServers"];
|
|
14621
15876
|
if (!isPlainObject3(servers))
|
|
@@ -14723,7 +15978,8 @@ function coveredPathExists(targetRepo, line) {
|
|
|
14723
15978
|
function selfBuildMergedFileRules() {
|
|
14724
15979
|
return {
|
|
14725
15980
|
settings: { rel: SETTINGS_REL, holdsOurs: () => false },
|
|
14726
|
-
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 }
|
|
14727
15983
|
};
|
|
14728
15984
|
}
|
|
14729
15985
|
function removeSelfBuildOwnedFile(targetRepo, rel, lists, name) {
|
|
@@ -14777,6 +16033,33 @@ function removeSelfBuildCrewRoster(targetRepo, lists, name) {
|
|
|
14777
16033
|
name(lists.failed, rel, rel);
|
|
14778
16034
|
}
|
|
14779
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
|
+
}
|
|
14780
16063
|
function removeSelfBuildEmptyDir(targetRepo, dir, lists, name) {
|
|
14781
16064
|
if (symlinkOnPath(targetRepo, dir))
|
|
14782
16065
|
return;
|
|
@@ -14826,8 +16109,13 @@ function uninstallSelfBuild(options) {
|
|
|
14826
16109
|
removeSelfBuildOwnedFile(targetRepo, rel, result, name);
|
|
14827
16110
|
}
|
|
14828
16111
|
removeSelfBuildCrewRoster(targetRepo, result, name);
|
|
16112
|
+
for (const { rel, text } of renderCrewProfiles()) {
|
|
16113
|
+
removeSelfBuildCrewProfile(targetRepo, rel, text, result, name);
|
|
16114
|
+
}
|
|
14829
16115
|
removeSelfBuildEmptyDir(targetRepo, ".halfcycle/bin", result, name);
|
|
14830
16116
|
removeSelfBuildEmptyDir(targetRepo, ".halfcycle", result, name);
|
|
16117
|
+
removeSelfBuildEmptyDir(targetRepo, ".codex/agents", result, name);
|
|
16118
|
+
removeSelfBuildEmptyDir(targetRepo, ".codex", result, name);
|
|
14831
16119
|
const foundNothing = result.removed.length + result.restored.length + result.kept.length + result.failed.length === 0;
|
|
14832
16120
|
if (!pinExisted && foundNothing) {
|
|
14833
16121
|
return { outcome: "not-installed", exitCode: 0, ...result, credential: nothingToSay };
|
|
@@ -14863,6 +16151,7 @@ function uninstall(options) {
|
|
|
14863
16151
|
const { targetRepo } = options;
|
|
14864
16152
|
const manifest = checked.manifest ?? noRecord(targetRepo, writeSet);
|
|
14865
16153
|
const deleteWhenEmpty = (created) => created || checked.manifest === null;
|
|
16154
|
+
const deleteCodexWhenEmpty = (record2) => record2 === void 0 || deleteWhenEmpty(record2.created);
|
|
14866
16155
|
const named = /* @__PURE__ */ new Set();
|
|
14867
16156
|
const name = (list2, text, path) => {
|
|
14868
16157
|
list2.push(text);
|
|
@@ -14881,6 +16170,13 @@ function uninstall(options) {
|
|
|
14881
16170
|
changed: unmergeMcp(value, mcpRecord).changed,
|
|
14882
16171
|
adopted: false
|
|
14883
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);
|
|
14884
16180
|
const deferred = /* @__PURE__ */ new Set([BUNDLE_PIN_REL, INSTALL_MANIFEST_REL]);
|
|
14885
16181
|
const recorded = new Map(manifest.files.map((entry) => [entry.path, entry]));
|
|
14886
16182
|
for (const path of [.../* @__PURE__ */ new Set([...recorded.keys(), ...writeSet.files])].sort()) {
|
|
@@ -14964,6 +16260,44 @@ function applyMergedJson(targetRepo, rule, file, deleteWhenEmpty, lists, name, e
|
|
|
14964
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);
|
|
14965
16261
|
}
|
|
14966
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
|
+
}
|
|
14967
16301
|
function removeInstalledFile(targetRepo, path, entry, writeSet, lists, name) {
|
|
14968
16302
|
if (!writeSet.files.has(path)) {
|
|
14969
16303
|
name(lists.leftAlone, `${path} (Halfcycle never writes this path, so it was not removed)`, path);
|
|
@@ -15095,7 +16429,7 @@ function filesUnder(root, rel) {
|
|
|
15095
16429
|
return out;
|
|
15096
16430
|
}
|
|
15097
16431
|
function shownPath(path, home) {
|
|
15098
|
-
const base = home ??
|
|
16432
|
+
const base = home ?? homedir5();
|
|
15099
16433
|
const rel = relative6(base, path);
|
|
15100
16434
|
const under = rel !== "" && !rel.startsWith("..") && !rel.startsWith(sep2);
|
|
15101
16435
|
return `${under ? `~/${rel.split(sep2).join("/")}` : path}/`;
|
|
@@ -15177,9 +16511,13 @@ function bundleVersion() {
|
|
|
15177
16511
|
function claudeCodeVersion(deps = {}) {
|
|
15178
16512
|
return probeClaudeCodeVersion(3e3, deps);
|
|
15179
16513
|
}
|
|
16514
|
+
function codexVersion(deps = {}) {
|
|
16515
|
+
return probeCodexVersion(3e3, deps);
|
|
16516
|
+
}
|
|
15180
16517
|
var REAL_PROBES = {
|
|
15181
16518
|
bundleVersion,
|
|
15182
16519
|
claudeCodeVersion,
|
|
16520
|
+
codexVersion,
|
|
15183
16521
|
projectName: (dir) => {
|
|
15184
16522
|
try {
|
|
15185
16523
|
return deriveProjectName(dir);
|
|
@@ -15227,6 +16565,9 @@ function openingBannerContent(projectDir, env = process.env, probes = REAL_PROBE
|
|
|
15227
16565
|
const facts = [
|
|
15228
16566
|
fact("Version", version, version),
|
|
15229
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()),
|
|
15230
16571
|
fact("Project", project, name),
|
|
15231
16572
|
fact("Status", status, installed === void 0 ? "new" : "installed"),
|
|
15232
16573
|
// `Website`, NOT `Plane` — see this function's docblock. (No issue number here:
|
|
@@ -15399,8 +16740,8 @@ async function runPace(repoRoot, start, deps) {
|
|
|
15399
16740
|
|
|
15400
16741
|
// dist/restore.js
|
|
15401
16742
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
15402
|
-
import { closeSync as closeSync3, fchmodSync, lstatSync as lstatSync4, mkdirSync as mkdirSync11, openSync as openSync3, readdirSync as readdirSync2, readFileSync as readFileSync21, readlinkSync, realpathSync as
|
|
15403
|
-
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";
|
|
15404
16745
|
import { join as join24, resolve as resolve7 } from "node:path";
|
|
15405
16746
|
var RESTORE_USAGE = `${HALFCYCLE_COMMAND} restore [<id>]`;
|
|
15406
16747
|
function parseRestoreArgs(rest) {
|
|
@@ -15423,7 +16764,7 @@ function runRestore(cwd, id, options = {}) {
|
|
|
15423
16764
|
if (engagementId === null) {
|
|
15424
16765
|
return refused2(`Halfcycle is not set up in this project, so it has saved nothing here. To set it up, run: ${HALFCYCLE_COMMAND}`);
|
|
15425
16766
|
}
|
|
15426
|
-
const folder = join24(options.home ??
|
|
16767
|
+
const folder = join24(options.home ?? homedir6(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME, engagementId, SAVED_DIR_NAME);
|
|
15427
16768
|
const refs = savedRefs(root);
|
|
15428
16769
|
if (refs === null)
|
|
15429
16770
|
return refused2("Halfcycle could not read the saved copies in this project, so nothing was put back.");
|
|
@@ -15809,7 +17150,7 @@ function lstatOrNull3(p) {
|
|
|
15809
17150
|
}
|
|
15810
17151
|
function realOrSelf(p) {
|
|
15811
17152
|
try {
|
|
15812
|
-
return
|
|
17153
|
+
return realpathSync6(p);
|
|
15813
17154
|
} catch {
|
|
15814
17155
|
return p;
|
|
15815
17156
|
}
|
|
@@ -15856,6 +17197,15 @@ function reportClaudeCodeVersion(verbose2, deps = {}) {
|
|
|
15856
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.
|
|
15857
17198
|
`);
|
|
15858
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
|
+
}
|
|
15859
17209
|
function shortId(id) {
|
|
15860
17210
|
return id.length > 10 ? `${id.slice(0, 8)}\u2026` : id;
|
|
15861
17211
|
}
|
|
@@ -16289,6 +17639,7 @@ ${PROJECT_RESET_LINE}
|
|
|
16289
17639
|
`);
|
|
16290
17640
|
}
|
|
16291
17641
|
reportClaudeCodeVersion(verbose);
|
|
17642
|
+
reportCodexVersion();
|
|
16292
17643
|
const identity = actingIdentity ?? heldAnswer?.identity ?? (credential && heldAnswer === void 0 ? await describeAccount(credential.serviceUrl, credential.token) : void 0);
|
|
16293
17644
|
const written = result.writtenPaths.length;
|
|
16294
17645
|
const merged = result.mergedPaths.length;
|
|
@@ -16315,6 +17666,10 @@ ${PROJECT_RESET_LINE}
|
|
|
16315
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
|
|
16316
17667
|
`);
|
|
16317
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}
|
|
16318
17673
|
`);
|
|
16319
17674
|
}
|
|
16320
17675
|
const consoleUrl = credential?.consoleUrl ?? (reusedWithoutFreshAnswer ? readBundlePin(targetRepo)?.projectPage : void 0);
|