halfcycle 0.4.18 → 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 +1557 -629
- 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 +1431 -97
- 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 +1077 -24
- package/dist/index.js.map +3 -3
- package/dist/install-manifest.d.ts +41 -0
- package/dist/install-manifest.d.ts.map +1 -1
- package/dist/install-plan.d.ts +5 -1
- package/dist/install-plan.d.ts.map +1 -1
- package/dist/install.d.ts +395 -10
- package/dist/install.d.ts.map +1 -1
- package/dist/merge-settings.d.ts +56 -0
- package/dist/merge-settings.d.ts.map +1 -1
- package/dist/node-floor.d.ts +12 -3
- package/dist/node-floor.d.ts.map +1 -1
- package/dist/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/index.js
CHANGED
|
@@ -115,6 +115,7 @@ ${block}`);
|
|
|
115
115
|
|
|
116
116
|
// ../core/dist/hook-wiring.js
|
|
117
117
|
var SETTINGS_REL = ".claude/settings.json";
|
|
118
|
+
var LOCAL_SETTINGS_REL = ".claude/settings.local.json";
|
|
118
119
|
var VENDORED_BIN_REL = ".halfcycle/bin/bin.bundle.mjs";
|
|
119
120
|
var PRE_FILTER_SHELL_FILENAME = "pre-tool-use-filter.sh";
|
|
120
121
|
var PRE_FILTER_FUNCTION = "halfcycle_pt_skip";
|
|
@@ -280,6 +281,7 @@ var HOOK_REMEDY = `Run ${SIGN_IN_COMMAND} in the project folder to sign in.`;
|
|
|
280
281
|
import { z as z2 } from "zod";
|
|
281
282
|
var TASK_PLAN_TASKS_MAX = 500;
|
|
282
283
|
var TASK_PLAN_ID_MAX = 64;
|
|
284
|
+
var TASK_PLAN_BUILDS_NONE = "none";
|
|
283
285
|
var TASK_PLAN_KINDS = ["build", "review", "spec", "walk", "check"];
|
|
284
286
|
var CONTROL_CHARACTER = /[\u0000-\u001f\u007f-\u009f]/;
|
|
285
287
|
var planText = z2.string().refine((text) => !CONTROL_CHARACTER.test(text), { message: "holds a control character" });
|
|
@@ -295,7 +297,8 @@ var taskPlanTaskSchema = z2.object({
|
|
|
295
297
|
interfaces: z2.array(planText).optional()
|
|
296
298
|
}).strict().optional(),
|
|
297
299
|
wholeProject: z2.literal(true).optional(),
|
|
298
|
-
ships: z2.enum(["deploy", "release"]).optional()
|
|
300
|
+
ships: z2.enum(["deploy", "release"]).optional(),
|
|
301
|
+
builds: z2.union([z2.literal(TASK_PLAN_BUILDS_NONE), z2.array(planText)]).optional()
|
|
299
302
|
}).strict().superRefine((task, ctx) => {
|
|
300
303
|
if (task.reviews !== void 0 && task.kind !== "review") {
|
|
301
304
|
ctx.addIssue({
|
|
@@ -332,6 +335,101 @@ var taskListFileSchema = z2.union([
|
|
|
332
335
|
z2.array(z2.unknown())
|
|
333
336
|
]);
|
|
334
337
|
|
|
338
|
+
// ../core/dist/agent-profile.js
|
|
339
|
+
var AGENT_IDS = ["claude-code", "codex"];
|
|
340
|
+
var CLAUDE_CODE_PROFILE = {
|
|
341
|
+
id: "claude-code",
|
|
342
|
+
settingsFiles: [SETTINGS_REL, LOCAL_SETTINGS_REL],
|
|
343
|
+
wiring: HALFCYCLE_HOOK_WIRING,
|
|
344
|
+
commands: HALFCYCLE_HOOK_COMMANDS,
|
|
345
|
+
wiringPaths: HALFCYCLE_HOOK_WIRING_PATHS,
|
|
346
|
+
writeCapableTools: WRITE_CAPABLE_TOOLS,
|
|
347
|
+
projectRoot: "claude-project-dir",
|
|
348
|
+
configFolder: "claude-config",
|
|
349
|
+
mcpRegistration: { file: MCP_REGISTRATION_REL, serversKey: "mcpServers", serverKey: MCP_SERVER_KEY },
|
|
350
|
+
tierModels: {
|
|
351
|
+
deep: { model: "opus" },
|
|
352
|
+
standard: { model: "sonnet" },
|
|
353
|
+
fast: { model: "haiku" }
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
var CODEX_HOOKS_REL = ".codex/hooks.json";
|
|
357
|
+
var CODEX_CONFIG_REL = ".codex/config.toml";
|
|
358
|
+
function codexHookCommand(script, args) {
|
|
359
|
+
const invocation = `cd "$(git rev-parse --show-toplevel)" && bash ./${script}`;
|
|
360
|
+
return args === void 0 ? invocation : `${invocation} ${args}`;
|
|
361
|
+
}
|
|
362
|
+
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.";
|
|
363
|
+
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.";
|
|
364
|
+
function codexHookCommandWindows(script, args) {
|
|
365
|
+
const tick = (text) => "`" + text + "`";
|
|
366
|
+
const argv = [script, ...args === void 0 ? [] : args.split(" ")].map(tick).join(",");
|
|
367
|
+
const program = [
|
|
368
|
+
"const p=require(`path`),f=require(`fs`),c=require(`child_process`);",
|
|
369
|
+
"let d=process.cwd();",
|
|
370
|
+
`for(;;){if(f.existsSync(p.join(d,${tick(VENDORED_BIN_REL)})))break;`,
|
|
371
|
+
"const u=p.dirname(d);",
|
|
372
|
+
"if(u===d){d=``;break}",
|
|
373
|
+
"d=u}",
|
|
374
|
+
`if(d===\`\`){console.error(${tick(CODEX_WINDOWS_NOT_SET_UP)})}else{`,
|
|
375
|
+
"const g=c.spawnSync(`git`,[`--exec-path`],{encoding:`utf8`});",
|
|
376
|
+
"const b=g.status===0?p.join(g.stdout.trim(),`..`,`..`,`..`,`bin`,`bash.exe`):``;",
|
|
377
|
+
`if(b===\`\`||!f.existsSync(b)){console.error(${tick(CODEX_WINDOWS_NO_GIT_BASH)})}else{`,
|
|
378
|
+
`const r=c.spawnSync(b,[${argv}],{cwd:d,stdio:\`inherit\`});`,
|
|
379
|
+
"process.exitCode=typeof r.status===`number`?r.status:1}}"
|
|
380
|
+
].join("");
|
|
381
|
+
return `node -e '${program}'`;
|
|
382
|
+
}
|
|
383
|
+
function codexHookRow(event, matcher, timeout, script, args) {
|
|
384
|
+
return {
|
|
385
|
+
event,
|
|
386
|
+
matcher,
|
|
387
|
+
timeout,
|
|
388
|
+
command: codexHookCommand(script, args),
|
|
389
|
+
script,
|
|
390
|
+
commandWindows: codexHookCommandWindows(script, args)
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
var CODEX_WRITE_CAPABLE_TOOLS = ["apply_patch", "Edit", "Write"];
|
|
394
|
+
var CODEX_PRE_TOOL_USE_MATCHER = "^(Bash|apply_patch)$";
|
|
395
|
+
var CODEX_HOOK_WIRING = [
|
|
396
|
+
codexHookRow("SessionStart", null, 5, SESSION_START_MARKER_REL),
|
|
397
|
+
codexHookRow("SessionStart", null, 10, GUARD_RUNNER_REL, "refresh-credential --agent codex"),
|
|
398
|
+
codexHookRow("UserPromptSubmit", null, 5, USER_PROMPT_REMINDER_REL),
|
|
399
|
+
codexHookRow("UserPromptSubmit", null, 10, USER_PROMPT_SCOPE_REL, "--agent codex"),
|
|
400
|
+
codexHookRow("UserPromptSubmit", null, 10, USER_PROMPT_FINDINGS_REL, "--agent codex"),
|
|
401
|
+
codexHookRow("PreToolUse", CODEX_PRE_TOOL_USE_MATCHER, 10, GUARD_RUNNER_REL, "hook pre-tool-use --agent codex"),
|
|
402
|
+
codexHookRow("PostToolUse", CODEX_WRITE_CAPABLE_TOOLS.join("|"), 60, GUARD_RUNNER_REL, "hook post-tool-use --agent codex"),
|
|
403
|
+
codexHookRow("Stop", null, 60, GUARD_RUNNER_REL, "hook stop --agent codex"),
|
|
404
|
+
codexHookRow("SubagentStop", null, 60, GUARD_RUNNER_REL, "hook subagent-stop --agent codex"),
|
|
405
|
+
codexHookRow("SessionEnd", null, 3, SESSION_END_MARKER_REL)
|
|
406
|
+
];
|
|
407
|
+
var RETIRED_CODEX_HOOK_COMMANDS = [];
|
|
408
|
+
function codexHookCommandSet(retired) {
|
|
409
|
+
return /* @__PURE__ */ new Set([...CODEX_HOOK_WIRING.map((entry) => entry.command), ...retired]);
|
|
410
|
+
}
|
|
411
|
+
var CODEX_PROFILE = {
|
|
412
|
+
id: "codex",
|
|
413
|
+
settingsFiles: [CODEX_HOOKS_REL],
|
|
414
|
+
wiring: CODEX_HOOK_WIRING,
|
|
415
|
+
commands: codexHookCommandSet(RETIRED_CODEX_HOOK_COMMANDS),
|
|
416
|
+
wiringPaths: [.../* @__PURE__ */ new Set([CODEX_HOOKS_REL, ...CODEX_HOOK_WIRING.map((entry) => entry.script), VENDORED_BIN_REL])],
|
|
417
|
+
writeCapableTools: CODEX_WRITE_CAPABLE_TOOLS,
|
|
418
|
+
projectRoot: "git-toplevel",
|
|
419
|
+
configFolder: "codex-home",
|
|
420
|
+
mcpRegistration: { file: CODEX_CONFIG_REL, serversKey: "mcp_servers", serverKey: MCP_SERVER_KEY },
|
|
421
|
+
tierModels: {
|
|
422
|
+
deep: { model: "gpt-6-luna", effort: "high" },
|
|
423
|
+
standard: { model: "gpt-6-luna", effort: "medium" },
|
|
424
|
+
fast: { model: "gpt-6-luna", effort: "low" }
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
var AGENT_PROFILES = {
|
|
428
|
+
"claude-code": CLAUDE_CODE_PROFILE,
|
|
429
|
+
codex: CODEX_PROFILE
|
|
430
|
+
};
|
|
431
|
+
var ALL_HOOK_COMMANDS = new Set(AGENT_IDS.flatMap((id) => [...AGENT_PROFILES[id].commands]));
|
|
432
|
+
|
|
335
433
|
// ../events/dist/result.js
|
|
336
434
|
import { z as z3 } from "zod";
|
|
337
435
|
var firedGuardSchema = z3.object({
|
|
@@ -1195,7 +1293,10 @@ var buildSnapshotReportResultSchema = z8.object({
|
|
|
1195
1293
|
// Which phase the graph was recorded against.
|
|
1196
1294
|
phase: buildPhaseSchema,
|
|
1197
1295
|
// When it was recorded.
|
|
1198
|
-
reportedAt: utcIso8601
|
|
1296
|
+
reportedAt: utcIso8601,
|
|
1297
|
+
// One sentence to pass on to the developer about the graph just stored. Absent
|
|
1298
|
+
// when there is nothing to say.
|
|
1299
|
+
warning: z8.string().min(1).max(400).optional()
|
|
1199
1300
|
}).strict();
|
|
1200
1301
|
|
|
1201
1302
|
// ../events/dist/phase-plan.js
|
|
@@ -3632,7 +3733,14 @@ function serializeManifest(manifest) {
|
|
|
3632
3733
|
files: [...manifest.files].sort((a, b) => byString(a.path, b.path)),
|
|
3633
3734
|
createdDirs: [...manifest.createdDirs].sort(byString),
|
|
3634
3735
|
leftAlone: [...manifest.leftAlone].sort(byString),
|
|
3635
|
-
settings
|
|
3736
|
+
settings,
|
|
3737
|
+
...manifest.codexHooks !== void 0 ? {
|
|
3738
|
+
codexHooks: {
|
|
3739
|
+
...manifest.codexHooks,
|
|
3740
|
+
hookCommands: [...manifest.codexHooks.hookCommands].sort(byString),
|
|
3741
|
+
eventsCreated: [...manifest.codexHooks.eventsCreated].sort(byString)
|
|
3742
|
+
}
|
|
3743
|
+
} : {}
|
|
3636
3744
|
};
|
|
3637
3745
|
return JSON.stringify(canonicalJson(ordered), null, 2) + "\n";
|
|
3638
3746
|
}
|
|
@@ -3686,7 +3794,7 @@ function parseManifest(text) {
|
|
|
3686
3794
|
}
|
|
3687
3795
|
if (!isRecord(raw))
|
|
3688
3796
|
throw new UnreadableManifestError("it is not a JSON object");
|
|
3689
|
-
onlyKeys(raw, ["format", "files", "createdDirs", "leftAlone", "settings", "mcp", "gitignore", "gitattributes"], "the manifest");
|
|
3797
|
+
onlyKeys(raw, ["format", "files", "createdDirs", "leftAlone", "settings", "mcp", "codexHooks", "codexConfig", "gitignore", "gitattributes"], "the manifest");
|
|
3690
3798
|
if (raw["format"] !== INSTALL_MANIFEST_FORMAT) {
|
|
3691
3799
|
throw new UnreadableManifestError(`its format is not ${INSTALL_MANIFEST_FORMAT}`);
|
|
3692
3800
|
}
|
|
@@ -3752,6 +3860,30 @@ function parseManifest(text) {
|
|
|
3752
3860
|
...typeof m["entrySha256"] === "string" ? { entrySha256: m["entrySha256"] } : {}
|
|
3753
3861
|
};
|
|
3754
3862
|
}
|
|
3863
|
+
let codexHooks;
|
|
3864
|
+
if (raw["codexHooks"] !== void 0) {
|
|
3865
|
+
const c = raw["codexHooks"];
|
|
3866
|
+
if (!isRecord(c))
|
|
3867
|
+
throw new UnreadableManifestError('"codexHooks" is not an object');
|
|
3868
|
+
onlyKeys(c, ["created", "adopted", "hookCommands", "eventsCreated", "hooksCreated"], '"codexHooks"');
|
|
3869
|
+
codexHooks = {
|
|
3870
|
+
created: bool(c["created"], "codexHooks.created"),
|
|
3871
|
+
adopted: bool(c["adopted"], "codexHooks.adopted"),
|
|
3872
|
+
hookCommands: stringArray(c["hookCommands"], "codexHooks.hookCommands"),
|
|
3873
|
+
eventsCreated: stringArray(c["eventsCreated"], "codexHooks.eventsCreated"),
|
|
3874
|
+
hooksCreated: bool(c["hooksCreated"], "codexHooks.hooksCreated")
|
|
3875
|
+
};
|
|
3876
|
+
}
|
|
3877
|
+
let codexConfig;
|
|
3878
|
+
if (raw["codexConfig"] !== void 0) {
|
|
3879
|
+
const c = raw["codexConfig"];
|
|
3880
|
+
if (!isRecord(c))
|
|
3881
|
+
throw new UnreadableManifestError('"codexConfig" is not an object');
|
|
3882
|
+
onlyKeys(c, ["created", "block"], '"codexConfig"');
|
|
3883
|
+
if (typeof c["block"] !== "string")
|
|
3884
|
+
throw new UnreadableManifestError('"codexConfig.block" is not a string');
|
|
3885
|
+
codexConfig = { created: bool(c["created"], "codexConfig.created"), block: c["block"] };
|
|
3886
|
+
}
|
|
3755
3887
|
if (raw["gitignore"] === void 0)
|
|
3756
3888
|
throw new UnreadableManifestError('"gitignore" is missing');
|
|
3757
3889
|
const gitignore = appendedTextRecord(raw["gitignore"], "gitignore");
|
|
@@ -3770,6 +3902,8 @@ function parseManifest(text) {
|
|
|
3770
3902
|
leftAlone,
|
|
3771
3903
|
settings,
|
|
3772
3904
|
...mcp !== void 0 ? { mcp } : {},
|
|
3905
|
+
...codexHooks !== void 0 ? { codexHooks } : {},
|
|
3906
|
+
...codexConfig !== void 0 ? { codexConfig } : {},
|
|
3773
3907
|
gitignore,
|
|
3774
3908
|
...gitattributes !== void 0 ? { gitattributes } : {}
|
|
3775
3909
|
};
|
|
@@ -3787,6 +3921,8 @@ function startManifest(previous, isMember, isMemberDir) {
|
|
|
3787
3921
|
createdDirs: new Set((previous?.createdDirs ?? []).filter(isMemberDir)),
|
|
3788
3922
|
settings: previous?.settings,
|
|
3789
3923
|
mcp: previous?.mcp,
|
|
3924
|
+
codexHooks: previous?.codexHooks,
|
|
3925
|
+
codexConfig: previous?.codexConfig,
|
|
3790
3926
|
gitignore: previous?.gitignore,
|
|
3791
3927
|
gitattributes: previous?.gitattributes
|
|
3792
3928
|
};
|
|
@@ -3847,6 +3983,19 @@ function recordMcp(draft, observed) {
|
|
|
3847
3983
|
...entrySha256 !== void 0 ? { entrySha256 } : {}
|
|
3848
3984
|
};
|
|
3849
3985
|
}
|
|
3986
|
+
function recordCodexHooks(draft, observed) {
|
|
3987
|
+
const prev = draft.codexHooks;
|
|
3988
|
+
draft.codexHooks = prev === void 0 ? observed : {
|
|
3989
|
+
created: prev.created,
|
|
3990
|
+
adopted: prev.adopted,
|
|
3991
|
+
hookCommands: union(prev.hookCommands, observed.hookCommands),
|
|
3992
|
+
eventsCreated: union(prev.eventsCreated, observed.eventsCreated),
|
|
3993
|
+
hooksCreated: prev.hooksCreated
|
|
3994
|
+
};
|
|
3995
|
+
}
|
|
3996
|
+
function recordCodexConfig(draft, observed) {
|
|
3997
|
+
draft.codexConfig = { created: draft.codexConfig?.created ?? observed.created, block: observed.block };
|
|
3998
|
+
}
|
|
3850
3999
|
function recordGitignore(draft, created, appended) {
|
|
3851
4000
|
draft.gitignore = carryAppended(draft.gitignore, created, appended);
|
|
3852
4001
|
}
|
|
@@ -3880,6 +4029,8 @@ function finishManifest(draft) {
|
|
|
3880
4029
|
denyCreated: false
|
|
3881
4030
|
},
|
|
3882
4031
|
...draft.mcp !== void 0 ? { mcp: draft.mcp } : {},
|
|
4032
|
+
...draft.codexHooks !== void 0 ? { codexHooks: draft.codexHooks } : {},
|
|
4033
|
+
...draft.codexConfig !== void 0 ? { codexConfig: draft.codexConfig } : {},
|
|
3883
4034
|
gitignore: draft.gitignore ?? { created: false, appended: [] },
|
|
3884
4035
|
...draft.gitattributes !== void 0 ? { gitattributes: draft.gitattributes } : {}
|
|
3885
4036
|
};
|
|
@@ -4025,6 +4176,30 @@ var PREVIEW_WORDS = Object.freeze({
|
|
|
4025
4176
|
change: "takes out an older Halfcycle sign-in; your own lines stay as they are",
|
|
4026
4177
|
leave: "an older Halfcycle sign-in that cannot be moved yet; left as it is",
|
|
4027
4178
|
yours: YOURS
|
|
4179
|
+
},
|
|
4180
|
+
"codex-checks": {
|
|
4181
|
+
add: "Codex's checks, the same ones Halfcycle runs for Claude Code",
|
|
4182
|
+
change: "adds the checks Halfcycle needs; your own entries stay as they are",
|
|
4183
|
+
leave: "already has the checks Halfcycle needs",
|
|
4184
|
+
yours: YOURS
|
|
4185
|
+
},
|
|
4186
|
+
"codex-server": {
|
|
4187
|
+
add: "tells Codex how to reach Halfcycle's service",
|
|
4188
|
+
change: "adds Halfcycle's entry; your own entries stay as they are",
|
|
4189
|
+
leave: "already has Halfcycle's entry",
|
|
4190
|
+
yours: YOURS
|
|
4191
|
+
},
|
|
4192
|
+
skill: {
|
|
4193
|
+
add: "a skill your coding agent can run to take the next Halfcycle step",
|
|
4194
|
+
change: "a newer version of this Halfcycle skill",
|
|
4195
|
+
leave: SAME,
|
|
4196
|
+
yours: YOURS
|
|
4197
|
+
},
|
|
4198
|
+
"crew-profile": {
|
|
4199
|
+
add: "a member of your build crew, so your coding agent shows who is working",
|
|
4200
|
+
change: "a newer version of this build-crew member",
|
|
4201
|
+
leave: SAME,
|
|
4202
|
+
yours: YOURS
|
|
4028
4203
|
}
|
|
4029
4204
|
});
|
|
4030
4205
|
var kindTable;
|
|
@@ -4049,13 +4224,21 @@ function kinds() {
|
|
|
4049
4224
|
[RETIRED_MCP_HEADERS_HELPER_REL, "halfcycle-folder"],
|
|
4050
4225
|
[LEGACY_CI_STANZA_REL, "halfcycle-folder"],
|
|
4051
4226
|
[CAPTURED_INDEX_REL, "captured-index"],
|
|
4052
|
-
[LEGACY_ENV_LOCAL_REL, "old-sign-in"]
|
|
4227
|
+
[LEGACY_ENV_LOCAL_REL, "old-sign-in"],
|
|
4228
|
+
[CODEX_HOOKS_REL2, "codex-checks"],
|
|
4229
|
+
[CODEX_CONFIG_REL2, "codex-server"]
|
|
4053
4230
|
]);
|
|
4054
4231
|
return kindTable;
|
|
4055
4232
|
}
|
|
4056
4233
|
function previewKindOf(rel) {
|
|
4057
4234
|
if (/^\.claude\/commands\/[^/]+\.md$/.test(rel))
|
|
4058
4235
|
return "command";
|
|
4236
|
+
if (/^\.agents\/skills\/[^/]+\/SKILL\.md$/.test(rel))
|
|
4237
|
+
return "skill";
|
|
4238
|
+
if (/^\.claude\/agents\/[^/]+\.md$/.test(rel))
|
|
4239
|
+
return "crew-profile";
|
|
4240
|
+
if (/^\.codex\/agents\/[^/]+\.toml$/.test(rel))
|
|
4241
|
+
return "crew-profile";
|
|
4059
4242
|
return kinds().get(rel);
|
|
4060
4243
|
}
|
|
4061
4244
|
function kindOf(rel) {
|
|
@@ -4081,6 +4264,7 @@ function planInstall(options) {
|
|
|
4081
4264
|
const previous = readPreviousManifest(targetRepo);
|
|
4082
4265
|
const dirsBefore = new Set([...writeSet.dirs].filter((dir) => existsSync3(join5(targetRepo, dir))));
|
|
4083
4266
|
const { version, commands } = planCommandStubs(targetRepo, previous, writeSet);
|
|
4267
|
+
const codexSkills = planCodexSkills(targetRepo, previous, writeSet);
|
|
4084
4268
|
const vendored = planVendoredBinary(targetRepo);
|
|
4085
4269
|
let settings;
|
|
4086
4270
|
try {
|
|
@@ -4094,6 +4278,8 @@ function planInstall(options) {
|
|
|
4094
4278
|
const ciStanza = planLegacyCiStanzaRemoval(targetRepo);
|
|
4095
4279
|
const mcp = planMcpRegistration(targetRepo, preview ? "pending" : credential);
|
|
4096
4280
|
const mcpHelper = mcp.kind === "none" ? null : planMcpHeadersHelperRemoval(targetRepo);
|
|
4281
|
+
const codexHooks = planCodexHooks(targetRepo);
|
|
4282
|
+
const codexConfig = planCodexConfig(targetRepo, preview ? "pending" : credential);
|
|
4097
4283
|
const gitignore = planGitignore(targetRepo);
|
|
4098
4284
|
const gitattributes = planGitattributes(targetRepo);
|
|
4099
4285
|
const identity = planProjectIdentity(targetRepo);
|
|
@@ -4105,6 +4291,7 @@ function planInstall(options) {
|
|
|
4105
4291
|
const legacy = migrateLegacyEnvLocal(targetRepo, storedAfter);
|
|
4106
4292
|
const pin = preview ? planBundlePinPreview(targetRepo, version, home) : planBundlePin(targetRepo, version, engagementId, engagementType, createdByAccountId, credential?.methodVersion);
|
|
4107
4293
|
const crew = planCrewRoster(targetRepo);
|
|
4294
|
+
const crewProfiles = planCrewProfiles(targetRepo, previous, writeSet);
|
|
4108
4295
|
const scan = { text: planBootstrapScan(targetRepo).text };
|
|
4109
4296
|
const steps = {
|
|
4110
4297
|
targetRepo,
|
|
@@ -4117,12 +4304,15 @@ function planInstall(options) {
|
|
|
4117
4304
|
previous,
|
|
4118
4305
|
dirsBefore,
|
|
4119
4306
|
commands,
|
|
4307
|
+
codexSkills,
|
|
4120
4308
|
vendored,
|
|
4121
4309
|
settings,
|
|
4122
4310
|
hooks,
|
|
4123
4311
|
ciStanza,
|
|
4124
4312
|
mcp,
|
|
4125
4313
|
mcpHelper,
|
|
4314
|
+
codexHooks,
|
|
4315
|
+
codexConfig,
|
|
4126
4316
|
gitignore,
|
|
4127
4317
|
gitattributes,
|
|
4128
4318
|
identity,
|
|
@@ -4130,6 +4320,7 @@ function planInstall(options) {
|
|
|
4130
4320
|
legacy,
|
|
4131
4321
|
pin,
|
|
4132
4322
|
crew,
|
|
4323
|
+
crewProfiles,
|
|
4133
4324
|
scan
|
|
4134
4325
|
};
|
|
4135
4326
|
const record2 = preview ? null : planInstallRecord(targetRepo, accountInstall(steps, predicted(steps)).manifestText);
|
|
@@ -4155,16 +4346,24 @@ function predicted(steps) {
|
|
|
4155
4346
|
lands(planned.rel, planned.text, planned.outcome === "written");
|
|
4156
4347
|
};
|
|
4157
4348
|
steps.commands.forEach(file);
|
|
4349
|
+
steps.codexSkills.forEach(file);
|
|
4158
4350
|
file(steps.vendored);
|
|
4159
4351
|
file(steps.settings.file);
|
|
4160
4352
|
steps.hooks.forEach(file);
|
|
4161
4353
|
if (steps.mcp.kind === "write")
|
|
4162
4354
|
lands(MCP_REGISTRATION_REL, steps.mcp.text, steps.mcp.outcome !== "skipped");
|
|
4355
|
+
if (steps.codexHooks.outcome === "written" && steps.codexHooks.text !== null) {
|
|
4356
|
+
lands(CODEX_HOOKS_REL2, steps.codexHooks.text, true);
|
|
4357
|
+
}
|
|
4358
|
+
if (steps.codexConfig.outcome === "written" && steps.codexConfig.text !== null) {
|
|
4359
|
+
lands(CODEX_CONFIG_REL2, steps.codexConfig.text, true);
|
|
4360
|
+
}
|
|
4163
4361
|
if (steps.identity.after !== null)
|
|
4164
4362
|
lands(PROJECT_IDENTITY_REL, steps.identity.after, steps.identity.action === "add");
|
|
4165
4363
|
if (steps.pin !== null)
|
|
4166
4364
|
file(steps.pin);
|
|
4167
4365
|
file(steps.crew);
|
|
4366
|
+
steps.crewProfiles.forEach(file);
|
|
4168
4367
|
if (steps.scan.text !== null)
|
|
4169
4368
|
lands(BOOTSTRAP_STATE_REL, steps.scan.text, true);
|
|
4170
4369
|
const mergeAfter = (planned) => planned.outcome === "written" ? planned.after : planned.before;
|
|
@@ -4222,6 +4421,7 @@ function entriesOf(steps, record2) {
|
|
|
4222
4421
|
entries.push({ path: rel, action: there ? "change" : "add", kind: kindOf(rel), bytes: PENDING });
|
|
4223
4422
|
};
|
|
4224
4423
|
steps.commands.forEach(fromFile);
|
|
4424
|
+
steps.codexSkills.forEach(fromFile);
|
|
4225
4425
|
fromFile(steps.vendored);
|
|
4226
4426
|
const settings = steps.settings;
|
|
4227
4427
|
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 });
|
|
@@ -4237,6 +4437,38 @@ function entriesOf(steps, record2) {
|
|
|
4237
4437
|
entries.push({ path: MCP_REGISTRATION_REL, action: "leave", kind: "server-entry", bytes: null });
|
|
4238
4438
|
}
|
|
4239
4439
|
retired(RETIRED_MCP_HEADERS_HELPER_REL, steps.mcpHelper);
|
|
4440
|
+
const codexHooks = steps.codexHooks;
|
|
4441
|
+
if (codexHooks.outcome === "written") {
|
|
4442
|
+
entries.push({ path: CODEX_HOOKS_REL2, action: codexHooks.before === null ? "add" : "change", kind: "codex-checks", bytes: codexHooks.text });
|
|
4443
|
+
} else if (codexHooks.outcome === "current") {
|
|
4444
|
+
entries.push({ path: CODEX_HOOKS_REL2, action: "leave", kind: "codex-checks", bytes: null });
|
|
4445
|
+
} else {
|
|
4446
|
+
entries.push({ path: CODEX_HOOKS_REL2, action: "leave", kind: "codex-checks", bytes: null, yours: true });
|
|
4447
|
+
}
|
|
4448
|
+
const codexConfig = steps.codexConfig;
|
|
4449
|
+
switch (codexConfig.outcome) {
|
|
4450
|
+
case "written":
|
|
4451
|
+
entries.push({ path: CODEX_CONFIG_REL2, action: codexConfig.before === null ? "add" : "change", kind: "codex-server", bytes: codexConfig.text });
|
|
4452
|
+
break;
|
|
4453
|
+
case "pending":
|
|
4454
|
+
pending(CODEX_CONFIG_REL2);
|
|
4455
|
+
break;
|
|
4456
|
+
case "current":
|
|
4457
|
+
case "no-credential":
|
|
4458
|
+
entries.push({ path: CODEX_CONFIG_REL2, action: "leave", kind: "codex-server", bytes: null });
|
|
4459
|
+
break;
|
|
4460
|
+
case "conflict":
|
|
4461
|
+
case "link":
|
|
4462
|
+
case "refused-address":
|
|
4463
|
+
entries.push({
|
|
4464
|
+
path: CODEX_CONFIG_REL2,
|
|
4465
|
+
action: "leave",
|
|
4466
|
+
kind: "codex-server",
|
|
4467
|
+
bytes: null,
|
|
4468
|
+
...existsSync3(join5(targetRepo, CODEX_CONFIG_REL2)) ? { yours: true } : {}
|
|
4469
|
+
});
|
|
4470
|
+
break;
|
|
4471
|
+
}
|
|
4240
4472
|
merge(GITIGNORE_REL, steps.gitignore);
|
|
4241
4473
|
merge(GITATTRIBUTES_REL, steps.gitattributes);
|
|
4242
4474
|
const identity = steps.identity;
|
|
@@ -4259,6 +4491,7 @@ function entriesOf(steps, record2) {
|
|
|
4259
4491
|
else
|
|
4260
4492
|
fromFile(steps.pin);
|
|
4261
4493
|
fromFile(steps.crew);
|
|
4494
|
+
steps.crewProfiles.forEach(fromFile);
|
|
4262
4495
|
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 });
|
|
4263
4496
|
if (record2 === null)
|
|
4264
4497
|
pending(INSTALL_MANIFEST_REL);
|
|
@@ -4299,6 +4532,8 @@ function applyInstallPlan(plan) {
|
|
|
4299
4532
|
}
|
|
4300
4533
|
for (const command of steps.commands)
|
|
4301
4534
|
putPlannedFile(targetRepo, command);
|
|
4535
|
+
for (const skill of steps.codexSkills)
|
|
4536
|
+
putPlannedFile(targetRepo, skill);
|
|
4302
4537
|
putPlannedFile(targetRepo, steps.vendored);
|
|
4303
4538
|
putPlannedFile(targetRepo, steps.settings.file);
|
|
4304
4539
|
for (const hook of steps.hooks)
|
|
@@ -4306,6 +4541,8 @@ function applyInstallPlan(plan) {
|
|
|
4306
4541
|
const ciStanza = steps.ciStanza === "removed" ? removePlannedRetiredFile(targetRepo, LEGACY_CI_STANZA_REL) : steps.ciStanza;
|
|
4307
4542
|
putPlannedMcp(targetRepo, steps.mcp);
|
|
4308
4543
|
const mcpHelper = steps.mcpHelper === "removed" ? removePlannedRetiredFile(targetRepo, RETIRED_MCP_HEADERS_HELPER_REL) : steps.mcpHelper;
|
|
4544
|
+
putPlannedCodexHooks(targetRepo, steps.codexHooks);
|
|
4545
|
+
putPlannedCodexConfig(targetRepo, steps.codexConfig);
|
|
4309
4546
|
const gitignore = putPlannedGitignore(targetRepo, steps.gitignore);
|
|
4310
4547
|
const gitignorePath = join5(targetRepo, GITIGNORE_REL);
|
|
4311
4548
|
const gitignoreAfter = existsSync3(gitignorePath) ? readFileSync4(gitignorePath, "utf-8") : null;
|
|
@@ -4318,6 +4555,8 @@ function applyInstallPlan(plan) {
|
|
|
4318
4555
|
const legacy = applyLegacyMigration(targetRepo, migrateLegacyEnvLocal(targetRepo, stored));
|
|
4319
4556
|
writeBundlePin(targetRepo, pin, []);
|
|
4320
4557
|
putPlannedFile(targetRepo, steps.crew);
|
|
4558
|
+
for (const profile of steps.crewProfiles)
|
|
4559
|
+
putPlannedFile(targetRepo, profile);
|
|
4321
4560
|
if (steps.scan.text !== null)
|
|
4322
4561
|
writeBootstrapState(targetRepo, steps.scan.text);
|
|
4323
4562
|
const { report, installManifest, manifestText } = accountInstall(steps, {
|
|
@@ -4347,9 +4586,15 @@ function applyInstallPlan(plan) {
|
|
|
4347
4586
|
replacedPaths: report.replacedPaths,
|
|
4348
4587
|
bootstrapScanRan: steps.scan.text !== null,
|
|
4349
4588
|
legacyCredentialKept: legacy.outcome === "kept" ? { keys: legacy.unbacked, missing: legacy.missingForAdoption } : null,
|
|
4350
|
-
notShared
|
|
4589
|
+
notShared,
|
|
4590
|
+
codex: { hooks: steps.codexHooks.outcome, config: appliedCodexConfig(steps.codexConfig.outcome) }
|
|
4351
4591
|
};
|
|
4352
4592
|
}
|
|
4593
|
+
function appliedCodexConfig(outcome) {
|
|
4594
|
+
if (outcome === "pending")
|
|
4595
|
+
throw new Error("[halfcycle] a preview cannot be applied: it is still waiting for the service.");
|
|
4596
|
+
return outcome;
|
|
4597
|
+
}
|
|
4353
4598
|
|
|
4354
4599
|
// dist/mcp-endpoint.js
|
|
4355
4600
|
var MCP_ENDPOINT_PATH = "/mcp";
|
|
@@ -4360,8 +4605,8 @@ function mcpEndpointUrl(origin) {
|
|
|
4360
4605
|
// dist/node-floor.js
|
|
4361
4606
|
var NODE_FLOOR_MAJOR = 18;
|
|
4362
4607
|
var NODE_FLOOR_SENTENCE_HEAD = `Halfcycle needs Node ${NODE_FLOOR_MAJOR} or newer; you have `;
|
|
4363
|
-
function nodeFloorProgram(guarded) {
|
|
4364
|
-
return `if(+process.versions.node.split('.')[0]<${NODE_FLOOR_MAJOR}){console.error('${NODE_FLOOR_SENTENCE_HEAD}'+process.versions.node)
|
|
4608
|
+
function nodeFloorProgram(guarded, belowFloor = "process.exitCode=1") {
|
|
4609
|
+
return `if(+process.versions.node.split('.')[0]<${NODE_FLOOR_MAJOR}){console.error('${NODE_FLOOR_SENTENCE_HEAD}'+process.versions.node);` + belowFloor + `}else{${guarded}}`;
|
|
4365
4610
|
}
|
|
4366
4611
|
|
|
4367
4612
|
// dist/merge-settings.js
|
|
@@ -4408,6 +4653,75 @@ function mergeSettings(existing, generated, retiredOurs, statusLine) {
|
|
|
4408
4653
|
}
|
|
4409
4654
|
return merged;
|
|
4410
4655
|
}
|
|
4656
|
+
function isPlainRecord(value) {
|
|
4657
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
4658
|
+
}
|
|
4659
|
+
function mergeCodexHooks(existing, generated, ours) {
|
|
4660
|
+
const existingHooks = isPlainRecord(existing["hooks"]) ? existing["hooks"] : {};
|
|
4661
|
+
const events = [
|
|
4662
|
+
...Object.keys(existingHooks),
|
|
4663
|
+
...Object.keys(generated.hooks).filter((event) => !(event in existingHooks))
|
|
4664
|
+
];
|
|
4665
|
+
const mergedHooks = {};
|
|
4666
|
+
for (const event of events) {
|
|
4667
|
+
const rows = generated.hooks[event] ?? [];
|
|
4668
|
+
const current = existingHooks[event];
|
|
4669
|
+
if (current === void 0) {
|
|
4670
|
+
mergedHooks[event] = rows.map(cloneGroup);
|
|
4671
|
+
continue;
|
|
4672
|
+
}
|
|
4673
|
+
if (!Array.isArray(current)) {
|
|
4674
|
+
mergedHooks[event] = current;
|
|
4675
|
+
continue;
|
|
4676
|
+
}
|
|
4677
|
+
const wanted = new Map(rows.map((row) => [row.hooks[0].command, row]));
|
|
4678
|
+
const placed = /* @__PURE__ */ new Set();
|
|
4679
|
+
const next = [];
|
|
4680
|
+
for (const group of current) {
|
|
4681
|
+
if (!isPlainRecord(group) || !Array.isArray(group["hooks"]) || !isHalfcycleEntry({ hooks: group["hooks"] }, ours)) {
|
|
4682
|
+
next.push(group);
|
|
4683
|
+
continue;
|
|
4684
|
+
}
|
|
4685
|
+
const hooks = group["hooks"];
|
|
4686
|
+
if (hooks.length === 1) {
|
|
4687
|
+
const command = hooks[0].command;
|
|
4688
|
+
const row = wanted.get(command);
|
|
4689
|
+
if (row !== void 0 && !placed.has(command)) {
|
|
4690
|
+
placed.add(command);
|
|
4691
|
+
next.push(cloneGroup(row));
|
|
4692
|
+
}
|
|
4693
|
+
continue;
|
|
4694
|
+
}
|
|
4695
|
+
const kept = [];
|
|
4696
|
+
for (const hook of hooks) {
|
|
4697
|
+
if (!isHalfcycleCommand(hook, ours)) {
|
|
4698
|
+
kept.push(hook);
|
|
4699
|
+
continue;
|
|
4700
|
+
}
|
|
4701
|
+
const command = hook.command;
|
|
4702
|
+
const row = wanted.get(command);
|
|
4703
|
+
if (row !== void 0 && !placed.has(command) && group["matcher"] === row.matcher) {
|
|
4704
|
+
placed.add(command);
|
|
4705
|
+
kept.push({ ...row.hooks[0] });
|
|
4706
|
+
}
|
|
4707
|
+
}
|
|
4708
|
+
if (kept.length > 0)
|
|
4709
|
+
next.push({ ...group, hooks: kept });
|
|
4710
|
+
}
|
|
4711
|
+
for (const row of rows) {
|
|
4712
|
+
if (!placed.has(row.hooks[0].command))
|
|
4713
|
+
next.push(cloneGroup(row));
|
|
4714
|
+
}
|
|
4715
|
+
mergedHooks[event] = next;
|
|
4716
|
+
}
|
|
4717
|
+
return { ...existing, hooks: mergedHooks };
|
|
4718
|
+
}
|
|
4719
|
+
function cloneGroup(group) {
|
|
4720
|
+
return {
|
|
4721
|
+
...group.matcher === void 0 ? {} : { matcher: group.matcher },
|
|
4722
|
+
hooks: group.hooks.map((hook) => ({ ...hook }))
|
|
4723
|
+
};
|
|
4724
|
+
}
|
|
4411
4725
|
function mergePermissions(existing, generated, retiredOurs) {
|
|
4412
4726
|
if (existing === void 0 && generated === void 0)
|
|
4413
4727
|
return void 0;
|
|
@@ -4430,8 +4744,261 @@ function mergePermissions(existing, generated, retiredOurs) {
|
|
|
4430
4744
|
return result;
|
|
4431
4745
|
}
|
|
4432
4746
|
|
|
4747
|
+
// dist/toml-lines.js
|
|
4748
|
+
var BARE_KEY_CHAR = /[A-Za-z0-9_-]/;
|
|
4749
|
+
var Unreadable = class extends Error {
|
|
4750
|
+
};
|
|
4751
|
+
function skipBlank(line, i) {
|
|
4752
|
+
while (i < line.length && (line[i] === " " || line[i] === " "))
|
|
4753
|
+
i += 1;
|
|
4754
|
+
return i;
|
|
4755
|
+
}
|
|
4756
|
+
function readKey(line, start) {
|
|
4757
|
+
const segments = [];
|
|
4758
|
+
let i = start;
|
|
4759
|
+
for (; ; ) {
|
|
4760
|
+
i = skipBlank(line, i);
|
|
4761
|
+
const c = line[i];
|
|
4762
|
+
if (c === '"') {
|
|
4763
|
+
const close = line.indexOf('"', i + 1);
|
|
4764
|
+
if (close < 0)
|
|
4765
|
+
throw new Unreadable();
|
|
4766
|
+
const segment = line.slice(i + 1, close);
|
|
4767
|
+
if (segment.includes("\\"))
|
|
4768
|
+
throw new Unreadable();
|
|
4769
|
+
segments.push(segment);
|
|
4770
|
+
i = close + 1;
|
|
4771
|
+
} else if (c === "'") {
|
|
4772
|
+
const close = line.indexOf("'", i + 1);
|
|
4773
|
+
if (close < 0)
|
|
4774
|
+
throw new Unreadable();
|
|
4775
|
+
segments.push(line.slice(i + 1, close));
|
|
4776
|
+
i = close + 1;
|
|
4777
|
+
} else {
|
|
4778
|
+
let j = i;
|
|
4779
|
+
while (j < line.length && BARE_KEY_CHAR.test(line[j]))
|
|
4780
|
+
j += 1;
|
|
4781
|
+
if (j === i)
|
|
4782
|
+
throw new Unreadable();
|
|
4783
|
+
segments.push(line.slice(i, j));
|
|
4784
|
+
i = j;
|
|
4785
|
+
}
|
|
4786
|
+
i = skipBlank(line, i);
|
|
4787
|
+
if (line[i] !== ".")
|
|
4788
|
+
return { segments, end: i };
|
|
4789
|
+
i += 1;
|
|
4790
|
+
}
|
|
4791
|
+
}
|
|
4792
|
+
function readValue(line, start, state) {
|
|
4793
|
+
let i = start;
|
|
4794
|
+
while (i < line.length) {
|
|
4795
|
+
if (state.multiline !== null) {
|
|
4796
|
+
const delimiter = state.multiline;
|
|
4797
|
+
const escapes = delimiter === '"""';
|
|
4798
|
+
while (i < line.length) {
|
|
4799
|
+
if (escapes && line[i] === "\\") {
|
|
4800
|
+
i += 2;
|
|
4801
|
+
continue;
|
|
4802
|
+
}
|
|
4803
|
+
if (line.startsWith(delimiter, i)) {
|
|
4804
|
+
i += 3;
|
|
4805
|
+
let extra = 0;
|
|
4806
|
+
while (extra < 2 && line[i] === delimiter[0]) {
|
|
4807
|
+
i += 1;
|
|
4808
|
+
extra += 1;
|
|
4809
|
+
}
|
|
4810
|
+
state.multiline = null;
|
|
4811
|
+
break;
|
|
4812
|
+
}
|
|
4813
|
+
i += 1;
|
|
4814
|
+
}
|
|
4815
|
+
continue;
|
|
4816
|
+
}
|
|
4817
|
+
const c = line[i];
|
|
4818
|
+
if (c === "#")
|
|
4819
|
+
return;
|
|
4820
|
+
if (c === '"' || c === "'") {
|
|
4821
|
+
const triple = c.repeat(3);
|
|
4822
|
+
if (line.startsWith(triple, i)) {
|
|
4823
|
+
state.multiline = triple;
|
|
4824
|
+
i += 3;
|
|
4825
|
+
continue;
|
|
4826
|
+
}
|
|
4827
|
+
let j = i + 1;
|
|
4828
|
+
for (; ; ) {
|
|
4829
|
+
if (j >= line.length)
|
|
4830
|
+
throw new Unreadable();
|
|
4831
|
+
if (c === '"' && line[j] === "\\") {
|
|
4832
|
+
j += 2;
|
|
4833
|
+
continue;
|
|
4834
|
+
}
|
|
4835
|
+
if (line[j] === c)
|
|
4836
|
+
break;
|
|
4837
|
+
j += 1;
|
|
4838
|
+
}
|
|
4839
|
+
i = j + 1;
|
|
4840
|
+
continue;
|
|
4841
|
+
}
|
|
4842
|
+
if (c === "[" || c === "{")
|
|
4843
|
+
state.depth += 1;
|
|
4844
|
+
else if (c === "]" || c === "}") {
|
|
4845
|
+
state.depth -= 1;
|
|
4846
|
+
if (state.depth < 0)
|
|
4847
|
+
throw new Unreadable();
|
|
4848
|
+
}
|
|
4849
|
+
i += 1;
|
|
4850
|
+
}
|
|
4851
|
+
}
|
|
4852
|
+
function scanTomlStatements(lines, skip) {
|
|
4853
|
+
const statements = [];
|
|
4854
|
+
const state = { multiline: null, depth: 0 };
|
|
4855
|
+
let table = [];
|
|
4856
|
+
let index = 0;
|
|
4857
|
+
try {
|
|
4858
|
+
for (index = 0; index < lines.length; index += 1) {
|
|
4859
|
+
const inValue = state.multiline !== null || state.depth > 0;
|
|
4860
|
+
if (skip !== void 0 && index === skip.from) {
|
|
4861
|
+
if (inValue)
|
|
4862
|
+
throw new Unreadable();
|
|
4863
|
+
table = skip.tableAfter;
|
|
4864
|
+
index = skip.to;
|
|
4865
|
+
continue;
|
|
4866
|
+
}
|
|
4867
|
+
let line = lines[index];
|
|
4868
|
+
if (index === 0 && line.startsWith("\uFEFF"))
|
|
4869
|
+
line = line.slice(1);
|
|
4870
|
+
if (inValue) {
|
|
4871
|
+
readValue(line, 0, state);
|
|
4872
|
+
continue;
|
|
4873
|
+
}
|
|
4874
|
+
let i = skipBlank(line, 0);
|
|
4875
|
+
if (i >= line.length || line[i] === "#")
|
|
4876
|
+
continue;
|
|
4877
|
+
if (line[i] === "[") {
|
|
4878
|
+
const array = line[i + 1] === "[";
|
|
4879
|
+
i += array ? 2 : 1;
|
|
4880
|
+
const key2 = readKey(line, i);
|
|
4881
|
+
i = key2.end;
|
|
4882
|
+
if (line[i] !== "]" || array && line[i + 1] !== "]")
|
|
4883
|
+
throw new Unreadable();
|
|
4884
|
+
i = skipBlank(line, i + (array ? 2 : 1));
|
|
4885
|
+
if (i < line.length && line[i] !== "#")
|
|
4886
|
+
throw new Unreadable();
|
|
4887
|
+
table = key2.segments;
|
|
4888
|
+
statements.push({ line: index, kind: array ? "array-table" : "table", path: key2.segments });
|
|
4889
|
+
continue;
|
|
4890
|
+
}
|
|
4891
|
+
const key = readKey(line, i);
|
|
4892
|
+
if (line[key.end] !== "=")
|
|
4893
|
+
throw new Unreadable();
|
|
4894
|
+
statements.push({ line: index, kind: "key", path: [...table, ...key.segments] });
|
|
4895
|
+
readValue(line, key.end + 1, state);
|
|
4896
|
+
}
|
|
4897
|
+
if (state.multiline !== null || state.depth > 0) {
|
|
4898
|
+
index = lines.length - 1;
|
|
4899
|
+
throw new Unreadable();
|
|
4900
|
+
}
|
|
4901
|
+
} catch (err) {
|
|
4902
|
+
if (err instanceof Unreadable)
|
|
4903
|
+
return { ok: false, line: index };
|
|
4904
|
+
throw err;
|
|
4905
|
+
}
|
|
4906
|
+
return { ok: true, statements };
|
|
4907
|
+
}
|
|
4908
|
+
|
|
4433
4909
|
// dist/install.js
|
|
4434
4910
|
var ZONE_B_DIR = ".workbench";
|
|
4911
|
+
var CODEX_HOOKS_REL2 = AGENT_PROFILES.codex.settingsFiles[0];
|
|
4912
|
+
var CODEX_CONFIG_REL2 = AGENT_PROFILES.codex.mcpRegistration.file;
|
|
4913
|
+
var CLAUDE_AGENTS_DIR = ".claude/agents";
|
|
4914
|
+
var CODEX_AGENTS_DIR = ".codex/agents";
|
|
4915
|
+
var CREW_CALLSIGN_PATTERN = /^[A-Z][A-Z0-9]*$/;
|
|
4916
|
+
function crewProfileRel(agent, callsign) {
|
|
4917
|
+
if (!CREW_CALLSIGN_PATTERN.test(callsign)) {
|
|
4918
|
+
throw new Error(`[halfcycle] crew callsign "${callsign}" is not a valid profile name (must match ^[A-Z][A-Z0-9]*$).`);
|
|
4919
|
+
}
|
|
4920
|
+
return agent === "claude-code" ? `${CLAUDE_AGENTS_DIR}/${callsign}.md` : `${CODEX_AGENTS_DIR}/${callsign.toLowerCase()}.toml`;
|
|
4921
|
+
}
|
|
4922
|
+
function crewProfileTarget(targetRepo, rel) {
|
|
4923
|
+
const folder = rel.startsWith(`${CLAUDE_AGENTS_DIR}/`) ? CLAUDE_AGENTS_DIR : CODEX_AGENTS_DIR;
|
|
4924
|
+
const target = resolve(join6(targetRepo, rel));
|
|
4925
|
+
if (dirname3(target) !== resolve(targetRepo, folder)) {
|
|
4926
|
+
throw new Error(`[halfcycle] crew profile "${rel}" does not sit directly in ${folder}.`);
|
|
4927
|
+
}
|
|
4928
|
+
return target;
|
|
4929
|
+
}
|
|
4930
|
+
var CREW_ROLE_WORDS = {
|
|
4931
|
+
"business-analyst": "business analyst",
|
|
4932
|
+
architect: "architect",
|
|
4933
|
+
"spec-writer": "spec writer",
|
|
4934
|
+
builder: "builder",
|
|
4935
|
+
"context-docs": "context and docs writer",
|
|
4936
|
+
"spec-reviewer": "spec reviewer",
|
|
4937
|
+
"code-test-reviewer": "code and test reviewer",
|
|
4938
|
+
"delivery-lead": "delivery lead",
|
|
4939
|
+
"quality-lead": "quality lead"
|
|
4940
|
+
};
|
|
4941
|
+
var CREW_TIER_WORDS = {
|
|
4942
|
+
deep: "You think carefully and deeply before you act, and take the time the work needs.",
|
|
4943
|
+
standard: "You think each step through at a steady pace before you act.",
|
|
4944
|
+
fast: "You work quickly on clear, well-defined work, and say so when a task needs more thought."
|
|
4945
|
+
};
|
|
4946
|
+
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.";
|
|
4947
|
+
var CREW_REVIEWER_TOOLS = "Read, Grep, Glob, Bash";
|
|
4948
|
+
var CREW_MEMBER_TOOLS = "Read, Edit, Write, Bash, Grep, Glob";
|
|
4949
|
+
function crewProfileBody(role, group, tier) {
|
|
4950
|
+
const lines = [`You are a ${CREW_ROLE_WORDS[role]} on this project's build crew.`, CREW_TIER_WORDS[tier]];
|
|
4951
|
+
if (group === "reviewer")
|
|
4952
|
+
lines.push(CREW_REVIEWER_WORDS);
|
|
4953
|
+
return `${lines.join("\n")}
|
|
4954
|
+
`;
|
|
4955
|
+
}
|
|
4956
|
+
function crewProfileDescription(member) {
|
|
4957
|
+
return `${member.callsign}, ${CREW_ROLE_WORDS[member.role]} (${member.discipline}), a member of this project's build crew.`;
|
|
4958
|
+
}
|
|
4959
|
+
function plainYamlValue(value) {
|
|
4960
|
+
if (/[\r\n]|: | #|^[-?:,[\]{}#&*!|>'"%@`\s]|\s$/.test(value)) {
|
|
4961
|
+
throw new Error(`[halfcycle] a crew profile value cannot be written as plain text: ${JSON.stringify(value)}`);
|
|
4962
|
+
}
|
|
4963
|
+
return value;
|
|
4964
|
+
}
|
|
4965
|
+
function claudeCrewProfile(member) {
|
|
4966
|
+
const tools = member.group === "reviewer" ? CREW_REVIEWER_TOOLS : CREW_MEMBER_TOOLS;
|
|
4967
|
+
return [
|
|
4968
|
+
"---",
|
|
4969
|
+
`name: ${plainYamlValue(member.callsign)}`,
|
|
4970
|
+
`description: ${plainYamlValue(crewProfileDescription(member))}`,
|
|
4971
|
+
`model: ${plainYamlValue(AGENT_PROFILES["claude-code"].tierModels[member.tier].model)}`,
|
|
4972
|
+
`tools: ${tools}`,
|
|
4973
|
+
"---",
|
|
4974
|
+
"",
|
|
4975
|
+
crewProfileBody(member.role, member.group, member.tier)
|
|
4976
|
+
].join("\n");
|
|
4977
|
+
}
|
|
4978
|
+
function codexCrewProfile(member) {
|
|
4979
|
+
const { model, effort } = AGENT_PROFILES.codex.tierModels[member.tier];
|
|
4980
|
+
if (effort === void 0)
|
|
4981
|
+
throw new Error(`[halfcycle] Codex's ${member.tier} tier names no reasoning effort.`);
|
|
4982
|
+
const body = crewProfileBody(member.role, member.group, member.tier);
|
|
4983
|
+
if (/[\\"]|[\u0000-\u0009\u000b-\u001f\u007f]/.test(body)) {
|
|
4984
|
+
throw new Error("[halfcycle] a crew profile body holds a character a Codex role cannot carry as written.");
|
|
4985
|
+
}
|
|
4986
|
+
return [
|
|
4987
|
+
`name = ${tomlBasicString(member.callsign.toLowerCase())}`,
|
|
4988
|
+
`description = ${tomlBasicString(crewProfileDescription(member))}`,
|
|
4989
|
+
`model = ${tomlBasicString(model)}`,
|
|
4990
|
+
`model_reasoning_effort = ${tomlBasicString(effort)}`,
|
|
4991
|
+
`developer_instructions = """`,
|
|
4992
|
+
`${body}"""`,
|
|
4993
|
+
""
|
|
4994
|
+
].join("\n");
|
|
4995
|
+
}
|
|
4996
|
+
function renderCrewProfiles(roster = CREW_ROSTER) {
|
|
4997
|
+
return [
|
|
4998
|
+
...roster.map((member) => ({ rel: crewProfileRel("claude-code", member.callsign), text: claudeCrewProfile(member) })),
|
|
4999
|
+
...roster.filter((member) => member.group !== "reviewer").map((member) => ({ rel: crewProfileRel("codex", member.callsign), text: codexCrewProfile(member) }))
|
|
5000
|
+
];
|
|
5001
|
+
}
|
|
4435
5002
|
var __filename = fileURLToPath(import.meta.url);
|
|
4436
5003
|
var __dirname = dirname3(__filename);
|
|
4437
5004
|
function findBundleRoot(startDir) {
|
|
@@ -4483,13 +5050,23 @@ var WRITE_ALLOWLIST = [
|
|
|
4483
5050
|
"test/fixtures/captured",
|
|
4484
5051
|
MCP_REGISTRATION_REL,
|
|
4485
5052
|
".gitattributes",
|
|
4486
|
-
ZONE_B_DIR
|
|
5053
|
+
ZONE_B_DIR,
|
|
5054
|
+
// Codex's twin of '.claude/commands' (`codex-install-target` AC4, T-14): one
|
|
5055
|
+
// `.agents/skills/<name>/SKILL.md` per command, same collision rule.
|
|
5056
|
+
".agents/skills",
|
|
5057
|
+
// Codex's checks file, merged beside a developer's own entries.
|
|
5058
|
+
CODEX_HOOKS_REL2,
|
|
5059
|
+
// Codex's settings file: one marked block is added, and the rest of the file is left as it is.
|
|
5060
|
+
CODEX_CONFIG_REL2,
|
|
5061
|
+
// Each crew member's own profile, by its exact name; no other file in those folders is written.
|
|
5062
|
+
...renderCrewProfiles().map((profile) => profile.rel)
|
|
4487
5063
|
];
|
|
5064
|
+
var CREW_PROFILE_RELS = new Set(renderCrewProfiles().map((p) => p.rel));
|
|
4488
5065
|
function isAllowlisted(targetRelPath) {
|
|
4489
5066
|
const normalised = targetRelPath.replace(/\\/g, "/");
|
|
4490
5067
|
return WRITE_ALLOWLIST.some((prefix) => {
|
|
4491
5068
|
const p = prefix.replace(/\\/g, "/");
|
|
4492
|
-
return normalised === p || normalised.startsWith(p + "/");
|
|
5069
|
+
return normalised === p || !CREW_PROFILE_RELS.has(p) && normalised.startsWith(p + "/");
|
|
4493
5070
|
});
|
|
4494
5071
|
}
|
|
4495
5072
|
var CREW_ROSTER_REL = ".halfcycle/crew.json";
|
|
@@ -4502,6 +5079,12 @@ var GITATTRIBUTES_REL = ".gitattributes";
|
|
|
4502
5079
|
function commandStubRel(name) {
|
|
4503
5080
|
return `.claude/commands/${name}.md`;
|
|
4504
5081
|
}
|
|
5082
|
+
function skillRel(name) {
|
|
5083
|
+
if (!/^[a-z][a-z0-9-]*$/.test(name)) {
|
|
5084
|
+
throw new Error(`[halfcycle] plugin.json command "${name}" is not a valid skill name (must match ^[a-z][a-z0-9-]*$).`);
|
|
5085
|
+
}
|
|
5086
|
+
return `.agents/skills/${name}/SKILL.md`;
|
|
5087
|
+
}
|
|
4505
5088
|
var RETIRED_MCP_HEADERS_HELPER_REL = ".halfcycle/mcp-headers.sh";
|
|
4506
5089
|
var RETIRED_WRITE_SET_FILES = [RETIRED_MCP_HEADERS_HELPER_REL];
|
|
4507
5090
|
var RETIRED_DENY_PATTERNS = [
|
|
@@ -4520,6 +5103,9 @@ function closedWriteSet() {
|
|
|
4520
5103
|
const commands = readPluginManifest().commands ?? [];
|
|
4521
5104
|
const files = /* @__PURE__ */ new Set([
|
|
4522
5105
|
...commands.map((cmd) => commandStubRel(cmd.name)),
|
|
5106
|
+
// Codex's twin of the stubs (`codex-install-target` AC4, T-14), derived from the
|
|
5107
|
+
// same manifest array the stubs come from — one closed-set source for both.
|
|
5108
|
+
...commands.map((cmd) => skillRel(cmd.name)),
|
|
4523
5109
|
...Object.keys(OWNED_GENERATED_HEADERS),
|
|
4524
5110
|
VENDORED_BIN_REL,
|
|
4525
5111
|
CREW_ROSTER_REL,
|
|
@@ -4528,9 +5114,12 @@ function closedWriteSet() {
|
|
|
4528
5114
|
BOOTSTRAP_STATE_REL,
|
|
4529
5115
|
CAPTURED_INDEX_REL,
|
|
4530
5116
|
INSTALL_MANIFEST_REL,
|
|
5117
|
+
// The crew profiles (`crew-agent-profiles` AC6), from the same generator the
|
|
5118
|
+
// allowlist and the plan read, so uninstall's set and the writer's cannot differ.
|
|
5119
|
+
...renderCrewProfiles().map((profile) => profile.rel),
|
|
4531
5120
|
...RETIRED_WRITE_SET_FILES
|
|
4532
5121
|
]);
|
|
4533
|
-
const merged = /* @__PURE__ */ new Set([SETTINGS_REL, MCP_REGISTRATION_REL, GITIGNORE_REL, GITATTRIBUTES_REL]);
|
|
5122
|
+
const merged = /* @__PURE__ */ new Set([SETTINGS_REL, MCP_REGISTRATION_REL, CODEX_HOOKS_REL2, CODEX_CONFIG_REL2, GITIGNORE_REL, GITATTRIBUTES_REL]);
|
|
4534
5123
|
const dirs = /* @__PURE__ */ new Set();
|
|
4535
5124
|
for (const path of [...files, ...merged]) {
|
|
4536
5125
|
for (let dir = posix2.dirname(path); dir !== "."; dir = posix2.dirname(dir))
|
|
@@ -4543,6 +5132,7 @@ function closedWriteSet() {
|
|
|
4543
5132
|
dirs,
|
|
4544
5133
|
ownedDirs,
|
|
4545
5134
|
hookCommands: new Set(HALFCYCLE_HOOK_COMMANDS),
|
|
5135
|
+
codexHookCommands: new Set(AGENT_PROFILES.codex.commands),
|
|
4546
5136
|
denyRuleSha256: new Set([...DENY_PATTERNS, ...RETIRED_DENY_PATTERNS].map((rule) => sha256Hex(rule))),
|
|
4547
5137
|
settingsSchema: SETTINGS_SCHEMA,
|
|
4548
5138
|
gitignoreLines: /* @__PURE__ */ new Set([GITIGNORE_HEADER, ...REQUIRED_GITIGNORE_ENTRIES, LEGACY_ENV_LOCAL_REL]),
|
|
@@ -4560,10 +5150,17 @@ function sha256OfWhatThisVersionWrites(commands) {
|
|
|
4560
5150
|
if (existsSync4(src))
|
|
4561
5151
|
out.set(rel, fileSha256(readFileSync5(src)));
|
|
4562
5152
|
};
|
|
4563
|
-
for (const cmd of commands)
|
|
4564
|
-
|
|
5153
|
+
for (const cmd of commands) {
|
|
5154
|
+
const srcFile = join6(BUNDLE_ROOT, ".claude-plugin", cmd.path);
|
|
5155
|
+
fromFile(commandStubRel(cmd.name), srcFile);
|
|
5156
|
+
if (existsSync4(srcFile)) {
|
|
5157
|
+
out.set(skillRel(cmd.name), fileSha256(Buffer.from(codexSkillContent(cmd.name, readFileSync5(srcFile, "utf-8")), "utf-8")));
|
|
5158
|
+
}
|
|
5159
|
+
}
|
|
4565
5160
|
for (const [rel, content] of hookScriptsThisVersionWrites())
|
|
4566
5161
|
out.set(rel, fileSha256(content));
|
|
5162
|
+
for (const profile of renderCrewProfiles())
|
|
5163
|
+
out.set(profile.rel, fileSha256(Buffer.from(profile.text, "utf-8")));
|
|
4567
5164
|
fromFile(CAPTURED_INDEX_REL, capturedIndexScaffold());
|
|
4568
5165
|
return out;
|
|
4569
5166
|
}
|
|
@@ -4667,6 +5264,8 @@ function findNotShared(targetRepo, manifest, collidedPaths) {
|
|
|
4667
5264
|
const merged = [
|
|
4668
5265
|
SETTINGS_REL,
|
|
4669
5266
|
...manifest.mcp !== void 0 ? [MCP_REGISTRATION_REL] : [],
|
|
5267
|
+
...manifest.codexHooks !== void 0 ? [CODEX_HOOKS_REL2] : [],
|
|
5268
|
+
...manifest.codexConfig !== void 0 ? [CODEX_CONFIG_REL2] : [],
|
|
4670
5269
|
...manifest.gitignore.created || manifest.gitignore.appended.length > 0 ? [GITIGNORE_REL] : [],
|
|
4671
5270
|
...manifest.gitattributes !== void 0 && (manifest.gitattributes.created || manifest.gitattributes.appended.length > 0) ? [GITATTRIBUTES_REL] : []
|
|
4672
5271
|
];
|
|
@@ -4727,6 +5326,55 @@ function readPluginManifest() {
|
|
|
4727
5326
|
const raw = readFileSync5(manifestPath, "utf-8");
|
|
4728
5327
|
return JSON.parse(raw);
|
|
4729
5328
|
}
|
|
5329
|
+
function parseCommandStub(raw, name) {
|
|
5330
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/.exec(raw);
|
|
5331
|
+
if (match === null) {
|
|
5332
|
+
throw new Error(`[halfcycle] commands/${name}.md has no frontmatter block; cannot build its Codex skill.`);
|
|
5333
|
+
}
|
|
5334
|
+
const [, frontmatter, body] = match;
|
|
5335
|
+
const desc = /^description:\s*(.+)$/m.exec(frontmatter);
|
|
5336
|
+
if (desc === null) {
|
|
5337
|
+
throw new Error(`[halfcycle] commands/${name}.md's frontmatter has no description; cannot build its Codex skill.`);
|
|
5338
|
+
}
|
|
5339
|
+
return { description: desc[1].trim(), body };
|
|
5340
|
+
}
|
|
5341
|
+
var TRUST_SENTENCE_WORDS = "Claude Code does not trust this folder yet, open it there and accept the workspace-trust prompt";
|
|
5342
|
+
var CODEX_TRUST_SENTENCE = "Codex does not trust this folder yet, open it there and trust it when asked";
|
|
5343
|
+
function trustSentencePattern() {
|
|
5344
|
+
const words = TRUST_SENTENCE_WORDS.split(" ").map((w) => w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
5345
|
+
return new RegExp(words.join("(?: |\\n> )"));
|
|
5346
|
+
}
|
|
5347
|
+
var MCP_RECONNECT_WORDS = "run `/mcp` and reconnect it";
|
|
5348
|
+
var CODEX_MCP_FALLBACK = "start a new Codex session";
|
|
5349
|
+
function mcpReconnectPattern() {
|
|
5350
|
+
const words = MCP_RECONNECT_WORDS.split(" ").map((w) => w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
5351
|
+
return new RegExp(words.join("(?: |\\n> )"));
|
|
5352
|
+
}
|
|
5353
|
+
function codexSkillBody(name, sourceBody) {
|
|
5354
|
+
const slashForm = "`/" + name + "`";
|
|
5355
|
+
if (!sourceBody.includes(slashForm)) {
|
|
5356
|
+
throw new Error(`[halfcycle] commands/${name}.md never names itself as ${slashForm}; cannot build its Codex skill.`);
|
|
5357
|
+
}
|
|
5358
|
+
const pattern = trustSentencePattern();
|
|
5359
|
+
if (!pattern.test(sourceBody)) {
|
|
5360
|
+
throw new Error(`[halfcycle] commands/${name}.md is missing the Claude Code trust sentence; cannot build its Codex skill.`);
|
|
5361
|
+
}
|
|
5362
|
+
const bareName = "`" + name + "`";
|
|
5363
|
+
const transformed = sourceBody.split(slashForm).join(bareName).replace(trustSentencePattern(), CODEX_TRUST_SENTENCE).replace(mcpReconnectPattern(), CODEX_MCP_FALLBACK);
|
|
5364
|
+
if (transformed.includes("/mcp")) {
|
|
5365
|
+
throw new Error(`[halfcycle] commands/${name}.md's Codex skill still names /mcp; cannot build.`);
|
|
5366
|
+
}
|
|
5367
|
+
return transformed;
|
|
5368
|
+
}
|
|
5369
|
+
function codexSkillContent(name, sourceRaw) {
|
|
5370
|
+
const { description, body } = parseCommandStub(sourceRaw, name);
|
|
5371
|
+
const transformed = codexSkillBody(name, body);
|
|
5372
|
+
return `---
|
|
5373
|
+
name: ${name}
|
|
5374
|
+
description: ${description}
|
|
5375
|
+
---
|
|
5376
|
+
${transformed}`;
|
|
5377
|
+
}
|
|
4730
5378
|
function planManifestCommands(manifest, targetRepo, previous, writeSet) {
|
|
4731
5379
|
const recordedFiles = new Map((previous?.files ?? []).filter((file) => writeSet.files.has(file.path)).map((file) => [file.path, file]));
|
|
4732
5380
|
return (manifest.commands ?? []).map((cmd) => {
|
|
@@ -4892,6 +5540,16 @@ function generateSettingsJson() {
|
|
|
4892
5540
|
};
|
|
4893
5541
|
return JSON.stringify(settings, null, 2) + "\n";
|
|
4894
5542
|
}
|
|
5543
|
+
function codexHooksValue() {
|
|
5544
|
+
const hooks = {};
|
|
5545
|
+
for (const { event, matcher, timeout, command, commandWindows } of AGENT_PROFILES.codex.wiring) {
|
|
5546
|
+
(hooks[event] ??= []).push({
|
|
5547
|
+
...matcher === null ? {} : { matcher },
|
|
5548
|
+
hooks: [{ type: "command", command, timeout, ...commandWindows === void 0 ? {} : { commandWindows } }]
|
|
5549
|
+
});
|
|
5550
|
+
}
|
|
5551
|
+
return { hooks };
|
|
5552
|
+
}
|
|
4895
5553
|
var GENERATED_GUARD_RUNNER_HEADER = "# Governance hook wrapper \u2014 generated by";
|
|
4896
5554
|
function generateGuardRunnerWrapper(preFilter = resolvePreFilterShell()) {
|
|
4897
5555
|
return `#!/usr/bin/env bash
|
|
@@ -4944,9 +5602,11 @@ ${preFilter.trimEnd()}
|
|
|
4944
5602
|
# Before a tool runs, on a bash new enough for the cheap pass: read the call
|
|
4945
5603
|
# once; when no risky action can be in it, answer here (the pass writes the
|
|
4946
5604
|
# time line) and do not start Node. Otherwise hand the same input on below.
|
|
5605
|
+
# The pass is given the hook's own arguments, so it knows which coding agent's
|
|
5606
|
+
# input it reads, and hands on any agent's call it has no test for.
|
|
4947
5607
|
if [ "$1" = hook ] && [ "$2" = pre-tool-use ] && [ "\${BASH_VERSINFO[0]:-0}" -ge 5 ]; then
|
|
4948
5608
|
hc_input=$(cat 2>/dev/null) || IFS= read -r -d '' hc_input
|
|
4949
|
-
${PRE_FILTER_FUNCTION} && exit 0
|
|
5609
|
+
${PRE_FILTER_FUNCTION} "$@" && exit 0
|
|
4950
5610
|
node -e "${nodeFloorProgram("")}" || {
|
|
4951
5611
|
printf '%s' "$hc_input" | halfcycle_pre_tool_use_fallback
|
|
4952
5612
|
exit 0
|
|
@@ -5186,7 +5846,7 @@ exit 0
|
|
|
5186
5846
|
var GENERATED_USER_PROMPT_SCOPE_HEADER = "# Typed scope approval \u2014 generated by";
|
|
5187
5847
|
function generateUserPromptScopeHook() {
|
|
5188
5848
|
return `#!/usr/bin/env bash
|
|
5189
|
-
${GENERATED_USER_PROMPT_SCOPE_HEADER} the Halfcycle installer, now also for the close review.
|
|
5849
|
+
${GENERATED_USER_PROMPT_SCOPE_HEADER} the Halfcycle installer, now also for the close review and Codex.
|
|
5190
5850
|
#
|
|
5191
5851
|
# Lets the owner of this project approve a phase's scope, or send it back,
|
|
5192
5852
|
# decide one part of a phase's close review, or turn builds running on their
|
|
@@ -5194,20 +5854,25 @@ ${GENERATED_USER_PROMPT_SCOPE_HEADER} the Halfcycle installer, now also for the
|
|
|
5194
5854
|
# Code session: "approve <phase>", "send back <phase>: <note>", "accept
|
|
5195
5855
|
# <phase>/<part>", "question <phase>/<part>: <words>", "carry <phase>/<part>:
|
|
5196
5856
|
# <words>", "drop <phase>/<part>: <words>", "reject <phase>/<task>: <words>",
|
|
5197
|
-
# "autonomy on" or "autonomy off".
|
|
5857
|
+
# "autonomy on" or "autonomy off". Also lets the person who is typing let one
|
|
5858
|
+
# step Halfcycle paused run once, by typing "allow" as the whole prompt.
|
|
5198
5859
|
#
|
|
5199
5860
|
# AN ORDINARY PROMPT STARTS NOTHING. This script reads the hook input once and
|
|
5200
5861
|
# looks only at the start of the prompt's value. Unless the prompt begins, after
|
|
5201
5862
|
# any whitespace (typed, or escaped in the input), with "approve ", "send back ",
|
|
5202
|
-
# "accept ", "question ", "carry ", "drop ", "reject " or "autonomy ", in any case,
|
|
5203
|
-
#
|
|
5863
|
+
# "accept ", "question ", "carry ", "drop ", "reject " or "autonomy ", in any case,
|
|
5864
|
+
# or is "allow" and nothing else but whitespace, it exits 0 with no output, and
|
|
5865
|
+
# Node never starts. A quote inside any other field arrives
|
|
5204
5866
|
# escaped, so no other field (the folder the session is in, for one) can read as
|
|
5205
5867
|
# the start of the prompt.
|
|
5206
5868
|
#
|
|
5207
5869
|
# Otherwise it finds the project root from its own location, never from the
|
|
5208
5870
|
# folder the session has moved to, and hands the same input to the guard
|
|
5209
5871
|
# wrapper beside it. That decides whether the line counts and prints at most
|
|
5210
|
-
# one
|
|
5872
|
+
# one line. Nothing here ever blocks a prompt. Any arguments this script was
|
|
5873
|
+
# given (a Codex row passes its agent's name) are handed on after the verb; in
|
|
5874
|
+
# Codex a typed line never counts, because Codex cannot tell a person's line
|
|
5875
|
+
# from one a program sent.
|
|
5211
5876
|
#
|
|
5212
5877
|
# FAILS OPEN up to that hand-over: any error before it exits 0 with no output.
|
|
5213
5878
|
# After it, the wrapper's own exit stands.
|
|
@@ -5223,7 +5888,7 @@ IFS= read -r -d '' HC_INPUT || true
|
|
|
5223
5888
|
# \\u escape, or as the UTF-8 bytes of a Unicode space. Compared byte for byte.
|
|
5224
5889
|
HC_LEAD='[[:space:]]|\\\\[nrtf]|\\\\u(000[9a-d]|0020|00a0|1680|200[0-9a]|202[89f]|205f|3000|feff)'
|
|
5225
5890
|
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'
|
|
5226
|
-
if ! printf '%s' "$HC_INPUT" | LC_ALL=C grep -Eiq "\\"prompt\\"[[:space:]]*:[[:space:]]*\\"($HC_LEAD)*(approve|send back|accept|question|carry|drop|reject|autonomy) "; then
|
|
5891
|
+
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
|
|
5227
5892
|
exit 0
|
|
5228
5893
|
fi
|
|
5229
5894
|
|
|
@@ -5233,16 +5898,17 @@ export CLAUDE_PROJECT_DIR="$HC_PROJECT_ROOT"
|
|
|
5233
5898
|
|
|
5234
5899
|
trap - ERR
|
|
5235
5900
|
set +e
|
|
5236
|
-
exec bash "$HC_PROJECT_ROOT/.claude/hooks/guard-runner.sh" hook user-prompt-submit <<<"$HC_INPUT"
|
|
5901
|
+
exec bash "$HC_PROJECT_ROOT/.claude/hooks/guard-runner.sh" hook user-prompt-submit <<<"$HC_INPUT" "$@"
|
|
5237
5902
|
`;
|
|
5238
5903
|
}
|
|
5239
5904
|
var GENERATED_USER_PROMPT_FINDINGS_HEADER = "# Findings hand-over \u2014 generated by";
|
|
5240
5905
|
function generateUserPromptFindingsHook() {
|
|
5241
5906
|
return `#!/usr/bin/env bash
|
|
5242
|
-
${GENERATED_USER_PROMPT_FINDINGS_HEADER} the Halfcycle installer.
|
|
5907
|
+
${GENERATED_USER_PROMPT_FINDINGS_HEADER} the Halfcycle installer, now also for Codex.
|
|
5243
5908
|
#
|
|
5244
5909
|
# At the person's next message, before the agent starts, hands it the details of
|
|
5245
|
-
# whatever Halfcycle's checks found at an earlier turn end, once.
|
|
5910
|
+
# whatever Halfcycle's checks found at an earlier turn end, once. Any arguments
|
|
5911
|
+
# this script was given (a Codex row passes its agent's name) are handed on.
|
|
5246
5912
|
#
|
|
5247
5913
|
# AN ORDINARY PROMPT STARTS NOTHING. With no findings file for this session, this
|
|
5248
5914
|
# script reads its session id with a bash regex (no node) and exits 0 with no
|
|
@@ -5279,7 +5945,7 @@ export CLAUDE_PROJECT_DIR="$HC_PROJECT_ROOT"
|
|
|
5279
5945
|
|
|
5280
5946
|
trap - ERR
|
|
5281
5947
|
set +e
|
|
5282
|
-
exec bash "$HC_PROJECT_ROOT/.claude/hooks/guard-runner.sh" hook user-prompt-findings <<<"$HC_INPUT"
|
|
5948
|
+
exec bash "$HC_PROJECT_ROOT/.claude/hooks/guard-runner.sh" hook user-prompt-findings <<<"$HC_INPUT" "$@"
|
|
5283
5949
|
`;
|
|
5284
5950
|
}
|
|
5285
5951
|
var CI_STANZA_KNOWN_HEADER_HASHES = [
|
|
@@ -5351,6 +6017,148 @@ var MCP_HEADERS_HELPER_COMMAND = `node -e "${nodeFloorProgram([
|
|
|
5351
6017
|
`console.error('halfcycle: no ${VENDORED_BIN_REL} at or above '+process.cwd()+'; run npx halfcycle in the project folder');`,
|
|
5352
6018
|
`process.exit(1)`
|
|
5353
6019
|
].join(""))}"`;
|
|
6020
|
+
var CODEX_ADDRESS_PATTERN = /^https?:\/\/[A-Za-z0-9.-]+(:[0-9]{1,5})?(\/[A-Za-z0-9._-]+)*\/mcp$/;
|
|
6021
|
+
function codexAddressAllowed(address) {
|
|
6022
|
+
return CODEX_ADDRESS_PATTERN.test(address);
|
|
6023
|
+
}
|
|
6024
|
+
var CODEX_NO_CREDENTIAL_STATEMENT = `console.log(JSON.stringify({Authorization:'Bearer ${NO_CREDENTIAL_PLACEHOLDER}'}))`;
|
|
6025
|
+
function codexMcpHeadersHelperCommand(address) {
|
|
6026
|
+
if (!codexAddressAllowed(address))
|
|
6027
|
+
return null;
|
|
6028
|
+
return `node -e "${nodeFloorProgram([
|
|
6029
|
+
`let o=0;`,
|
|
6030
|
+
`const n=function(){if(o)return;o=1;${CODEX_NO_CREDENTIAL_STATEMENT}};`,
|
|
6031
|
+
`try{`,
|
|
6032
|
+
`const p=require('path'),f=require('fs'),c=require('child_process');`,
|
|
6033
|
+
`let d=process.cwd(),b='';`,
|
|
6034
|
+
`for(;;){`,
|
|
6035
|
+
`const t=p.join(d,'${VENDORED_BIN_REL}');`,
|
|
6036
|
+
`if(f.existsSync(t)){b=t;break}`,
|
|
6037
|
+
`const u=p.dirname(d);`,
|
|
6038
|
+
`if(u===d)break;`,
|
|
6039
|
+
`d=u}`,
|
|
6040
|
+
`if(b===''){`,
|
|
6041
|
+
`console.error('halfcycle: no ${VENDORED_BIN_REL} at or above '+process.cwd()+'; run npx halfcycle in the project folder');`,
|
|
6042
|
+
`n()}else{`,
|
|
6043
|
+
`const r=c.spawnSync(process.execPath,[b,'mcp-headers','--url','${address}'],{stdio:['ignore','pipe','inherit']});`,
|
|
6044
|
+
`if(r.status===0&&r.stdout&&r.stdout.length){o=1;process.stdout.write(r.stdout)}else{n()}}`,
|
|
6045
|
+
`}catch(e){n()}`
|
|
6046
|
+
].join(""), CODEX_NO_CREDENTIAL_STATEMENT)}"`;
|
|
6047
|
+
}
|
|
6048
|
+
var CODEX_CONFIG_OPEN_MARKER = "# >>> Halfcycle \u2014 generated by the Halfcycle installer; replaced on every install";
|
|
6049
|
+
var CODEX_CONFIG_CLOSE_MARKER = "# <<< Halfcycle";
|
|
6050
|
+
function tomlBasicString(value) {
|
|
6051
|
+
if (/[\u0000-\u001f\u007f]/.test(value)) {
|
|
6052
|
+
throw new Error("[halfcycle] a value for Codex's settings holds a control character.");
|
|
6053
|
+
}
|
|
6054
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
6055
|
+
}
|
|
6056
|
+
function codexServerPath() {
|
|
6057
|
+
const { serversKey, serverKey } = AGENT_PROFILES.codex.mcpRegistration;
|
|
6058
|
+
return [serversKey, serverKey];
|
|
6059
|
+
}
|
|
6060
|
+
function generateCodexConfigBlock(address) {
|
|
6061
|
+
return codexConfigBlockWith(address, codexMcpHeadersHelperCommand);
|
|
6062
|
+
}
|
|
6063
|
+
function codexConfigBlockWith(address, helperFor) {
|
|
6064
|
+
if (!codexAddressAllowed(address))
|
|
6065
|
+
return null;
|
|
6066
|
+
const helper = helperFor(address);
|
|
6067
|
+
if (helper === null)
|
|
6068
|
+
return null;
|
|
6069
|
+
const [serversKey, serverKey] = codexServerPath();
|
|
6070
|
+
return [
|
|
6071
|
+
CODEX_CONFIG_OPEN_MARKER,
|
|
6072
|
+
`[${serversKey}.${serverKey}]`,
|
|
6073
|
+
`url = ${tomlBasicString(address)}`,
|
|
6074
|
+
`http_headers_helper = ${tomlBasicString(helper)}`,
|
|
6075
|
+
CODEX_CONFIG_CLOSE_MARKER
|
|
6076
|
+
].join("\n");
|
|
6077
|
+
}
|
|
6078
|
+
function locateCodexBlock(lines) {
|
|
6079
|
+
const opens = [];
|
|
6080
|
+
const closes = [];
|
|
6081
|
+
for (const [index, line] of lines.entries()) {
|
|
6082
|
+
const trimmed = line.trim();
|
|
6083
|
+
if (trimmed.startsWith("# >>> Halfcycle")) {
|
|
6084
|
+
if (line !== CODEX_CONFIG_OPEN_MARKER)
|
|
6085
|
+
return { kind: "broken" };
|
|
6086
|
+
opens.push(index);
|
|
6087
|
+
} else if (trimmed.startsWith("# <<< Halfcycle")) {
|
|
6088
|
+
if (line !== CODEX_CONFIG_CLOSE_MARKER)
|
|
6089
|
+
return { kind: "broken" };
|
|
6090
|
+
closes.push(index);
|
|
6091
|
+
}
|
|
6092
|
+
}
|
|
6093
|
+
if (opens.length === 0 && closes.length === 0)
|
|
6094
|
+
return { kind: "none" };
|
|
6095
|
+
if (opens.length === 1 && closes.length === 1 && opens[0] < closes[0]) {
|
|
6096
|
+
return { kind: "one", open: opens[0], close: closes[0] };
|
|
6097
|
+
}
|
|
6098
|
+
return { kind: "broken" };
|
|
6099
|
+
}
|
|
6100
|
+
function definesCodexServer(statement, servers, server) {
|
|
6101
|
+
const [first, second] = statement.path;
|
|
6102
|
+
if (first !== servers)
|
|
6103
|
+
return false;
|
|
6104
|
+
if (statement.kind === "table")
|
|
6105
|
+
return second === server;
|
|
6106
|
+
return statement.path.length === 1 || second === server;
|
|
6107
|
+
}
|
|
6108
|
+
function linesOf(text) {
|
|
6109
|
+
return text.split("\n").map((line) => line.endsWith("\r") ? line.slice(0, -1) : line);
|
|
6110
|
+
}
|
|
6111
|
+
function codexConfigConflict(existing) {
|
|
6112
|
+
const lines = linesOf(existing);
|
|
6113
|
+
const at = locateCodexBlock(lines);
|
|
6114
|
+
if (at.kind === "broken")
|
|
6115
|
+
return "broken-markers";
|
|
6116
|
+
const [servers, server] = codexServerPath();
|
|
6117
|
+
const scan = scanTomlStatements(lines, at.kind === "one" ? { from: at.open, to: at.close, tableAfter: [servers, server] } : void 0);
|
|
6118
|
+
if (!scan.ok)
|
|
6119
|
+
return "unreadable";
|
|
6120
|
+
return scan.statements.some((statement) => definesCodexServer(statement, servers, server)) ? "server-named" : null;
|
|
6121
|
+
}
|
|
6122
|
+
function holdsCodexBlock(existing) {
|
|
6123
|
+
return locateCodexBlock(linesOf(existing)).kind === "one";
|
|
6124
|
+
}
|
|
6125
|
+
function codexBlockSpan(text) {
|
|
6126
|
+
const at = locateCodexBlock(linesOf(text));
|
|
6127
|
+
if (at.kind !== "one")
|
|
6128
|
+
return at.kind;
|
|
6129
|
+
const raw = text.split("\n");
|
|
6130
|
+
let start = 0;
|
|
6131
|
+
for (let i = 0; i < at.open; i += 1)
|
|
6132
|
+
start += raw[i].length + 1;
|
|
6133
|
+
let end = start;
|
|
6134
|
+
for (let i = at.open; i <= at.close; i += 1)
|
|
6135
|
+
end += raw[i].length + 1;
|
|
6136
|
+
return { start, end: Math.min(end, text.length) };
|
|
6137
|
+
}
|
|
6138
|
+
function reconcileCodexConfig(existing, block) {
|
|
6139
|
+
if (existing === null || existing === "") {
|
|
6140
|
+
const text2 = `${block}
|
|
6141
|
+
`;
|
|
6142
|
+
return { kind: "new", text: text2, appended: existing === null ? null : text2, block: text2 };
|
|
6143
|
+
}
|
|
6144
|
+
const conflict = codexConfigConflict(existing);
|
|
6145
|
+
if (conflict !== null)
|
|
6146
|
+
return { kind: "conflict", reason: conflict };
|
|
6147
|
+
const eol = lineEndingOf(existing);
|
|
6148
|
+
const blockText = block.split("\n").join(eol);
|
|
6149
|
+
const span = codexBlockSpan(existing);
|
|
6150
|
+
if (span === "broken")
|
|
6151
|
+
return { kind: "conflict", reason: "broken-markers" };
|
|
6152
|
+
if (span === "none") {
|
|
6153
|
+
const appended = `${existing.endsWith("\n") ? "" : eol}${eol}${blockText}${eol}`;
|
|
6154
|
+
return { kind: "new", text: existing + appended, appended, block: `${blockText}${eol}` };
|
|
6155
|
+
}
|
|
6156
|
+
const old = existing.slice(span.start, span.end);
|
|
6157
|
+
const ending = old.endsWith("\r\n") ? "\r\n" : old.endsWith("\n") ? "\n" : "";
|
|
6158
|
+
const inFile = `${blockText}${ending}`;
|
|
6159
|
+
const text = existing.slice(0, span.start) + inFile + existing.slice(span.end);
|
|
6160
|
+
return text === existing ? { kind: "unchanged", block: inFile } : { kind: "new", text, appended: null, block: inFile };
|
|
6161
|
+
}
|
|
5354
6162
|
var MCP_HEADERS_KNOWN_HEADER_HASHES = [
|
|
5355
6163
|
"37a733ea588b3361b2bbfd86c44e0c95da30d7af6acffd1243ece83c811f0af2",
|
|
5356
6164
|
"6732ec6d2d2bf7cb33017749a3d021b59af653dcd07c3811693b5a242d44b1a1"
|
|
@@ -5738,6 +6546,21 @@ function planCommandStubs(targetRepo, previous, writeSet) {
|
|
|
5738
6546
|
const manifest = readPluginManifest();
|
|
5739
6547
|
return { version: manifest.version, commands: planManifestCommands(manifest, targetRepo, previous, writeSet) };
|
|
5740
6548
|
}
|
|
6549
|
+
function planCodexSkills(targetRepo, previous, writeSet) {
|
|
6550
|
+
const manifest = readPluginManifest();
|
|
6551
|
+
const recordedFiles = new Map((previous?.files ?? []).filter((file) => writeSet.files.has(file.path)).map((file) => [file.path, file]));
|
|
6552
|
+
return (manifest.commands ?? []).map((cmd) => {
|
|
6553
|
+
const srcFile = join6(BUNDLE_ROOT, ".claude-plugin", cmd.path);
|
|
6554
|
+
if (!existsSync4(srcFile)) {
|
|
6555
|
+
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.`);
|
|
6556
|
+
}
|
|
6557
|
+
const rel = skillRel(cmd.name);
|
|
6558
|
+
const content = codexSkillContent(cmd.name, readFileSync5(srcFile, "utf-8"));
|
|
6559
|
+
const recorded = recordedFiles.get(rel);
|
|
6560
|
+
const planned = decideCollisionSafe(join6(targetRepo, rel), targetRepo, content, (current) => bytesAreOurs(rel, current, recorded, writeSet));
|
|
6561
|
+
return { ...planned, editedCollided: planned.outcome === "collided" && recorded !== void 0 };
|
|
6562
|
+
});
|
|
6563
|
+
}
|
|
5741
6564
|
function planVendoredBinary(targetRepo) {
|
|
5742
6565
|
return decideOwned(join6(targetRepo, ".halfcycle", "bin", "bin.bundle.mjs"), targetRepo, readFileSync5(resolveVendoredBinary(), "utf-8"));
|
|
5743
6566
|
}
|
|
@@ -5785,9 +6608,156 @@ function putPlannedMcp(targetRepo, planned) {
|
|
|
5785
6608
|
return;
|
|
5786
6609
|
writeAllowlisted(join6(targetRepo, MCP_REGISTRATION_REL), targetRepo, planned.text, []);
|
|
5787
6610
|
}
|
|
6611
|
+
function fileInFolderPlace(root, rel) {
|
|
6612
|
+
for (let dir = posix2.dirname(rel); dir !== "."; dir = posix2.dirname(dir)) {
|
|
6613
|
+
const found = lstatOrNull(join6(root, dir));
|
|
6614
|
+
if (found !== null && !found.isDirectory())
|
|
6615
|
+
return true;
|
|
6616
|
+
}
|
|
6617
|
+
return false;
|
|
6618
|
+
}
|
|
6619
|
+
function symlinkOnPath(root, rel) {
|
|
6620
|
+
let current = root;
|
|
6621
|
+
for (const segment of rel.split("/")) {
|
|
6622
|
+
current = join6(current, segment);
|
|
6623
|
+
try {
|
|
6624
|
+
if (lstatSync(current).isSymbolicLink())
|
|
6625
|
+
return true;
|
|
6626
|
+
} catch {
|
|
6627
|
+
return false;
|
|
6628
|
+
}
|
|
6629
|
+
}
|
|
6630
|
+
return false;
|
|
6631
|
+
}
|
|
6632
|
+
function planCodexHooks(targetRepo) {
|
|
6633
|
+
const generated = codexHooksValue();
|
|
6634
|
+
const leave = (outcome, before2) => ({
|
|
6635
|
+
outcome,
|
|
6636
|
+
before: before2,
|
|
6637
|
+
existing: null,
|
|
6638
|
+
generated,
|
|
6639
|
+
text: null
|
|
6640
|
+
});
|
|
6641
|
+
if (symlinkOnPath(targetRepo, CODEX_HOOKS_REL2))
|
|
6642
|
+
return leave("link", null);
|
|
6643
|
+
const folder = lstatOrNull(join6(targetRepo, posix2.dirname(CODEX_HOOKS_REL2)));
|
|
6644
|
+
if (folder !== null && !folder.isDirectory())
|
|
6645
|
+
return leave("unreadable", null);
|
|
6646
|
+
const path = join6(targetRepo, CODEX_HOOKS_REL2);
|
|
6647
|
+
let before = null;
|
|
6648
|
+
if (lstatOrNull(path) !== null) {
|
|
6649
|
+
try {
|
|
6650
|
+
before = readFileSync5(path, "utf-8");
|
|
6651
|
+
} catch {
|
|
6652
|
+
return leave("unreadable", null);
|
|
6653
|
+
}
|
|
6654
|
+
}
|
|
6655
|
+
let existing = {};
|
|
6656
|
+
if (before !== null) {
|
|
6657
|
+
let value;
|
|
6658
|
+
try {
|
|
6659
|
+
value = JSON.parse(before);
|
|
6660
|
+
} catch {
|
|
6661
|
+
return leave("unreadable", before);
|
|
6662
|
+
}
|
|
6663
|
+
if (!isPlainObject(value))
|
|
6664
|
+
return leave("unreadable", before);
|
|
6665
|
+
const hooks = value["hooks"];
|
|
6666
|
+
if (hooks !== void 0 && !isPlainObject(hooks))
|
|
6667
|
+
return leave("unreadable", before);
|
|
6668
|
+
const ourEvents = Object.keys(generated.hooks);
|
|
6669
|
+
if (hooks !== void 0 && ourEvents.some((event) => event in hooks && !Array.isArray(hooks[event]))) {
|
|
6670
|
+
return leave("unreadable", before);
|
|
6671
|
+
}
|
|
6672
|
+
existing = value;
|
|
6673
|
+
}
|
|
6674
|
+
const merged = mergeCodexHooks(existing, generated, AGENT_PROFILES.codex.commands);
|
|
6675
|
+
if (before !== null && JSON.stringify(merged) === JSON.stringify(existing)) {
|
|
6676
|
+
return { outcome: "current", before, existing, generated, text: null };
|
|
6677
|
+
}
|
|
6678
|
+
const text = inLineEndingOf(before, JSON.stringify(merged, null, 2) + "\n");
|
|
6679
|
+
return { outcome: "written", before, existing: before === null ? null : existing, generated, text };
|
|
6680
|
+
}
|
|
6681
|
+
function putPlannedCodexHooks(targetRepo, planned) {
|
|
6682
|
+
if (planned.outcome !== "written" || planned.text === null)
|
|
6683
|
+
return;
|
|
6684
|
+
writeAllowlisted(join6(targetRepo, CODEX_HOOKS_REL2), targetRepo, planned.text, []);
|
|
6685
|
+
}
|
|
6686
|
+
function readCodexConfig(targetRepo) {
|
|
6687
|
+
const folder = lstatOrNull(join6(targetRepo, posix2.dirname(CODEX_CONFIG_REL2)));
|
|
6688
|
+
if (folder !== null && !folder.isDirectory())
|
|
6689
|
+
return "unreadable";
|
|
6690
|
+
const path = join6(targetRepo, CODEX_CONFIG_REL2);
|
|
6691
|
+
const stat = lstatOrNull(path);
|
|
6692
|
+
if (stat === null)
|
|
6693
|
+
return { text: null };
|
|
6694
|
+
if (!stat.isFile())
|
|
6695
|
+
return "unreadable";
|
|
6696
|
+
let bytes;
|
|
6697
|
+
try {
|
|
6698
|
+
bytes = readFileSync5(path);
|
|
6699
|
+
} catch {
|
|
6700
|
+
return "unreadable";
|
|
6701
|
+
}
|
|
6702
|
+
const text = bytes.toString("utf-8");
|
|
6703
|
+
return Buffer.from(text, "utf-8").equals(bytes) ? { text } : "unreadable";
|
|
6704
|
+
}
|
|
6705
|
+
function planCodexConfig(targetRepo, credential) {
|
|
6706
|
+
const plan = (outcome, extra = {}) => ({
|
|
6707
|
+
outcome,
|
|
6708
|
+
before: null,
|
|
6709
|
+
text: null,
|
|
6710
|
+
appended: null,
|
|
6711
|
+
inFile: null,
|
|
6712
|
+
conflict: null,
|
|
6713
|
+
blockKept: false,
|
|
6714
|
+
...extra
|
|
6715
|
+
});
|
|
6716
|
+
if (credential === void 0)
|
|
6717
|
+
return plan("no-credential");
|
|
6718
|
+
if (symlinkOnPath(targetRepo, CODEX_CONFIG_REL2))
|
|
6719
|
+
return plan("link");
|
|
6720
|
+
const read = readCodexConfig(targetRepo);
|
|
6721
|
+
if (read === "unreadable")
|
|
6722
|
+
return plan("conflict", { conflict: "unreadable" });
|
|
6723
|
+
const before = read.text;
|
|
6724
|
+
if (credential === "pending") {
|
|
6725
|
+
const conflict = before === null ? null : codexConfigConflict(before);
|
|
6726
|
+
return conflict === null ? plan("pending", { before }) : plan("conflict", { before, conflict });
|
|
6727
|
+
}
|
|
6728
|
+
const block = generateCodexConfigBlock(mcpEndpointUrl(credential.mcpUrl));
|
|
6729
|
+
if (block === null)
|
|
6730
|
+
return plan("refused-address", { before, blockKept: before !== null && holdsCodexBlock(before) });
|
|
6731
|
+
const reconciled = reconcileCodexConfig(before, block);
|
|
6732
|
+
switch (reconciled.kind) {
|
|
6733
|
+
case "conflict":
|
|
6734
|
+
return plan("conflict", { before, conflict: reconciled.reason });
|
|
6735
|
+
case "unchanged":
|
|
6736
|
+
return plan("current", { before, inFile: reconciled.block });
|
|
6737
|
+
case "new":
|
|
6738
|
+
return plan("written", { before, text: reconciled.text, appended: reconciled.appended, inFile: reconciled.block });
|
|
6739
|
+
}
|
|
6740
|
+
}
|
|
6741
|
+
function putPlannedCodexConfig(targetRepo, planned) {
|
|
6742
|
+
if (planned.outcome !== "written" || planned.text === null)
|
|
6743
|
+
return;
|
|
6744
|
+
writeAllowlisted(join6(targetRepo, CODEX_CONFIG_REL2), targetRepo, planned.text, []);
|
|
6745
|
+
}
|
|
5788
6746
|
function planCrewRoster(targetRepo) {
|
|
5789
6747
|
return decideOwned(join6(targetRepo, CREW_ROSTER_REL), targetRepo, renderCrewRoster());
|
|
5790
6748
|
}
|
|
6749
|
+
function planCrewProfiles(targetRepo, previous, writeSet) {
|
|
6750
|
+
const recordedFiles = new Map((previous?.files ?? []).filter((file) => writeSet.files.has(file.path)).map((file) => [file.path, file]));
|
|
6751
|
+
return renderCrewProfiles().map(({ rel, text }) => {
|
|
6752
|
+
const target = crewProfileTarget(targetRepo, rel);
|
|
6753
|
+
if (symlinkOnPath(targetRepo, rel) || fileInFolderPlace(targetRepo, rel)) {
|
|
6754
|
+
return { rel, text, outcome: "collided", replacedExisting: false, editedCollided: false };
|
|
6755
|
+
}
|
|
6756
|
+
const recorded = recordedFiles.get(rel);
|
|
6757
|
+
const planned = decideCollisionSafe(target, targetRepo, text, (current) => bytesAreOurs(rel, current, recorded, writeSet));
|
|
6758
|
+
return { ...planned, editedCollided: planned.outcome === "collided" && recorded !== void 0 };
|
|
6759
|
+
});
|
|
6760
|
+
}
|
|
5791
6761
|
function planInstallRecord(targetRepo, text) {
|
|
5792
6762
|
return decideOwned(join6(targetRepo, INSTALL_MANIFEST_REL), targetRepo, text);
|
|
5793
6763
|
}
|
|
@@ -5811,6 +6781,12 @@ function accountInstall(steps, actual) {
|
|
|
5811
6781
|
report.editedCollidedPaths.push(command.rel);
|
|
5812
6782
|
noteFile(draft, command.rel, command.outcome, actual.bytesAfter);
|
|
5813
6783
|
}
|
|
6784
|
+
for (const skill of steps.codexSkills) {
|
|
6785
|
+
recordOwnedGenerated(report, skill);
|
|
6786
|
+
if (skill.editedCollided)
|
|
6787
|
+
report.editedCollidedPaths.push(skill.rel);
|
|
6788
|
+
noteFile(draft, skill.rel, skill.outcome, actual.bytesAfter);
|
|
6789
|
+
}
|
|
5814
6790
|
noteFile(draft, VENDORED_BIN_REL, recordOwnedGenerated(report, steps.vendored), actual.bytesAfter);
|
|
5815
6791
|
const settings = steps.settings;
|
|
5816
6792
|
recordSettingsMerge(draft, settings.existing, settings.generated, settings.preexisted, settings.merged);
|
|
@@ -5868,6 +6844,8 @@ function accountInstall(steps, actual) {
|
|
|
5868
6844
|
if (mcp.existing !== null)
|
|
5869
6845
|
recordMcpAdoptionOnly(draft, mcp.existing);
|
|
5870
6846
|
}
|
|
6847
|
+
recordCodexHooksStep(report, draft, steps.codexHooks);
|
|
6848
|
+
recordCodexConfigStep(report, draft, steps.codexConfig);
|
|
5871
6849
|
recordGitignoreReconcile(draft, steps.gitignore.before, actual.gitignoreAfter, writeSet);
|
|
5872
6850
|
if (actual.gitignore === "failed") {
|
|
5873
6851
|
report.skippedPaths.push(".gitignore (write failed \u2014 see credential refusal)");
|
|
@@ -5910,6 +6888,12 @@ function accountInstall(steps, actual) {
|
|
|
5910
6888
|
report.writtenPaths.push(BUNDLE_PIN_REL);
|
|
5911
6889
|
noteFile(draft, BUNDLE_PIN_REL, "written", actual.bytesAfter);
|
|
5912
6890
|
noteFile(draft, CREW_ROSTER_REL, recordOwnedGenerated(report, steps.crew), actual.bytesAfter);
|
|
6891
|
+
for (const profile of steps.crewProfiles) {
|
|
6892
|
+
recordOwnedGenerated(report, profile);
|
|
6893
|
+
if (profile.editedCollided)
|
|
6894
|
+
report.editedCollidedPaths.push(profile.rel);
|
|
6895
|
+
noteFile(draft, profile.rel, profile.outcome, actual.bytesAfter);
|
|
6896
|
+
}
|
|
5913
6897
|
if (actual.stateExistsAfter)
|
|
5914
6898
|
recordPerMachineFile(draft, BOOTSTRAP_STATE_REL);
|
|
5915
6899
|
for (const dir of writeSet.dirs) {
|
|
@@ -5920,9 +6904,78 @@ function accountInstall(steps, actual) {
|
|
|
5920
6904
|
assertManifestInWriteSet(installManifest, writeSet);
|
|
5921
6905
|
return { report, installManifest, manifestText: serializeManifest(installManifest) };
|
|
5922
6906
|
}
|
|
6907
|
+
function recordCodexHooksStep(report, draft, planned) {
|
|
6908
|
+
switch (planned.outcome) {
|
|
6909
|
+
case "unreadable":
|
|
6910
|
+
report.skippedPaths.push(`${CODEX_HOOKS_REL2} (left as it is \u2014 it could not be read, so Codex's checks were not added)`);
|
|
6911
|
+
return;
|
|
6912
|
+
case "link":
|
|
6913
|
+
report.skippedPaths.push(`${CODEX_HOOKS_REL2} (left as it is \u2014 it is a link Halfcycle does not follow)`);
|
|
6914
|
+
return;
|
|
6915
|
+
case "written":
|
|
6916
|
+
(planned.before === null ? report.writtenPaths : report.mergedPaths).push(CODEX_HOOKS_REL2);
|
|
6917
|
+
break;
|
|
6918
|
+
case "current":
|
|
6919
|
+
report.skippedPaths.push(CODEX_HOOKS_REL2);
|
|
6920
|
+
break;
|
|
6921
|
+
}
|
|
6922
|
+
const existingHooks = planned.existing !== null && isPlainObject(planned.existing["hooks"]) ? planned.existing["hooks"] : void 0;
|
|
6923
|
+
const ours = AGENT_PROFILES.codex.commands;
|
|
6924
|
+
const heldOurs = Object.values(existingHooks ?? {}).some((groups) => Array.isArray(groups) && groups.some((group) => isPlainObject(group) && Array.isArray(group["hooks"]) && isHalfcycleEntry({ hooks: group["hooks"] }, ours)));
|
|
6925
|
+
recordCodexHooks(draft, {
|
|
6926
|
+
created: planned.before === null,
|
|
6927
|
+
adopted: draft.previous === null && heldOurs,
|
|
6928
|
+
hookCommands: Object.values(planned.generated.hooks).flatMap((groups) => groups.flatMap((group) => group.hooks.map((h) => h.command))),
|
|
6929
|
+
eventsCreated: Object.keys(planned.generated.hooks).filter((event) => !(existingHooks && event in existingHooks)),
|
|
6930
|
+
hooksCreated: planned.existing === null || planned.existing["hooks"] === void 0
|
|
6931
|
+
});
|
|
6932
|
+
}
|
|
6933
|
+
function recordCodexConfigStep(report, draft, planned) {
|
|
6934
|
+
const prev = draft.codexConfig;
|
|
6935
|
+
const separatorOf = (block) => {
|
|
6936
|
+
const at = block.indexOf(CODEX_CONFIG_OPEN_MARKER);
|
|
6937
|
+
return at > 0 ? block.slice(0, at) : "";
|
|
6938
|
+
};
|
|
6939
|
+
switch (planned.outcome) {
|
|
6940
|
+
case "written": {
|
|
6941
|
+
(planned.before === null ? report.writtenPaths : report.mergedPaths).push(CODEX_CONFIG_REL2);
|
|
6942
|
+
const block = planned.appended !== null ? planned.appended : planned.before === null ? planned.text : `${prev === void 0 ? "" : separatorOf(prev.block)}${planned.inFile}`;
|
|
6943
|
+
recordCodexConfig(draft, { created: planned.before === null, block });
|
|
6944
|
+
return;
|
|
6945
|
+
}
|
|
6946
|
+
case "current":
|
|
6947
|
+
report.skippedPaths.push(CODEX_CONFIG_REL2);
|
|
6948
|
+
if (prev === void 0)
|
|
6949
|
+
recordCodexConfig(draft, { created: false, block: planned.inFile });
|
|
6950
|
+
return;
|
|
6951
|
+
case "no-credential":
|
|
6952
|
+
report.skippedPaths.push(`${CODEX_CONFIG_REL2} (no MCP origin \u2014 no credential supplied)`);
|
|
6953
|
+
return;
|
|
6954
|
+
case "link":
|
|
6955
|
+
report.skippedPaths.push(`${CODEX_CONFIG_REL2} (left as it is \u2014 it is a link Halfcycle does not follow)`);
|
|
6956
|
+
return;
|
|
6957
|
+
case "conflict":
|
|
6958
|
+
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)`);
|
|
6959
|
+
return;
|
|
6960
|
+
case "refused-address":
|
|
6961
|
+
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)`);
|
|
6962
|
+
return;
|
|
6963
|
+
case "pending":
|
|
6964
|
+
throw new Error("[halfcycle] a plan still waiting for the service cannot be applied or recorded.");
|
|
6965
|
+
}
|
|
6966
|
+
}
|
|
5923
6967
|
function recordInstallRecord(report, planned) {
|
|
5924
6968
|
recordOwnedGenerated(report, planned);
|
|
5925
6969
|
}
|
|
6970
|
+
var SELF_BUILD_GITIGNORE_REL = ".halfcycle/.gitignore";
|
|
6971
|
+
var SELF_BUILD_WRITE_SET = [
|
|
6972
|
+
BUNDLE_PIN_REL,
|
|
6973
|
+
VENDORED_BIN_REL,
|
|
6974
|
+
SELF_BUILD_GITIGNORE_REL,
|
|
6975
|
+
CREW_ROSTER_REL,
|
|
6976
|
+
MCP_REGISTRATION_REL,
|
|
6977
|
+
...renderCrewProfiles().map((profile) => profile.rel)
|
|
6978
|
+
];
|
|
5926
6979
|
|
|
5927
6980
|
// dist/control-origin.js
|
|
5928
6981
|
var DEFAULT_CONTROL_ORIGIN = "https://control.halfcycle.ai";
|