halfcycle 0.11.1 → 0.12.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +1 -1
- package/bin/bin.bundle.mjs +964 -351
- package/bin/pre-tool-use-filter.sh +4 -0
- package/commands/agent-teams-orchestration.md +1 -1
- package/commands/consistency-check.md +1 -1
- package/commands/context-update.md +1 -1
- package/commands/halfcycle-design.md +1 -1
- package/commands/halfcycle-setup.md +1 -1
- package/commands/stack-assembly-worker.md +1 -1
- package/commands/task-execution.md +1 -1
- package/commands/task-graph.md +1 -1
- package/commands/write-spec.md +1 -1
- package/dist/banner-facts.d.ts +7 -2
- package/dist/banner-facts.d.ts.map +1 -1
- package/dist/build-record/obtain.d.ts.map +1 -1
- package/dist/change.d.ts.map +1 -1
- package/dist/check-criteria.d.ts +1 -1
- package/dist/check-criteria.d.ts.map +1 -1
- package/dist/check-report.d.ts +1 -1
- package/dist/check-report.d.ts.map +1 -1
- package/dist/check-tasks.d.ts +10 -1
- package/dist/check-tasks.d.ts.map +1 -1
- package/dist/cli-contract.d.ts +1 -1
- package/dist/cli-contract.d.ts.map +1 -1
- package/dist/cli.js +1412 -873
- package/dist/cli.js.map +3 -3
- package/dist/close-phase.d.ts +5 -3
- package/dist/close-phase.d.ts.map +1 -1
- package/dist/confirm-identity.d.ts +3 -0
- package/dist/confirm-identity.d.ts.map +1 -1
- package/dist/crew.d.ts +37 -0
- package/dist/crew.d.ts.map +1 -0
- package/dist/draw/index.d.ts +1 -1
- package/dist/draw/index.d.ts.map +1 -1
- package/dist/engagement-credential.d.ts +4 -0
- package/dist/engagement-credential.d.ts.map +1 -1
- package/dist/failure-line.d.ts +3 -2
- package/dist/failure-line.d.ts.map +1 -1
- package/dist/folder-refusals.d.ts.map +1 -1
- package/dist/index.js +330 -224
- package/dist/index.js.map +3 -3
- package/dist/install-manifest.d.ts +5 -1
- package/dist/install-manifest.d.ts.map +1 -1
- package/dist/install-plan.d.ts.map +1 -1
- package/dist/install.d.ts +45 -11
- package/dist/install.d.ts.map +1 -1
- package/dist/local-allow.d.ts +72 -3
- package/dist/local-allow.d.ts.map +1 -1
- package/dist/loopback-signin.d.ts +6 -5
- package/dist/loopback-signin.d.ts.map +1 -1
- package/dist/merge-settings.d.ts +16 -1
- package/dist/merge-settings.d.ts.map +1 -1
- package/dist/open-phase.d.ts +1 -1
- package/dist/open-phase.d.ts.map +1 -1
- package/dist/own-version.d.ts +42 -0
- package/dist/own-version.d.ts.map +1 -0
- package/dist/pace.d.ts +2 -2
- package/dist/pace.d.ts.map +1 -1
- package/dist/plan.d.ts +4 -3
- package/dist/plan.d.ts.map +1 -1
- package/dist/pre-create-checks.d.ts.map +1 -1
- package/dist/preview.d.ts +7 -2
- package/dist/preview.d.ts.map +1 -1
- package/dist/ready.d.ts +1 -1
- package/dist/ready.d.ts.map +1 -1
- package/dist/regression.d.ts +48 -0
- package/dist/regression.d.ts.map +1 -0
- package/dist/request-shapes-verb.d.ts +1 -1
- package/dist/request-shapes-verb.d.ts.map +1 -1
- package/dist/restore.d.ts +1 -1
- package/dist/restore.d.ts.map +1 -1
- package/dist/review.d.ts +2 -2
- package/dist/review.d.ts.map +1 -1
- package/dist/route.d.ts +1 -1
- package/dist/route.d.ts.map +1 -1
- package/dist/size.d.ts.map +1 -1
- package/dist/uninstall.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
// dist/install.js
|
|
2
2
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
3
3
|
import { createHash as createHash2 } from "node:crypto";
|
|
4
|
-
import { existsSync as
|
|
4
|
+
import { existsSync as existsSync5, lstatSync, mkdirSync as mkdirSync4, readFileSync as readFileSync6, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "node:fs";
|
|
5
5
|
import { homedir as homedir2 } from "node:os";
|
|
6
|
-
import { dirname as
|
|
7
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { dirname as dirname4, isAbsolute as isAbsolute2, join as join7, posix as posix2, relative, resolve } from "node:path";
|
|
8
7
|
|
|
9
8
|
// ../core/dist/guard-record.js
|
|
10
9
|
import { z } from "zod";
|
|
@@ -268,14 +267,40 @@ function looksLikeSecret(text) {
|
|
|
268
267
|
var MCP_REGISTRATION_REL = ".mcp.json";
|
|
269
268
|
var MCP_SERVER_KEY = "halfcycle";
|
|
270
269
|
|
|
270
|
+
// ../core/dist/release-version.js
|
|
271
|
+
var RELEASE_VERSION_PATTERN = /^\d+\.\d+\.\d+$/;
|
|
272
|
+
|
|
273
|
+
// ../core/dist/release-tag.js
|
|
274
|
+
var PRE_RELEASE_VERSION_PATTERN = /^(\d+\.\d+\.\d+)-rc\.\d+$/;
|
|
275
|
+
var PACKAGE_COMMAND = "npx halfcycle";
|
|
276
|
+
var COMMAND_FORMS = /npx halfcycle(?:@latest|(?![@\p{L}\p{N}-]))/gu;
|
|
277
|
+
function isPreReleaseVersion(value) {
|
|
278
|
+
return typeof value === "string" && PRE_RELEASE_VERSION_PATTERN.test(value);
|
|
279
|
+
}
|
|
280
|
+
function releaseTagOfVersion(version) {
|
|
281
|
+
return isPreReleaseVersion(version) ? "next" : "latest";
|
|
282
|
+
}
|
|
283
|
+
function halfcycleCommand(version) {
|
|
284
|
+
return `${PACKAGE_COMMAND}@${releaseTagOfVersion(version)}`;
|
|
285
|
+
}
|
|
286
|
+
function bareHalfcycleCommand(version) {
|
|
287
|
+
return releaseTagOfVersion(version) === "next" ? `${PACKAGE_COMMAND}@next` : PACKAGE_COMMAND;
|
|
288
|
+
}
|
|
289
|
+
function rewriteHalfcycleCommands(text, tag) {
|
|
290
|
+
if (tag === "latest")
|
|
291
|
+
return text;
|
|
292
|
+
return text.replace(COMMAND_FORMS, `${PACKAGE_COMMAND}@${tag}`);
|
|
293
|
+
}
|
|
294
|
+
|
|
271
295
|
// ../core/dist/shell-quote.js
|
|
272
296
|
function quoteForShell(value) {
|
|
273
297
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
274
298
|
}
|
|
275
|
-
var HALFCYCLE_COMMAND =
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
299
|
+
var HALFCYCLE_COMMAND = halfcycleCommand();
|
|
300
|
+
function doctorLine(version) {
|
|
301
|
+
return `Run \`${bareHalfcycleCommand(version)} doctor\` to check set up, sign-in, connection, checks and version.`;
|
|
302
|
+
}
|
|
303
|
+
var DOCTOR_LINE = doctorLine();
|
|
279
304
|
|
|
280
305
|
// ../core/dist/github-address.js
|
|
281
306
|
var GITHUB_REPOSITORY_ADDRESS = /^https:\/\/github\.com\/[A-Za-z0-9][A-Za-z0-9-]{0,38}\/(?!\.{1,2}$)[A-Za-z0-9._-]{1,100}$/;
|
|
@@ -293,8 +318,14 @@ var REMOTE_FORMS = [
|
|
|
293
318
|
];
|
|
294
319
|
|
|
295
320
|
// ../core/dist/sign-in.js
|
|
296
|
-
|
|
297
|
-
|
|
321
|
+
function signInCommand(version) {
|
|
322
|
+
return `"${bareHalfcycleCommand(version)}"`;
|
|
323
|
+
}
|
|
324
|
+
var SIGN_IN_COMMAND = signInCommand();
|
|
325
|
+
function hookRemedy(version) {
|
|
326
|
+
return `Run ${signInCommand(version)} in the project folder to sign in.`;
|
|
327
|
+
}
|
|
328
|
+
var HOOK_REMEDY = hookRemedy();
|
|
298
329
|
|
|
299
330
|
// ../core/dist/task-plan.js
|
|
300
331
|
import { z as z2 } from "zod";
|
|
@@ -408,7 +439,7 @@ function codexHookCommand(script, args) {
|
|
|
408
439
|
const invocation = `cd "$(git rev-parse --show-toplevel)" && bash ./${script}`;
|
|
409
440
|
return args === void 0 ? invocation : `${invocation} ${args}`;
|
|
410
441
|
}
|
|
411
|
-
var CODEX_WINDOWS_NOT_SET_UP =
|
|
442
|
+
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 ${bareHalfcycleCommand()} in the project folder.`;
|
|
412
443
|
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.";
|
|
413
444
|
function codexHookCommandWindows(script, args) {
|
|
414
445
|
const tick = (text) => "`" + text + "`";
|
|
@@ -3111,8 +3142,22 @@ var deviceConfirmResponseSchema = z19.union([
|
|
|
3111
3142
|
var DASHBOARD_URL = "https://app.halfcycle.ai";
|
|
3112
3143
|
|
|
3113
3144
|
// ../events/dist/project-page.js
|
|
3114
|
-
|
|
3115
|
-
|
|
3145
|
+
var SERVICE_HOST = /^(?:control|delivery)(-[a-z0-9]+)?\.halfcycle\.ai$/;
|
|
3146
|
+
function consoleOrigin(serviceAddress) {
|
|
3147
|
+
const trimmed = serviceAddress?.trim();
|
|
3148
|
+
if (!trimmed)
|
|
3149
|
+
return DASHBOARD_URL;
|
|
3150
|
+
let host;
|
|
3151
|
+
try {
|
|
3152
|
+
host = new URL(trimmed.includes("://") ? trimmed : `https://${trimmed}`).hostname.toLowerCase();
|
|
3153
|
+
} catch {
|
|
3154
|
+
return DASHBOARD_URL;
|
|
3155
|
+
}
|
|
3156
|
+
const suffix = SERVICE_HOST.exec(host)?.[1];
|
|
3157
|
+
return suffix === void 0 ? DASHBOARD_URL : `https://app${suffix}.halfcycle.ai`;
|
|
3158
|
+
}
|
|
3159
|
+
function projectPageUrl(engagementId, serviceAddress) {
|
|
3160
|
+
return isEngagementId(engagementId) ? `${consoleOrigin(serviceAddress)}/${encodeURIComponent(engagementId)}` : void 0;
|
|
3116
3161
|
}
|
|
3117
3162
|
|
|
3118
3163
|
// ../events/dist/credential-wire.js
|
|
@@ -3135,8 +3180,14 @@ var NO_CREDENTIAL_PLACEHOLDER = "halfcycle:no-credential";
|
|
|
3135
3180
|
var NOT_THIS_ACCOUNT_PLACEHOLDER = "halfcycle:not-this-account";
|
|
3136
3181
|
var UPDATE_CLAUDE_CODE_PLACEHOLDER = "halfcycle:update-claude-code";
|
|
3137
3182
|
var SWITCH_ACCOUNT_ANSWER = 'answer "s" when asked';
|
|
3138
|
-
|
|
3139
|
-
|
|
3183
|
+
function noCredentialSentence(version) {
|
|
3184
|
+
return `Halfcycle is not connected: this machine has no working Halfcycle credential for this project. Run "${bareHalfcycleCommand(version)}" in the project folder to sign in again.`;
|
|
3185
|
+
}
|
|
3186
|
+
var NO_CREDENTIAL_SENTENCE = noCredentialSentence();
|
|
3187
|
+
function notThisAccountSentence(version) {
|
|
3188
|
+
return `Halfcycle is not connected: this project belongs to another Halfcycle account. To switch, run "${bareHalfcycleCommand(version)}" in this project and ${SWITCH_ACCOUNT_ANSWER}.`;
|
|
3189
|
+
}
|
|
3190
|
+
var NOT_THIS_ACCOUNT_SENTENCE = notThisAccountSentence();
|
|
3140
3191
|
var UPDATE_CLAUDE_CODE_SENTENCE = "Halfcycle is not connected: this Claude Code version does not name the server it connects to, so no credential was sent. Update Claude Code, then reopen.";
|
|
3141
3192
|
var NO_NODE_SENTENCE = "Halfcycle is not connected: Node is not installed, or not on the path this session uses, so nothing here is checked. Install Node, then reopen.";
|
|
3142
3193
|
var PLACEHOLDER_SENTENCES = Object.freeze({
|
|
@@ -3572,9 +3623,9 @@ var phaseModelsFileSchema = z26.object({
|
|
|
3572
3623
|
}).strict();
|
|
3573
3624
|
|
|
3574
3625
|
// dist/engagement-credential.js
|
|
3575
|
-
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3626
|
+
import { chmodSync, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
|
|
3576
3627
|
import { platform as platform2 } from "node:os";
|
|
3577
|
-
import { join as
|
|
3628
|
+
import { join as join3 } from "node:path";
|
|
3578
3629
|
|
|
3579
3630
|
// dist/account-credential.js
|
|
3580
3631
|
import { homedir, platform } from "node:os";
|
|
@@ -3583,6 +3634,46 @@ function halfcycleHome(home) {
|
|
|
3583
3634
|
return join(home ?? homedir(), HALFCYCLE_DIR_NAME);
|
|
3584
3635
|
}
|
|
3585
3636
|
|
|
3637
|
+
// dist/own-version.js
|
|
3638
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3639
|
+
import { dirname as dirname2, join as join2 } from "node:path";
|
|
3640
|
+
import { fileURLToPath } from "node:url";
|
|
3641
|
+
var moduleDir = dirname2(fileURLToPath(import.meta.url));
|
|
3642
|
+
function findBundleRoot(startDir) {
|
|
3643
|
+
let candidate = startDir;
|
|
3644
|
+
for (let i = 0; i < 4; i++) {
|
|
3645
|
+
const manifest = join2(candidate, ".claude-plugin", "plugin.json");
|
|
3646
|
+
try {
|
|
3647
|
+
if (existsSync(manifest))
|
|
3648
|
+
return candidate;
|
|
3649
|
+
} catch {
|
|
3650
|
+
}
|
|
3651
|
+
candidate = dirname2(candidate);
|
|
3652
|
+
}
|
|
3653
|
+
return join2(startDir, "..");
|
|
3654
|
+
}
|
|
3655
|
+
var BUNDLE_ROOT = findBundleRoot(moduleDir);
|
|
3656
|
+
function bundleVersion() {
|
|
3657
|
+
try {
|
|
3658
|
+
const pkg = JSON.parse(readFileSync(join2(BUNDLE_ROOT, "package.json"), "utf-8"));
|
|
3659
|
+
return typeof pkg.version === "string" ? pkg.version : void 0;
|
|
3660
|
+
} catch {
|
|
3661
|
+
return void 0;
|
|
3662
|
+
}
|
|
3663
|
+
}
|
|
3664
|
+
var cachedVersion;
|
|
3665
|
+
function ownVersion() {
|
|
3666
|
+
if (cachedVersion === void 0)
|
|
3667
|
+
cachedVersion = { value: bundleVersion() };
|
|
3668
|
+
return cachedVersion.value;
|
|
3669
|
+
}
|
|
3670
|
+
function ownCommand() {
|
|
3671
|
+
return halfcycleCommand(ownVersion());
|
|
3672
|
+
}
|
|
3673
|
+
function ownBareCommand() {
|
|
3674
|
+
return bareHalfcycleCommand(ownVersion());
|
|
3675
|
+
}
|
|
3676
|
+
|
|
3586
3677
|
// dist/engagement-credential.js
|
|
3587
3678
|
var ENGAGEMENTS_DIR = ENGAGEMENTS_DIR_NAME;
|
|
3588
3679
|
var ENV_FILENAME = ENGAGEMENT_ENV_FILENAME;
|
|
@@ -3590,7 +3681,7 @@ var HALFCYCLE_DIR = HALFCYCLE_DIR_NAME;
|
|
|
3590
3681
|
var InvalidEngagementIdError = class extends Error {
|
|
3591
3682
|
constructor(engagementId) {
|
|
3592
3683
|
const shown = JSON.stringify(engagementId.length > 80 ? `${engagementId.slice(0, 80)}\u2026` : engagementId);
|
|
3593
|
-
super(`${shown} is not a Halfcycle project id, so it cannot name a folder on this machine and nothing was written. A project id looks like 3f2a1c4e-8b7d-4e2f-9a61-0c5d7e8f9b10. If it is the "${PIN_ENGAGEMENT_ID_FIELD}" in .halfcycle/bundle.json, delete that file and run "
|
|
3684
|
+
super(`${shown} is not a Halfcycle project id, so it cannot name a folder on this machine and nothing was written. A project id looks like 3f2a1c4e-8b7d-4e2f-9a61-0c5d7e8f9b10. If it is the "${PIN_ENGAGEMENT_ID_FIELD}" in .halfcycle/bundle.json, delete that file and run "${ownBareCommand()}" again: this repository is then set up as a new Halfcycle project.`);
|
|
3594
3685
|
this.name = "InvalidEngagementIdError";
|
|
3595
3686
|
}
|
|
3596
3687
|
};
|
|
@@ -3600,10 +3691,10 @@ function assertEngagementId(engagementId) {
|
|
|
3600
3691
|
}
|
|
3601
3692
|
function engagementStateDir(engagementId, home) {
|
|
3602
3693
|
assertEngagementId(engagementId);
|
|
3603
|
-
return
|
|
3694
|
+
return join3(halfcycleHome(home), ENGAGEMENTS_DIR, engagementId);
|
|
3604
3695
|
}
|
|
3605
3696
|
function engagementEnvPath(engagementId, home) {
|
|
3606
|
-
return
|
|
3697
|
+
return join3(engagementStateDir(engagementId, home), ENV_FILENAME);
|
|
3607
3698
|
}
|
|
3608
3699
|
var GUARD_ENV_KEYS = [
|
|
3609
3700
|
"GUARD_SERVICE_URL",
|
|
@@ -3625,7 +3716,7 @@ function reconcileEnvText2(existing, values, keys = ENGAGEMENT_ENV_KEYS) {
|
|
|
3625
3716
|
}
|
|
3626
3717
|
function readEngagementEnv(engagementId, home) {
|
|
3627
3718
|
try {
|
|
3628
|
-
return parseEnvText(
|
|
3719
|
+
return parseEnvText(readFileSync2(engagementEnvPath(engagementId, home), "utf-8"));
|
|
3629
3720
|
} catch {
|
|
3630
3721
|
return null;
|
|
3631
3722
|
}
|
|
@@ -3635,7 +3726,7 @@ function writeEngagementEnv(engagementId, values, home, keys = ENGAGEMENT_ENV_KE
|
|
|
3635
3726
|
const dir = engagementStateDir(engagementId, home);
|
|
3636
3727
|
let existing;
|
|
3637
3728
|
try {
|
|
3638
|
-
existing =
|
|
3729
|
+
existing = readFileSync2(path, "utf-8");
|
|
3639
3730
|
} catch {
|
|
3640
3731
|
existing = null;
|
|
3641
3732
|
}
|
|
@@ -3666,7 +3757,7 @@ function engagementResolutionShell() {
|
|
|
3666
3757
|
return `# --- Halfcycle credential resolution (generated; do not edit) ----------------
|
|
3667
3758
|
# THE CREDENTIAL IS NOT IN THIS REPOSITORY. It lives at
|
|
3668
3759
|
# $HOME/${HALFCYCLE_DIR}/${ENGAGEMENTS_DIR}/<engagement-id>/${ENV_FILENAME}
|
|
3669
|
-
# written owner-only by
|
|
3760
|
+
# written owner-only by \`${bareHalfcycleCommand()}\`. What this repository holds is the
|
|
3670
3761
|
# engagement id, in the committed .halfcycle/bundle.json. So: read the id out of
|
|
3671
3762
|
# the pin, then name the file. There is no jq here and node is not dependable on a
|
|
3672
3763
|
# desktop session's PATH, so the id is cut out with sed \u2014 after flattening the
|
|
@@ -3720,8 +3811,8 @@ halfcycle_env_file() {
|
|
|
3720
3811
|
|
|
3721
3812
|
// dist/identity.js
|
|
3722
3813
|
import { execFileSync } from "node:child_process";
|
|
3723
|
-
import { existsSync, linkSync, mkdirSync as mkdirSync2, readFileSync as
|
|
3724
|
-
import { dirname as
|
|
3814
|
+
import { existsSync as existsSync2, linkSync, mkdirSync as mkdirSync2, readFileSync as readFileSync3, renameSync, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
3815
|
+
import { dirname as dirname3, isAbsolute, join as join4 } from "node:path";
|
|
3725
3816
|
import { randomUUID } from "node:crypto";
|
|
3726
3817
|
var PROJECT_IDENTITY_NOTE = 'projectId is NOT the engagement id \u2014 it identifies this repository/worktree to Halfcycle, not an engagement. The engagement id lives in .halfcycle/bundle.json, in its "engagementId" field.';
|
|
3727
3818
|
var PROJECT_IDENTITY_REL = ".halfcycle/project.json";
|
|
@@ -3834,13 +3925,13 @@ function readIgnoredPaths(targetRepoRoot, paths) {
|
|
|
3834
3925
|
const toplevel = git(targetRepoRoot, ["rev-parse", "--show-toplevel"]);
|
|
3835
3926
|
if (toplevel === null)
|
|
3836
3927
|
return null;
|
|
3837
|
-
return ignored.map((entry) => isAbsolute(entry.source) ? entry : { ...entry, source:
|
|
3928
|
+
return ignored.map((entry) => isAbsolute(entry.source) ? entry : { ...entry, source: join4(toplevel, entry.source) });
|
|
3838
3929
|
}
|
|
3839
3930
|
function mintOrReadIdentity(targetRepoRoot) {
|
|
3840
|
-
const path =
|
|
3841
|
-
if (
|
|
3931
|
+
const path = join4(targetRepoRoot, ".halfcycle", "project.json");
|
|
3932
|
+
if (existsSync2(path)) {
|
|
3842
3933
|
repairRemoteCredential(targetRepoRoot);
|
|
3843
|
-
const onDisk = JSON.parse(
|
|
3934
|
+
const onDisk = JSON.parse(readFileSync3(path, "utf-8"));
|
|
3844
3935
|
const identity = {
|
|
3845
3936
|
projectId: onDisk.projectId ?? "",
|
|
3846
3937
|
rootCommit: onDisk.rootCommit ?? null,
|
|
@@ -3865,10 +3956,10 @@ function repairRemoteCredential(targetRepoRoot) {
|
|
|
3865
3956
|
replaceProjectIdentityFile(targetRepoRoot, repaired);
|
|
3866
3957
|
}
|
|
3867
3958
|
function plannedRemoteRepair(targetRepoRoot) {
|
|
3868
|
-
const path =
|
|
3959
|
+
const path = join4(targetRepoRoot, PROJECT_IDENTITY_REL);
|
|
3869
3960
|
let onDisk;
|
|
3870
3961
|
try {
|
|
3871
|
-
const parsed = JSON.parse(
|
|
3962
|
+
const parsed = JSON.parse(readFileSync3(path, "utf-8"));
|
|
3872
3963
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
3873
3964
|
return null;
|
|
3874
3965
|
onDisk = parsed;
|
|
@@ -3887,7 +3978,7 @@ function plannedRemoteRepair(targetRepoRoot) {
|
|
|
3887
3978
|
});
|
|
3888
3979
|
}
|
|
3889
3980
|
function replaceProjectIdentityFile(targetRepoRoot, text) {
|
|
3890
|
-
const path =
|
|
3981
|
+
const path = join4(targetRepoRoot, PROJECT_IDENTITY_REL);
|
|
3891
3982
|
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
3892
3983
|
try {
|
|
3893
3984
|
writeFileSync2(temporary, text, { encoding: "utf-8", flag: "wx" });
|
|
@@ -3898,8 +3989,8 @@ function replaceProjectIdentityFile(targetRepoRoot, text) {
|
|
|
3898
3989
|
}
|
|
3899
3990
|
}
|
|
3900
3991
|
function planProjectIdentity(targetRepoRoot) {
|
|
3901
|
-
const path =
|
|
3902
|
-
if (!
|
|
3992
|
+
const path = join4(targetRepoRoot, PROJECT_IDENTITY_REL);
|
|
3993
|
+
if (!existsSync2(path)) {
|
|
3903
3994
|
return { action: "add", after: serializeProjectIdentity(mintIdentity(targetRepoRoot)) };
|
|
3904
3995
|
}
|
|
3905
3996
|
const repaired = plannedRemoteRepair(targetRepoRoot);
|
|
@@ -3907,7 +3998,7 @@ function planProjectIdentity(targetRepoRoot) {
|
|
|
3907
3998
|
return { action: "change", after: repaired };
|
|
3908
3999
|
let current;
|
|
3909
4000
|
try {
|
|
3910
|
-
current =
|
|
4001
|
+
current = readFileSync3(path, "utf-8");
|
|
3911
4002
|
} catch {
|
|
3912
4003
|
current = null;
|
|
3913
4004
|
}
|
|
@@ -3917,8 +4008,8 @@ function serializeProjectIdentity(identity) {
|
|
|
3917
4008
|
return JSON.stringify(identity, null, 2) + "\n";
|
|
3918
4009
|
}
|
|
3919
4010
|
function createProjectIdentityFile(targetRepoRoot, text) {
|
|
3920
|
-
const path =
|
|
3921
|
-
mkdirSync2(
|
|
4011
|
+
const path = join4(targetRepoRoot, PROJECT_IDENTITY_REL);
|
|
4012
|
+
mkdirSync2(dirname3(path), { recursive: true });
|
|
3922
4013
|
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
3923
4014
|
try {
|
|
3924
4015
|
writeFileSync2(temporary, text ?? serializeProjectIdentity(mintIdentity(targetRepoRoot)), {
|
|
@@ -3937,8 +4028,8 @@ function createProjectIdentityFile(targetRepoRoot, text) {
|
|
|
3937
4028
|
}
|
|
3938
4029
|
|
|
3939
4030
|
// dist/install-plan.js
|
|
3940
|
-
import { existsSync as
|
|
3941
|
-
import { join as
|
|
4031
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5 } from "node:fs";
|
|
4032
|
+
import { join as join6, posix } from "node:path";
|
|
3942
4033
|
|
|
3943
4034
|
// dist/install-manifest.js
|
|
3944
4035
|
import { Buffer as Buffer2 } from "node:buffer";
|
|
@@ -4076,11 +4167,15 @@ function parseManifest(text) {
|
|
|
4076
4167
|
"hooksCreated",
|
|
4077
4168
|
"permissionsCreated",
|
|
4078
4169
|
"denyCreated",
|
|
4079
|
-
"statusLineSha256"
|
|
4170
|
+
"statusLineSha256",
|
|
4171
|
+
"attributionSha256"
|
|
4080
4172
|
], '"settings"');
|
|
4081
4173
|
if (s["statusLineSha256"] !== void 0 && (typeof s["statusLineSha256"] !== "string" || !HEX64.test(s["statusLineSha256"]))) {
|
|
4082
4174
|
throw new UnreadableManifestError('"settings.statusLineSha256" is not a hash');
|
|
4083
4175
|
}
|
|
4176
|
+
if (s["attributionSha256"] !== void 0 && (typeof s["attributionSha256"] !== "string" || !HEX64.test(s["attributionSha256"]))) {
|
|
4177
|
+
throw new UnreadableManifestError('"settings.attributionSha256" is not a hash');
|
|
4178
|
+
}
|
|
4084
4179
|
const settings = {
|
|
4085
4180
|
created: bool(s["created"], "settings.created"),
|
|
4086
4181
|
adopted: bool(s["adopted"], "settings.adopted"),
|
|
@@ -4091,7 +4186,8 @@ function parseManifest(text) {
|
|
|
4091
4186
|
hooksCreated: bool(s["hooksCreated"], "settings.hooksCreated"),
|
|
4092
4187
|
permissionsCreated: bool(s["permissionsCreated"], "settings.permissionsCreated"),
|
|
4093
4188
|
denyCreated: bool(s["denyCreated"], "settings.denyCreated"),
|
|
4094
|
-
...typeof s["statusLineSha256"] === "string" ? { statusLineSha256: s["statusLineSha256"] } : {}
|
|
4189
|
+
...typeof s["statusLineSha256"] === "string" ? { statusLineSha256: s["statusLineSha256"] } : {},
|
|
4190
|
+
...typeof s["attributionSha256"] === "string" ? { attributionSha256: s["attributionSha256"] } : {}
|
|
4095
4191
|
};
|
|
4096
4192
|
let mcp;
|
|
4097
4193
|
if (raw["mcp"] !== void 0) {
|
|
@@ -4198,6 +4294,7 @@ function recordSettings(draft, observed) {
|
|
|
4198
4294
|
return;
|
|
4199
4295
|
}
|
|
4200
4296
|
const statusLineSha256 = observed.statusLineSha256 ?? prev.statusLineSha256;
|
|
4297
|
+
const attributionSha256 = observed.attributionSha256 ?? prev.attributionSha256;
|
|
4201
4298
|
draft.settings = {
|
|
4202
4299
|
created: prev.created,
|
|
4203
4300
|
adopted: prev.adopted,
|
|
@@ -4208,7 +4305,8 @@ function recordSettings(draft, observed) {
|
|
|
4208
4305
|
hooksCreated: prev.hooksCreated,
|
|
4209
4306
|
permissionsCreated: prev.permissionsCreated,
|
|
4210
4307
|
denyCreated: prev.denyCreated,
|
|
4211
|
-
...statusLineSha256 !== void 0 ? { statusLineSha256 } : {}
|
|
4308
|
+
...statusLineSha256 !== void 0 ? { statusLineSha256 } : {},
|
|
4309
|
+
...attributionSha256 !== void 0 ? { attributionSha256 } : {}
|
|
4212
4310
|
};
|
|
4213
4311
|
}
|
|
4214
4312
|
function recordDenyRetired(draft, hashes) {
|
|
@@ -4289,8 +4387,8 @@ function union(a, b) {
|
|
|
4289
4387
|
}
|
|
4290
4388
|
|
|
4291
4389
|
// dist/scan.js
|
|
4292
|
-
import { existsSync as
|
|
4293
|
-
import { join as
|
|
4390
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
|
|
4391
|
+
import { join as join5 } from "node:path";
|
|
4294
4392
|
var HALFCYCLE_STATE_FORMAT = "halfcycle-state-file/v1";
|
|
4295
4393
|
var HALFCYCLE_STATE_NOTE = [
|
|
4296
4394
|
"This file is a local cache written ONCE when Halfcycle was installed. It is not a",
|
|
@@ -4313,7 +4411,7 @@ var LAYER_ARTEFACT_PATHS = {
|
|
|
4313
4411
|
function scanLayers(targetRepo, scannedAt = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
4314
4412
|
const layers = {};
|
|
4315
4413
|
for (const [layer, paths] of Object.entries(LAYER_ARTEFACT_PATHS)) {
|
|
4316
|
-
const found = paths.filter((p) =>
|
|
4414
|
+
const found = paths.filter((p) => existsSync3(join5(targetRepo, p)));
|
|
4317
4415
|
if (found.length > 0) {
|
|
4318
4416
|
layers[layer] = {
|
|
4319
4417
|
value: "asserted",
|
|
@@ -4333,17 +4431,17 @@ function runBootstrapScan(targetRepo) {
|
|
|
4333
4431
|
return { state: planned.state, ran: planned.text !== null };
|
|
4334
4432
|
}
|
|
4335
4433
|
function planBootstrapScan(targetRepo) {
|
|
4336
|
-
const statePath =
|
|
4337
|
-
if (
|
|
4338
|
-
const existing = JSON.parse(
|
|
4434
|
+
const statePath = join5(targetRepo, BOOTSTRAP_STATE_REL);
|
|
4435
|
+
if (existsSync3(statePath)) {
|
|
4436
|
+
const existing = JSON.parse(readFileSync4(statePath, "utf-8"));
|
|
4339
4437
|
return { state: existing, text: null };
|
|
4340
4438
|
}
|
|
4341
4439
|
const state = scanLayers(targetRepo);
|
|
4342
4440
|
return { state, text: JSON.stringify(state, null, 2) + "\n" };
|
|
4343
4441
|
}
|
|
4344
4442
|
function writeBootstrapState(targetRepo, text) {
|
|
4345
|
-
mkdirSync3(
|
|
4346
|
-
writeFileSync3(
|
|
4443
|
+
mkdirSync3(join5(targetRepo, ".halfcycle"), { recursive: true });
|
|
4444
|
+
writeFileSync3(join5(targetRepo, BOOTSTRAP_STATE_REL), text, "utf-8");
|
|
4347
4445
|
}
|
|
4348
4446
|
function mergeWithRecords(state, layersWithRealRecords) {
|
|
4349
4447
|
const merged = {};
|
|
@@ -4500,7 +4598,7 @@ function kindOf(rel) {
|
|
|
4500
4598
|
}
|
|
4501
4599
|
var STEPS = /* @__PURE__ */ new WeakMap();
|
|
4502
4600
|
function planInstall(options) {
|
|
4503
|
-
const { targetRepo, engagementId, engagementType, credential, home, env, createdByAccountId } = options;
|
|
4601
|
+
const { targetRepo, engagementId, engagementType, credential, home, env, createdByAccountId, serviceUrl } = options;
|
|
4504
4602
|
const preview = engagementId === void 0;
|
|
4505
4603
|
if (!preview) {
|
|
4506
4604
|
assertEngagementId(engagementId);
|
|
@@ -4508,12 +4606,12 @@ function planInstall(options) {
|
|
|
4508
4606
|
throw new Error("[halfcycle] a plan for a project needs its kind as well as its id.");
|
|
4509
4607
|
}
|
|
4510
4608
|
}
|
|
4511
|
-
if (!
|
|
4609
|
+
if (!existsSync4(targetRepo)) {
|
|
4512
4610
|
throw new Error(`[halfcycle] Target repo does not exist: ${targetRepo}`);
|
|
4513
4611
|
}
|
|
4514
4612
|
const writeSet = closedWriteSet();
|
|
4515
4613
|
const previous = readPreviousManifest(targetRepo);
|
|
4516
|
-
const dirsBefore = new Set([...writeSet.dirs].filter((dir) =>
|
|
4614
|
+
const dirsBefore = new Set([...writeSet.dirs].filter((dir) => existsSync4(join6(targetRepo, dir))));
|
|
4517
4615
|
const { version, commands } = planCommandStubs(targetRepo, previous, writeSet);
|
|
4518
4616
|
const codexSkills = planCodexSkills(targetRepo, previous, writeSet);
|
|
4519
4617
|
const vendored = planVendoredBinary(targetRepo);
|
|
@@ -4535,12 +4633,12 @@ function planInstall(options) {
|
|
|
4535
4633
|
const gitattributes = planGitattributes(targetRepo);
|
|
4536
4634
|
const identity = planProjectIdentity(targetRepo);
|
|
4537
4635
|
if (identity.after === null)
|
|
4538
|
-
|
|
4636
|
+
readFileSync5(join6(targetRepo, PROJECT_IDENTITY_REL));
|
|
4539
4637
|
const credentialPlan = !preview && credential !== void 0 ? planEngagementCredential(credential, engagementId, home) : null;
|
|
4540
4638
|
const credentialWrite = credentialPlan === null ? null : { outcome: credentialPlan.outcome, existed: credentialPlan.existed, path: engagementEnvPath(engagementId, home) };
|
|
4541
4639
|
const storedAfter = credentialPlan?.storedAfter ?? (preview ? Object.fromEntries(ENGAGEMENT_ENV_KEYS.map((key) => [key, "supplied at sign-in"])) : readEngagementEnv(engagementId, home) ?? {});
|
|
4542
4640
|
const legacy = migrateLegacyEnvLocal(targetRepo, storedAfter);
|
|
4543
|
-
const pin = preview ? planBundlePinPreview(targetRepo, version, home) : planBundlePin(targetRepo, version, engagementId, engagementType, createdByAccountId, credential?.methodVersion);
|
|
4641
|
+
const pin = preview ? planBundlePinPreview(targetRepo, version, home) : planBundlePin(targetRepo, version, engagementId, engagementType, createdByAccountId, credential?.methodVersion, credential?.serviceUrl ?? serviceUrl);
|
|
4544
4642
|
const crew = planCrewRoster(targetRepo);
|
|
4545
4643
|
const crewProfiles = planCrewProfiles(targetRepo, previous, writeSet);
|
|
4546
4644
|
const scan = { text: planBootstrapScan(targetRepo).text };
|
|
@@ -4668,7 +4766,7 @@ function entriesOf(steps, record2) {
|
|
|
4668
4766
|
}
|
|
4669
4767
|
};
|
|
4670
4768
|
const pending = (rel) => {
|
|
4671
|
-
const there =
|
|
4769
|
+
const there = existsSync4(join6(targetRepo, rel));
|
|
4672
4770
|
entries.push({ path: rel, action: there ? "change" : "add", kind: kindOf(rel), bytes: PENDING });
|
|
4673
4771
|
};
|
|
4674
4772
|
steps.commands.forEach(fromFile);
|
|
@@ -4716,7 +4814,7 @@ function entriesOf(steps, record2) {
|
|
|
4716
4814
|
action: "leave",
|
|
4717
4815
|
kind: "codex-server",
|
|
4718
4816
|
bytes: null,
|
|
4719
|
-
...
|
|
4817
|
+
...existsSync4(join6(targetRepo, CODEX_CONFIG_REL2)) ? { yours: true } : {}
|
|
4720
4818
|
});
|
|
4721
4819
|
break;
|
|
4722
4820
|
}
|
|
@@ -4795,10 +4893,10 @@ function applyInstallPlan(plan) {
|
|
|
4795
4893
|
putPlannedCodexHooks(targetRepo, steps.codexHooks);
|
|
4796
4894
|
putPlannedCodexConfig(targetRepo, steps.codexConfig);
|
|
4797
4895
|
const gitignore = putPlannedGitignore(targetRepo, steps.gitignore);
|
|
4798
|
-
const gitignorePath =
|
|
4799
|
-
const gitignoreAfter =
|
|
4896
|
+
const gitignorePath = join6(targetRepo, GITIGNORE_REL);
|
|
4897
|
+
const gitignoreAfter = existsSync4(gitignorePath) ? readFileSync5(gitignorePath, "utf-8") : null;
|
|
4800
4898
|
const gitattributes = putPlannedGitattributes(targetRepo, steps.gitattributes);
|
|
4801
|
-
const gitattributesAfter = readTextOrNull(
|
|
4899
|
+
const gitattributesAfter = readTextOrNull(join6(targetRepo, GITATTRIBUTES_REL));
|
|
4802
4900
|
writeProjectIdentity(targetRepo, steps.identity);
|
|
4803
4901
|
const credential = steps.credential;
|
|
4804
4902
|
const credentialOutcome = steps.credentialWrite !== null && credential !== void 0 ? writeEngagementCredential(credential, engagementId, steps.home) : null;
|
|
@@ -4819,9 +4917,9 @@ function applyInstallPlan(plan) {
|
|
|
4819
4917
|
gitattributesAfter,
|
|
4820
4918
|
credential: credentialOutcome,
|
|
4821
4919
|
legacy,
|
|
4822
|
-
bytesAfter: (rel) =>
|
|
4823
|
-
dirExistsAfter: (dir) =>
|
|
4824
|
-
stateExistsAfter:
|
|
4920
|
+
bytesAfter: (rel) => readFileSync5(join6(targetRepo, rel)),
|
|
4921
|
+
dirExistsAfter: (dir) => existsSync4(join6(targetRepo, dir)),
|
|
4922
|
+
stateExistsAfter: existsSync4(join6(targetRepo, BOOTSTRAP_STATE_REL))
|
|
4825
4923
|
});
|
|
4826
4924
|
const record2 = planInstallRecord(targetRepo, manifestText);
|
|
4827
4925
|
putPlannedFile(targetRepo, record2);
|
|
@@ -4876,7 +4974,7 @@ function withoutHalfcycleCommands(entry, halfcycleCommands) {
|
|
|
4876
4974
|
const remaining = entry.hooks.filter((h) => !isHalfcycleCommand(h, halfcycleCommands));
|
|
4877
4975
|
return remaining.length > 0 ? { ...entry, hooks: remaining } : null;
|
|
4878
4976
|
}
|
|
4879
|
-
function mergeSettings(existing, generated, retiredOurs, statusLine) {
|
|
4977
|
+
function mergeSettings(existing, generated, retiredOurs, statusLine, attribution) {
|
|
4880
4978
|
const merged = { ...existing };
|
|
4881
4979
|
if (generated.$schema !== void 0) {
|
|
4882
4980
|
merged.$schema = generated.$schema;
|
|
@@ -4902,6 +5000,9 @@ function mergeSettings(existing, generated, retiredOurs, statusLine) {
|
|
|
4902
5000
|
if (statusLine !== void 0 && !("statusLine" in existing)) {
|
|
4903
5001
|
merged.statusLine = { ...statusLine };
|
|
4904
5002
|
}
|
|
5003
|
+
if (attribution !== void 0 && !("attribution" in existing) && !("includeCoAuthoredBy" in existing)) {
|
|
5004
|
+
merged.attribution = { ...attribution };
|
|
5005
|
+
}
|
|
4905
5006
|
return merged;
|
|
4906
5007
|
}
|
|
4907
5008
|
function isPlainRecord(value) {
|
|
@@ -5172,8 +5273,8 @@ function crewProfileRel(agent, callsign) {
|
|
|
5172
5273
|
}
|
|
5173
5274
|
function crewProfileTarget(targetRepo, rel) {
|
|
5174
5275
|
const folder = rel.startsWith(`${CLAUDE_AGENTS_DIR}/`) ? CLAUDE_AGENTS_DIR : CODEX_AGENTS_DIR;
|
|
5175
|
-
const target = resolve(
|
|
5176
|
-
if (
|
|
5276
|
+
const target = resolve(join7(targetRepo, rel));
|
|
5277
|
+
if (dirname4(target) !== resolve(targetRepo, folder)) {
|
|
5177
5278
|
throw new Error(`[halfcycle] crew profile "${rel}" does not sit directly in ${folder}.`);
|
|
5178
5279
|
}
|
|
5179
5280
|
return target;
|
|
@@ -5256,41 +5357,25 @@ function renderCrewProfiles(roster = CREW_ROSTER) {
|
|
|
5256
5357
|
])
|
|
5257
5358
|
];
|
|
5258
5359
|
}
|
|
5259
|
-
var __filename = fileURLToPath(import.meta.url);
|
|
5260
|
-
var __dirname = dirname3(__filename);
|
|
5261
|
-
function findBundleRoot(startDir) {
|
|
5262
|
-
let candidate = startDir;
|
|
5263
|
-
for (let i = 0; i < 4; i++) {
|
|
5264
|
-
const manifest = join6(candidate, ".claude-plugin", "plugin.json");
|
|
5265
|
-
try {
|
|
5266
|
-
if (existsSync4(manifest))
|
|
5267
|
-
return candidate;
|
|
5268
|
-
} catch {
|
|
5269
|
-
}
|
|
5270
|
-
candidate = dirname3(candidate);
|
|
5271
|
-
}
|
|
5272
|
-
return join6(startDir, "..");
|
|
5273
|
-
}
|
|
5274
|
-
var BUNDLE_ROOT = findBundleRoot(__dirname);
|
|
5275
5360
|
function resolveVendoredBinary() {
|
|
5276
5361
|
const candidates = [
|
|
5277
|
-
|
|
5278
|
-
|
|
5362
|
+
join7(BUNDLE_ROOT, "bin", "bin.bundle.mjs"),
|
|
5363
|
+
join7(BUNDLE_ROOT, "..", "runner", "dist", "bin.bundle.mjs")
|
|
5279
5364
|
];
|
|
5280
5365
|
for (const candidate of candidates) {
|
|
5281
|
-
if (
|
|
5366
|
+
if (existsSync5(candidate))
|
|
5282
5367
|
return candidate;
|
|
5283
5368
|
}
|
|
5284
5369
|
throw new Error(`[halfcycle] Cannot find the self-contained guard binary bin.bundle.mjs. This copy of the halfcycle package is incomplete \u2014 reinstall it, or rebuild it if you are working on it. Looked in: ${candidates.join(", ")}.`);
|
|
5285
5370
|
}
|
|
5286
5371
|
function resolvePreFilterShell() {
|
|
5287
5372
|
const candidates = [
|
|
5288
|
-
|
|
5289
|
-
|
|
5373
|
+
join7(BUNDLE_ROOT, "bin", PRE_FILTER_SHELL_FILENAME),
|
|
5374
|
+
join7(BUNDLE_ROOT, "..", "runner", "dist", PRE_FILTER_SHELL_FILENAME)
|
|
5290
5375
|
];
|
|
5291
5376
|
for (const candidate of candidates) {
|
|
5292
|
-
if (
|
|
5293
|
-
return
|
|
5377
|
+
if (existsSync5(candidate))
|
|
5378
|
+
return readFileSync6(candidate, "utf-8");
|
|
5294
5379
|
}
|
|
5295
5380
|
throw new Error(`[halfcycle] Cannot find ${PRE_FILTER_SHELL_FILENAME}. This copy of the halfcycle package is incomplete \u2014 reinstall it, or rebuild it if you are working on it. Looked in: ${candidates.join(", ")}.`);
|
|
5296
5381
|
}
|
|
@@ -5329,7 +5414,7 @@ function isAllowlisted(targetRelPath) {
|
|
|
5329
5414
|
var CREW_ROSTER_REL = ".halfcycle/crew.json";
|
|
5330
5415
|
var CAPTURED_INDEX_REL = "test/fixtures/captured/manifest.json";
|
|
5331
5416
|
function capturedIndexScaffold() {
|
|
5332
|
-
return
|
|
5417
|
+
return join7(BUNDLE_ROOT, "scaffolding", "test", "fixtures", "captured", "manifest.json");
|
|
5333
5418
|
}
|
|
5334
5419
|
var GITIGNORE_REL = ".gitignore";
|
|
5335
5420
|
var GITATTRIBUTES_REL = ".gitattributes";
|
|
@@ -5398,20 +5483,22 @@ function closedWriteSet() {
|
|
|
5398
5483
|
...lfRules([...Object.keys(OWNED_GENERATED_HEADERS), ...RETIRED_WRITE_SET_FILES])
|
|
5399
5484
|
]),
|
|
5400
5485
|
generatedSha256: sha256OfWhatThisVersionWrites(commands),
|
|
5401
|
-
statusLineSha256: /* @__PURE__ */ new Set([canonicalSha256(HALFCYCLE_STATUS_LINE)])
|
|
5486
|
+
statusLineSha256: /* @__PURE__ */ new Set([canonicalSha256(HALFCYCLE_STATUS_LINE)]),
|
|
5487
|
+
attributionSha256: /* @__PURE__ */ new Set([canonicalSha256(HALFCYCLE_ATTRIBUTION)])
|
|
5402
5488
|
};
|
|
5403
5489
|
}
|
|
5404
5490
|
function sha256OfWhatThisVersionWrites(commands) {
|
|
5405
5491
|
const out = /* @__PURE__ */ new Map();
|
|
5406
5492
|
const fromFile = (rel, src) => {
|
|
5407
|
-
if (
|
|
5408
|
-
out.set(rel, fileSha256(
|
|
5493
|
+
if (existsSync5(src))
|
|
5494
|
+
out.set(rel, fileSha256(readFileSync6(src)));
|
|
5409
5495
|
};
|
|
5410
5496
|
for (const cmd of commands) {
|
|
5411
|
-
const srcFile =
|
|
5412
|
-
|
|
5413
|
-
|
|
5414
|
-
out.set(
|
|
5497
|
+
const srcFile = join7(BUNDLE_ROOT, ".claude-plugin", cmd.path);
|
|
5498
|
+
if (existsSync5(srcFile)) {
|
|
5499
|
+
const text = commandFileText(readFileSync6(srcFile, "utf-8"), ownVersion());
|
|
5500
|
+
out.set(commandStubRel(cmd.name), fileSha256(Buffer.from(text, "utf-8")));
|
|
5501
|
+
out.set(skillRel(cmd.name), fileSha256(Buffer.from(codexSkillContent(cmd.name, text), "utf-8")));
|
|
5415
5502
|
}
|
|
5416
5503
|
}
|
|
5417
5504
|
for (const [rel, content] of hookScriptsThisVersionWrites())
|
|
@@ -5428,7 +5515,7 @@ function bytesAreOurs(path, bytes, recorded, writeSet) {
|
|
|
5428
5515
|
}
|
|
5429
5516
|
function readPreviousManifest(targetRepo) {
|
|
5430
5517
|
try {
|
|
5431
|
-
return parseManifest(
|
|
5518
|
+
return parseManifest(readFileSync6(join7(targetRepo, INSTALL_MANIFEST_REL), "utf-8"));
|
|
5432
5519
|
} catch {
|
|
5433
5520
|
return null;
|
|
5434
5521
|
}
|
|
@@ -5458,7 +5545,8 @@ function recordSettingsMerge(draft, existing, generated, preexisted, merged) {
|
|
|
5458
5545
|
hooksCreated: existing.hooks === void 0,
|
|
5459
5546
|
permissionsCreated: existing.permissions === void 0,
|
|
5460
5547
|
denyCreated: existing.permissions?.deny === void 0,
|
|
5461
|
-
...ourStatusLineSha256(merged) !== void 0 ? { statusLineSha256: ourStatusLineSha256(merged) } : {}
|
|
5548
|
+
...ourStatusLineSha256(merged) !== void 0 ? { statusLineSha256: ourStatusLineSha256(merged) } : {},
|
|
5549
|
+
...attributionWrittenThisRun(existing, merged) ? { attributionSha256: canonicalSha256(HALFCYCLE_ATTRIBUTION) } : {}
|
|
5462
5550
|
});
|
|
5463
5551
|
}
|
|
5464
5552
|
function ourStatusLineSha256(settings) {
|
|
@@ -5467,6 +5555,11 @@ function ourStatusLineSha256(settings) {
|
|
|
5467
5555
|
const hash = canonicalSha256(settings["statusLine"]);
|
|
5468
5556
|
return hash === canonicalSha256(HALFCYCLE_STATUS_LINE) ? hash : void 0;
|
|
5469
5557
|
}
|
|
5558
|
+
function attributionWrittenThisRun(existing, merged) {
|
|
5559
|
+
if ("attribution" in existing || "includeCoAuthoredBy" in existing)
|
|
5560
|
+
return false;
|
|
5561
|
+
return "attribution" in merged && canonicalSha256(merged["attribution"]) === canonicalSha256(HALFCYCLE_ATTRIBUTION);
|
|
5562
|
+
}
|
|
5470
5563
|
function recordMcpMerge(draft, existingText, writtenText) {
|
|
5471
5564
|
const before = existingText === null ? void 0 : JSON.parse(existingText);
|
|
5472
5565
|
const servers = isPlainObject(before) ? before["mcpServers"] : void 0;
|
|
@@ -5532,8 +5625,8 @@ function findNotShared(targetRepo, manifest, collidedPaths) {
|
|
|
5532
5625
|
for (let dir = posix2.dirname(rel); dir !== "."; dir = posix2.dirname(dir)) {
|
|
5533
5626
|
let plain = dirVerdicts.get(dir);
|
|
5534
5627
|
if (plain === void 0) {
|
|
5535
|
-
const abs =
|
|
5536
|
-
plain = !lstatOrNull(abs)?.isSymbolicLink() && lstatOrNull(
|
|
5628
|
+
const abs = join7(targetRepo, dir);
|
|
5629
|
+
plain = !lstatOrNull(abs)?.isSymbolicLink() && lstatOrNull(join7(abs, ".git")) === null;
|
|
5537
5630
|
dirVerdicts.set(dir, plain);
|
|
5538
5631
|
}
|
|
5539
5632
|
if (!plain)
|
|
@@ -5547,7 +5640,7 @@ function findNotShared(targetRepo, manifest, collidedPaths) {
|
|
|
5547
5640
|
...merged,
|
|
5548
5641
|
INSTALL_MANIFEST_REL
|
|
5549
5642
|
])
|
|
5550
|
-
].filter((rel) => !collided.has(rel)).filter((rel) => lstatOrNull(
|
|
5643
|
+
].filter((rel) => !collided.has(rel)).filter((rel) => lstatOrNull(join7(targetRepo, rel)) !== null).filter(travelsThroughPlainDirs).sort();
|
|
5551
5644
|
const ignored = readIgnoredPaths(targetRepo, paths);
|
|
5552
5645
|
if (ignored === null)
|
|
5553
5646
|
return null;
|
|
@@ -5570,7 +5663,7 @@ function lstatOrNull(path) {
|
|
|
5570
5663
|
}
|
|
5571
5664
|
function readFileOrNull(path) {
|
|
5572
5665
|
try {
|
|
5573
|
-
return
|
|
5666
|
+
return readFileSync6(path, "utf-8");
|
|
5574
5667
|
} catch {
|
|
5575
5668
|
return null;
|
|
5576
5669
|
}
|
|
@@ -5579,8 +5672,8 @@ function isPlainObject(value) {
|
|
|
5579
5672
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
5580
5673
|
}
|
|
5581
5674
|
function readPluginManifest() {
|
|
5582
|
-
const manifestPath =
|
|
5583
|
-
const raw =
|
|
5675
|
+
const manifestPath = join7(BUNDLE_ROOT, ".claude-plugin", "plugin.json");
|
|
5676
|
+
const raw = readFileSync6(manifestPath, "utf-8");
|
|
5584
5677
|
return JSON.parse(raw);
|
|
5585
5678
|
}
|
|
5586
5679
|
function parseCommandStub(raw, name) {
|
|
@@ -5623,6 +5716,9 @@ function codexSkillBody(name, sourceBody) {
|
|
|
5623
5716
|
}
|
|
5624
5717
|
return transformed;
|
|
5625
5718
|
}
|
|
5719
|
+
function commandFileText(source, version) {
|
|
5720
|
+
return rewriteHalfcycleCommands(source, releaseTagOfVersion(version));
|
|
5721
|
+
}
|
|
5626
5722
|
function codexSkillContent(name, sourceRaw) {
|
|
5627
5723
|
const { description, body } = parseCommandStub(sourceRaw, name);
|
|
5628
5724
|
const transformed = codexSkillBody(name, body);
|
|
@@ -5635,14 +5731,14 @@ ${transformed}`;
|
|
|
5635
5731
|
function planManifestCommands(manifest, targetRepo, previous, writeSet) {
|
|
5636
5732
|
const recordedFiles = new Map((previous?.files ?? []).filter((file) => writeSet.files.has(file.path)).map((file) => [file.path, file]));
|
|
5637
5733
|
return (manifest.commands ?? []).map((cmd) => {
|
|
5638
|
-
const srcFile =
|
|
5639
|
-
if (!
|
|
5734
|
+
const srcFile = join7(BUNDLE_ROOT, ".claude-plugin", cmd.path);
|
|
5735
|
+
if (!existsSync5(srcFile)) {
|
|
5640
5736
|
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.`);
|
|
5641
5737
|
}
|
|
5642
5738
|
const rel = commandStubRel(cmd.name);
|
|
5643
|
-
const content =
|
|
5739
|
+
const content = commandFileText(readFileSync6(srcFile, "utf-8"), ownVersion());
|
|
5644
5740
|
const recorded = recordedFiles.get(rel);
|
|
5645
|
-
const planned = decideCollisionSafe(
|
|
5741
|
+
const planned = decideCollisionSafe(join7(targetRepo, rel), targetRepo, content, (current) => bytesAreOurs(rel, current, recorded, writeSet));
|
|
5646
5742
|
return { ...planned, editedCollided: planned.outcome === "collided" && recorded !== void 0 };
|
|
5647
5743
|
});
|
|
5648
5744
|
}
|
|
@@ -5652,8 +5748,8 @@ function putPlannedFile(targetRepoRoot, planned) {
|
|
|
5652
5748
|
if (!isAllowlisted(planned.rel)) {
|
|
5653
5749
|
throw new Error(`[halfcycle] Write-allowlist violation: attempted to write "${planned.rel}". Only the method surface and scaffolding paths are writable.`);
|
|
5654
5750
|
}
|
|
5655
|
-
const target =
|
|
5656
|
-
mkdirSync4(
|
|
5751
|
+
const target = join7(targetRepoRoot, planned.rel);
|
|
5752
|
+
mkdirSync4(dirname4(target), { recursive: true });
|
|
5657
5753
|
writeFileSync4(target, planned.text, "utf-8");
|
|
5658
5754
|
}
|
|
5659
5755
|
function writeAllowlisted(targetAbsPath, targetRepoRoot, content, writtenPaths) {
|
|
@@ -5663,7 +5759,7 @@ function writeAllowlisted(targetAbsPath, targetRepoRoot, content, writtenPaths)
|
|
|
5663
5759
|
}
|
|
5664
5760
|
if (readFileOrNull(targetAbsPath) === content)
|
|
5665
5761
|
return;
|
|
5666
|
-
mkdirSync4(
|
|
5762
|
+
mkdirSync4(dirname4(targetAbsPath), { recursive: true });
|
|
5667
5763
|
writeFileSync4(targetAbsPath, content, "utf-8");
|
|
5668
5764
|
writtenPaths.push(rel);
|
|
5669
5765
|
}
|
|
@@ -5672,7 +5768,7 @@ function decideCollisionSafe(targetAbsPath, targetRepoRoot, content, isOurs) {
|
|
|
5672
5768
|
if (!isAllowlisted(rel)) {
|
|
5673
5769
|
throw new Error(`[halfcycle] Write-allowlist violation: attempted to write "${rel}". Only the method surface and scaffolding paths are writable.`);
|
|
5674
5770
|
}
|
|
5675
|
-
const current =
|
|
5771
|
+
const current = existsSync5(targetAbsPath) ? readFileSync6(targetAbsPath) : null;
|
|
5676
5772
|
const text = inLineEndingOf(current?.toString("utf-8") ?? null, content);
|
|
5677
5773
|
if (current === null)
|
|
5678
5774
|
return { rel, text, outcome: "written", replacedExisting: false };
|
|
@@ -5695,7 +5791,7 @@ function decideOwned(targetAbsPath, targetRepoRoot, content) {
|
|
|
5695
5791
|
const text = inLineEndingOf(current, content);
|
|
5696
5792
|
if (current === text)
|
|
5697
5793
|
return { rel, text, outcome: "skipped", replacedExisting: false };
|
|
5698
|
-
return { rel, text, outcome: "written", replacedExisting:
|
|
5794
|
+
return { rel, text, outcome: "written", replacedExisting: existsSync5(targetAbsPath) };
|
|
5699
5795
|
}
|
|
5700
5796
|
var GENERATED_HEADER_LINE_INDEX = 1;
|
|
5701
5797
|
function decideOwnedGenerated(targetAbsPath, targetRepoRoot, content, generatedMarker) {
|
|
@@ -5703,9 +5799,9 @@ function decideOwnedGenerated(targetAbsPath, targetRepoRoot, content, generatedM
|
|
|
5703
5799
|
if (!isAllowlisted(rel)) {
|
|
5704
5800
|
throw new Error(`[halfcycle] Write-allowlist violation: attempted to write "${rel}". Only the method surface and scaffolding paths are writable.`);
|
|
5705
5801
|
}
|
|
5706
|
-
if (!
|
|
5802
|
+
if (!existsSync5(targetAbsPath))
|
|
5707
5803
|
return { rel, text: content, outcome: "written", replacedExisting: false };
|
|
5708
|
-
const current =
|
|
5804
|
+
const current = readFileSync6(targetAbsPath, "utf-8");
|
|
5709
5805
|
if (current === content)
|
|
5710
5806
|
return { rel, text: content, outcome: "skipped", replacedExisting: false };
|
|
5711
5807
|
const headerLine = current.split("\n")[GENERATED_HEADER_LINE_INDEX] ?? "";
|
|
@@ -5735,8 +5831,8 @@ var DENY_PATTERNS = [
|
|
|
5735
5831
|
];
|
|
5736
5832
|
var USER_PROMPT_REMINDER_SENTENCE = "Where this project stands, and what order to work in, is recorded in the Halfcycle service and nowhere in this repository \u2014 call halfcycle_resolve before you answer or plan, because the files here will look complete and current and still be wrong about position.";
|
|
5737
5833
|
function readExistingSettings(targetRepo) {
|
|
5738
|
-
const settingsPath =
|
|
5739
|
-
return
|
|
5834
|
+
const settingsPath = join7(targetRepo, SETTINGS_REL);
|
|
5835
|
+
return existsSync5(settingsPath) ? JSON.parse(readFileSync6(settingsPath, "utf-8")) : void 0;
|
|
5740
5836
|
}
|
|
5741
5837
|
var SettingsParseRefused = class extends Error {
|
|
5742
5838
|
constructor(message) {
|
|
@@ -5745,33 +5841,37 @@ var SettingsParseRefused = class extends Error {
|
|
|
5745
5841
|
}
|
|
5746
5842
|
};
|
|
5747
5843
|
function settingsParseRefusal(targetRepo, err) {
|
|
5748
|
-
const file =
|
|
5844
|
+
const file = join7(targetRepo, SETTINGS_REL);
|
|
5749
5845
|
const offset = /position (\d+)/.exec(err instanceof Error ? err.message : "")?.[1];
|
|
5750
5846
|
let where = "";
|
|
5751
5847
|
if (offset !== void 0) {
|
|
5752
|
-
const before =
|
|
5848
|
+
const before = readFileSync6(file, "utf-8").slice(0, Number(offset)).split("\n");
|
|
5753
5849
|
where = ` (line ${before.length}, column ${before[before.length - 1].length + 1})`;
|
|
5754
5850
|
}
|
|
5755
|
-
return `${file} could not be read as JSON${where} \u2014 fix it and run "
|
|
5851
|
+
return `${file} could not be read as JSON${where} \u2014 fix it and run "${ownBareCommand()}" again.`;
|
|
5756
5852
|
}
|
|
5757
5853
|
var SETTINGS_SCHEMA = "https://json.schemastore.org/claude-code-settings.json";
|
|
5758
5854
|
var HALFCYCLE_STATUS_LINE = Object.freeze({
|
|
5759
5855
|
type: "command",
|
|
5760
5856
|
command: 'bash "$CLAUDE_PROJECT_DIR"/.claude/hooks/guard-runner.sh statusline'
|
|
5761
5857
|
});
|
|
5858
|
+
var HALFCYCLE_ATTRIBUTION = Object.freeze({
|
|
5859
|
+
commit: "",
|
|
5860
|
+
pr: ""
|
|
5861
|
+
});
|
|
5762
5862
|
function statusLineSetElsewhere(targetRepo, home = homedir2(), env = process.env) {
|
|
5763
|
-
const places = [
|
|
5863
|
+
const places = [join7(targetRepo, ".claude", "settings.local.json"), join7(home, ".claude", "settings.json")];
|
|
5764
5864
|
const configDir = env["CLAUDE_CONFIG_DIR"];
|
|
5765
5865
|
if (configDir !== void 0 && configDir !== "") {
|
|
5766
|
-
const expanded = configDir === "~" ? home : configDir.startsWith("~/") ?
|
|
5767
|
-
places.push(
|
|
5866
|
+
const expanded = configDir === "~" ? home : configDir.startsWith("~/") ? join7(home, configDir.slice(2)) : configDir;
|
|
5867
|
+
places.push(join7(isAbsolute2(expanded) ? expanded : resolve(expanded), "settings.json"));
|
|
5768
5868
|
}
|
|
5769
5869
|
return places.some((path) => {
|
|
5770
|
-
if (!
|
|
5870
|
+
if (!existsSync5(path))
|
|
5771
5871
|
return false;
|
|
5772
5872
|
let value;
|
|
5773
5873
|
try {
|
|
5774
|
-
value = JSON.parse(
|
|
5874
|
+
value = JSON.parse(readFileSync6(path, "utf-8"));
|
|
5775
5875
|
} catch {
|
|
5776
5876
|
return true;
|
|
5777
5877
|
}
|
|
@@ -6114,8 +6214,10 @@ ${GENERATED_USER_PROMPT_SCOPE_HEADER} the Halfcycle installer, now also for the
|
|
|
6114
6214
|
# "autonomy on" or "autonomy off". Also lets the person who is typing let one
|
|
6115
6215
|
# step Halfcycle paused run once, by typing "allow", or "allow" and the step's
|
|
6116
6216
|
# number ("allow 2"), as the whole prompt, and change
|
|
6117
|
-
# how a build
|
|
6118
|
-
#
|
|
6217
|
+
# how many agents a build runs, by typing "faster", "run 5 agents", "run 20
|
|
6218
|
+
# agents at once", "max pace" or "as many agents as possible", or how much is
|
|
6219
|
+
# kept free, by typing "keep 30% free" or "use up to 70%", as the whole prompt,
|
|
6220
|
+
# with or without a build running, and answer a phase's usage limit by
|
|
6119
6221
|
# typing "keep going", "stop", "limit it to 1.5 times" or "limit it to 10% of
|
|
6120
6222
|
# the week" as the whole prompt.
|
|
6121
6223
|
#
|
|
@@ -6124,7 +6226,7 @@ ${GENERATED_USER_PROMPT_SCOPE_HEADER} the Halfcycle installer, now also for the
|
|
|
6124
6226
|
# any whitespace (typed, or escaped in the input), with "approve ", "send back ",
|
|
6125
6227
|
# "accept ", "question ", "carry ", "drop ", "reject " or "autonomy ", in any case,
|
|
6126
6228
|
# or is "allow" (alone, or with spaces and a number of up to four digits), one
|
|
6127
|
-
# of the
|
|
6229
|
+
# of the pacing phrases or one of the usage-limit phrases
|
|
6128
6230
|
# and nothing else but whitespace,
|
|
6129
6231
|
# it exits 0 with no output, and Node never starts. A quote inside any other field arrives
|
|
6130
6232
|
# escaped, so no other field (the folder the session is in, for one) can read as
|
|
@@ -6152,7 +6254,11 @@ IFS= read -r -d '' HC_INPUT || true
|
|
|
6152
6254
|
# \\u escape, or as the UTF-8 bytes of a Unicode space. Compared byte for byte.
|
|
6153
6255
|
HC_LEAD='[[:space:]]|\\\\[nrtf]|\\\\u(000[9a-d]|0020|00a0|1680|200[0-9a]|202[89f]|205f|3000|feff)'
|
|
6154
6256
|
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'
|
|
6155
|
-
|
|
6257
|
+
# The pace phrases, matched whole after an optional "please" (the verb's own
|
|
6258
|
+
# reading is the same set or narrower): a number of agents, the most, faster, a
|
|
6259
|
+
# vague amount it answers with an ask for a number, or a free share.
|
|
6260
|
+
HC_PACE='((run|use|start|go)( +with| +at)? +)?(up +to +|the +)?(faster|more +agents|(-?[0-9]+(\\.[0-9]+)?|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve|thirteen|fourteen|fifteen|sixteen|seventeen|eighteen|nineteen|twenty) +agents?|(max|maximum|full|top) +(pace|speed)|(max|maximum)( +possible)?( +number +of)? +agents|as +many +agents +as +(possible|you +can)|(lots +of|a +lot +of|loads +of|plenty +of|many|some|several|a +few|a +bunch +of) +agents)( +(at +once|at +a +time|in +parallel|concurrently|simultaneously|together))?|keep +[0-9]{1,3}% +free|use +up +to +[0-9]{1,3}%'
|
|
6261
|
+
if ! printf '%s' "$HC_INPUT" | LC_ALL=C grep -Eiq "\\"prompt\\"[[:space:]]*:[[:space:]]*\\"($HC_LEAD)*((approve|send back|accept|question|carry|drop|reject|autonomy|decline) |allow( +[0-9]{1,4})?($HC_LEAD)*\\"|(keep going|stop|stop here|limit it to [0-9]{1,9}(\\.[0-9]{1,2})? (times|million|billion|million tokens|billion tokens)|limit it to [0-9]{1,3}% of the week)($HC_LEAD)*\\"|(please,? +)?($HC_PACE)(,? +please)?[.!]*($HC_LEAD)*\\")"; then
|
|
6156
6262
|
exit 0
|
|
6157
6263
|
fi
|
|
6158
6264
|
|
|
@@ -6238,12 +6344,12 @@ function isTrackedAndClean(targetRepo, relPath) {
|
|
|
6238
6344
|
return true;
|
|
6239
6345
|
}
|
|
6240
6346
|
function decideRetiredRemoval(targetRepo, rel, headerLineIndex, knownHeaderHashes) {
|
|
6241
|
-
const path =
|
|
6242
|
-
if (!
|
|
6347
|
+
const path = join7(targetRepo, rel);
|
|
6348
|
+
if (!existsSync5(path))
|
|
6243
6349
|
return "absent";
|
|
6244
6350
|
let raw;
|
|
6245
6351
|
try {
|
|
6246
|
-
raw =
|
|
6352
|
+
raw = readFileSync6(path, "utf-8");
|
|
6247
6353
|
} catch {
|
|
6248
6354
|
return "failed";
|
|
6249
6355
|
}
|
|
@@ -6257,7 +6363,7 @@ function decideRetiredRemoval(targetRepo, rel, headerLineIndex, knownHeaderHashe
|
|
|
6257
6363
|
}
|
|
6258
6364
|
function removePlannedRetiredFile(targetRepo, rel) {
|
|
6259
6365
|
try {
|
|
6260
|
-
rmSync2(
|
|
6366
|
+
rmSync2(join7(targetRepo, rel));
|
|
6261
6367
|
} catch {
|
|
6262
6368
|
return "failed";
|
|
6263
6369
|
}
|
|
@@ -6278,7 +6384,7 @@ var MCP_HEADERS_HELPER_COMMAND = `node -e "${nodeFloorProgram([
|
|
|
6278
6384
|
`const u=p.dirname(d);`,
|
|
6279
6385
|
`if(u===d)break;`,
|
|
6280
6386
|
`d=u}`,
|
|
6281
|
-
`console.error('halfcycle: no ${VENDORED_BIN_REL} at or above '+process.cwd()+'; run
|
|
6387
|
+
`console.error('halfcycle: no ${VENDORED_BIN_REL} at or above '+process.cwd()+'; run ${ownBareCommand()} in the project folder');`,
|
|
6282
6388
|
`process.exit(1)`
|
|
6283
6389
|
].join(""))}"`;
|
|
6284
6390
|
var CODEX_ADDRESS_PATTERN = /^https?:\/\/[A-Za-z0-9.-]+(:[0-9]{1,5})?(\/[A-Za-z0-9._-]+)*\/mcp$/;
|
|
@@ -6302,7 +6408,7 @@ function codexMcpHeadersHelperCommand(address) {
|
|
|
6302
6408
|
`if(u===d)break;`,
|
|
6303
6409
|
`d=u}`,
|
|
6304
6410
|
`if(b===''){`,
|
|
6305
|
-
`console.error('halfcycle: no ${VENDORED_BIN_REL} at or above '+process.cwd()+'; run
|
|
6411
|
+
`console.error('halfcycle: no ${VENDORED_BIN_REL} at or above '+process.cwd()+'; run ${ownBareCommand()} in the project folder');`,
|
|
6306
6412
|
`n()}else{`,
|
|
6307
6413
|
`const r=c.spawnSync(process.execPath,[b,'mcp-headers','--url','${address}'],{stdio:['ignore','pipe','inherit']});`,
|
|
6308
6414
|
`if(r.status===0&&r.stdout&&r.stdout.length){o=1;process.stdout.write(r.stdout)}else{n()}}`,
|
|
@@ -6628,9 +6734,9 @@ function gitignoreCovers(content) {
|
|
|
6628
6734
|
return REQUIRED_GITIGNORE_ENTRIES.every((entry) => lines.has(entry));
|
|
6629
6735
|
}
|
|
6630
6736
|
function planGitignore(targetRepo) {
|
|
6631
|
-
const gitignorePath =
|
|
6737
|
+
const gitignorePath = join7(targetRepo, GITIGNORE_REL);
|
|
6632
6738
|
const header = GITIGNORE_HEADER;
|
|
6633
|
-
const before =
|
|
6739
|
+
const before = existsSync5(gitignorePath) ? readFileSync6(gitignorePath, "utf-8") : null;
|
|
6634
6740
|
if (before === null) {
|
|
6635
6741
|
return { before, after: [header, ...REQUIRED_GITIGNORE_ENTRIES].join("\n") + "\n", outcome: "written" };
|
|
6636
6742
|
}
|
|
@@ -6646,7 +6752,7 @@ function putPlannedGitignore(targetRepo, planned) {
|
|
|
6646
6752
|
if (planned.outcome !== "written" || planned.after === null)
|
|
6647
6753
|
return planned.outcome;
|
|
6648
6754
|
try {
|
|
6649
|
-
writeFileSync4(
|
|
6755
|
+
writeFileSync4(join7(targetRepo, GITIGNORE_REL), planned.after, "utf-8");
|
|
6650
6756
|
return "written";
|
|
6651
6757
|
} catch {
|
|
6652
6758
|
return "failed";
|
|
@@ -6655,7 +6761,7 @@ function putPlannedGitignore(targetRepo, planned) {
|
|
|
6655
6761
|
var GITATTRIBUTES_HEADER = "# Halfcycle \u2014 generated scripts keep LF line endings";
|
|
6656
6762
|
function readTextOrNull(path) {
|
|
6657
6763
|
try {
|
|
6658
|
-
return
|
|
6764
|
+
return readFileSync6(path, "utf-8");
|
|
6659
6765
|
} catch {
|
|
6660
6766
|
return null;
|
|
6661
6767
|
}
|
|
@@ -6665,12 +6771,12 @@ function lfRules(paths) {
|
|
|
6665
6771
|
}
|
|
6666
6772
|
var REQUIRED_GITATTRIBUTES_RULES = lfRules(Object.keys(OWNED_GENERATED_HEADERS));
|
|
6667
6773
|
function planGitattributes(targetRepo) {
|
|
6668
|
-
const path =
|
|
6774
|
+
const path = join7(targetRepo, GITATTRIBUTES_REL);
|
|
6669
6775
|
const rules = REQUIRED_GITATTRIBUTES_RULES;
|
|
6670
6776
|
const before = readTextOrNull(path);
|
|
6671
6777
|
if (lstatOrNull(path)?.isSymbolicLink() === true)
|
|
6672
6778
|
return { before, after: before, outcome: "failed" };
|
|
6673
|
-
if (!
|
|
6779
|
+
if (!existsSync5(path)) {
|
|
6674
6780
|
return { before, after: [GITATTRIBUTES_HEADER, ...rules].join("\n") + "\n", outcome: "written" };
|
|
6675
6781
|
}
|
|
6676
6782
|
if (before === null)
|
|
@@ -6688,7 +6794,7 @@ function putPlannedGitattributes(targetRepo, planned) {
|
|
|
6688
6794
|
if (planned.outcome !== "written" || planned.after === null)
|
|
6689
6795
|
return planned.outcome;
|
|
6690
6796
|
try {
|
|
6691
|
-
writeAllowlisted(
|
|
6797
|
+
writeAllowlisted(join7(targetRepo, GITATTRIBUTES_REL), targetRepo, planned.after, []);
|
|
6692
6798
|
return "written";
|
|
6693
6799
|
} catch {
|
|
6694
6800
|
return "failed";
|
|
@@ -6707,12 +6813,12 @@ function previousPinForEngagement(targetRepoRoot, engagementId) {
|
|
|
6707
6813
|
function nonEmpty(value) {
|
|
6708
6814
|
return typeof value === "string" && value.trim() !== "" ? value : void 0;
|
|
6709
6815
|
}
|
|
6710
|
-
function planBundlePin(targetRepoRoot, version, engagementId, engagementType, createdByAccountId, methodVersion) {
|
|
6816
|
+
function planBundlePin(targetRepoRoot, version, engagementId, engagementType, createdByAccountId, methodVersion, serviceUrl) {
|
|
6711
6817
|
const previous = previousPinForEngagement(targetRepoRoot, engagementId);
|
|
6712
6818
|
const carried = pinCreatedByAccountId(previous) ?? nonEmpty(createdByAccountId);
|
|
6713
6819
|
const carriedMethodVersion = nonEmpty(methodVersion) ?? nonEmpty(previous?.methodVersion);
|
|
6714
6820
|
const installedAt = nonEmpty(previous?.installedAt) ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
6715
|
-
const projectPage = projectPageUrl(engagementId);
|
|
6821
|
+
const projectPage = projectPageUrl(engagementId, serviceUrl);
|
|
6716
6822
|
const pin = {
|
|
6717
6823
|
version,
|
|
6718
6824
|
engagementId,
|
|
@@ -6722,7 +6828,7 @@ function planBundlePin(targetRepoRoot, version, engagementId, engagementType, cr
|
|
|
6722
6828
|
...carriedMethodVersion !== void 0 ? { methodVersion: carriedMethodVersion } : {},
|
|
6723
6829
|
...projectPage !== void 0 ? { projectPage } : {}
|
|
6724
6830
|
};
|
|
6725
|
-
const current = readFileOrNull(
|
|
6831
|
+
const current = readFileOrNull(join7(targetRepoRoot, BUNDLE_PIN_REL));
|
|
6726
6832
|
const text = inLineEndingOf(current, JSON.stringify(pin, null, 2) + "\n");
|
|
6727
6833
|
return {
|
|
6728
6834
|
rel: BUNDLE_PIN_REL,
|
|
@@ -6750,10 +6856,10 @@ function planBundlePinPreview(targetRepoRoot, version, home) {
|
|
|
6750
6856
|
if (pinned === null || pinned.engagementId !== existing.engagementId || pinned.credential === void 0) {
|
|
6751
6857
|
return null;
|
|
6752
6858
|
}
|
|
6753
|
-
return planBundlePin(targetRepoRoot, version, existing.engagementId, existing.engagementType, void 0, void 0);
|
|
6859
|
+
return planBundlePin(targetRepoRoot, version, existing.engagementId, existing.engagementType, void 0, void 0, pinned.credential.serviceUrl);
|
|
6754
6860
|
}
|
|
6755
6861
|
function writeBundlePin(targetRepoRoot, planned, writtenPaths) {
|
|
6756
|
-
const pinPath =
|
|
6862
|
+
const pinPath = join7(targetRepoRoot, BUNDLE_PIN_REL);
|
|
6757
6863
|
writeAllowlisted(pinPath, targetRepoRoot, planned.text, writtenPaths);
|
|
6758
6864
|
}
|
|
6759
6865
|
function renderCrewRoster() {
|
|
@@ -6768,10 +6874,10 @@ function pinCreatedByAccountId(pin) {
|
|
|
6768
6874
|
return nonEmpty(pin?.createdByAccountId) ?? nonEmpty(pin?.accountId);
|
|
6769
6875
|
}
|
|
6770
6876
|
function readBundlePin(targetRepoRoot) {
|
|
6771
|
-
const pinPath =
|
|
6772
|
-
if (!
|
|
6877
|
+
const pinPath = join7(targetRepoRoot, BUNDLE_PIN_REL);
|
|
6878
|
+
if (!existsSync5(pinPath))
|
|
6773
6879
|
return null;
|
|
6774
|
-
return JSON.parse(
|
|
6880
|
+
return JSON.parse(readFileSync6(pinPath, "utf-8"));
|
|
6775
6881
|
}
|
|
6776
6882
|
function readPinnedEngagement(targetRepoRoot, home) {
|
|
6777
6883
|
const pin = readBundlePin(targetRepoRoot);
|
|
@@ -6794,7 +6900,7 @@ function readPinnedEngagement(targetRepoRoot, home) {
|
|
|
6794
6900
|
}
|
|
6795
6901
|
function readLegacyEnvLocal(targetRepoRoot) {
|
|
6796
6902
|
try {
|
|
6797
|
-
return parseEnvText(
|
|
6903
|
+
return parseEnvText(readFileSync6(join7(targetRepoRoot, LEGACY_ENV_LOCAL_REL), "utf-8"));
|
|
6798
6904
|
} catch {
|
|
6799
6905
|
return null;
|
|
6800
6906
|
}
|
|
@@ -6843,7 +6949,7 @@ function writeEngagementCredential(credential, engagementId, home) {
|
|
|
6843
6949
|
function planEngagementCredential(credential, engagementId, home) {
|
|
6844
6950
|
let existing;
|
|
6845
6951
|
try {
|
|
6846
|
-
existing =
|
|
6952
|
+
existing = readFileSync6(engagementEnvPath(engagementId, home), "utf-8");
|
|
6847
6953
|
} catch {
|
|
6848
6954
|
existing = null;
|
|
6849
6955
|
}
|
|
@@ -6886,12 +6992,12 @@ function engagementCredentialValues(credential, engagementId) {
|
|
|
6886
6992
|
}
|
|
6887
6993
|
var LEGACY_ENV_LOCAL_REL = ".env.local";
|
|
6888
6994
|
function migrateLegacyEnvLocal(targetRepo, stored) {
|
|
6889
|
-
const path =
|
|
6890
|
-
if (!
|
|
6995
|
+
const path = join7(targetRepo, LEGACY_ENV_LOCAL_REL);
|
|
6996
|
+
if (!existsSync5(path))
|
|
6891
6997
|
return { outcome: "absent", unbacked: [], missingForAdoption: [] };
|
|
6892
6998
|
let raw;
|
|
6893
6999
|
try {
|
|
6894
|
-
raw =
|
|
7000
|
+
raw = readFileSync6(path, "utf-8");
|
|
6895
7001
|
} catch {
|
|
6896
7002
|
return { outcome: "failed", unbacked: [], missingForAdoption: [] };
|
|
6897
7003
|
}
|
|
@@ -6917,7 +7023,7 @@ function migrateLegacyEnvLocal(targetRepo, stored) {
|
|
|
6917
7023
|
return { outcome: "stripped", unbacked: [], missingForAdoption: [], keptText: kept.join("\n") };
|
|
6918
7024
|
}
|
|
6919
7025
|
function applyLegacyMigration(targetRepo, decided) {
|
|
6920
|
-
const path =
|
|
7026
|
+
const path = join7(targetRepo, LEGACY_ENV_LOCAL_REL);
|
|
6921
7027
|
try {
|
|
6922
7028
|
if (decided.outcome === "removed") {
|
|
6923
7029
|
rmSync2(path);
|
|
@@ -6943,23 +7049,23 @@ function planCodexSkills(targetRepo, previous, writeSet) {
|
|
|
6943
7049
|
const manifest = readPluginManifest();
|
|
6944
7050
|
const recordedFiles = new Map((previous?.files ?? []).filter((file) => writeSet.files.has(file.path)).map((file) => [file.path, file]));
|
|
6945
7051
|
return (manifest.commands ?? []).map((cmd) => {
|
|
6946
|
-
const srcFile =
|
|
6947
|
-
if (!
|
|
7052
|
+
const srcFile = join7(BUNDLE_ROOT, ".claude-plugin", cmd.path);
|
|
7053
|
+
if (!existsSync5(srcFile)) {
|
|
6948
7054
|
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.`);
|
|
6949
7055
|
}
|
|
6950
7056
|
const rel = skillRel(cmd.name);
|
|
6951
|
-
const content = codexSkillContent(cmd.name,
|
|
7057
|
+
const content = codexSkillContent(cmd.name, commandFileText(readFileSync6(srcFile, "utf-8"), ownVersion()));
|
|
6952
7058
|
const recorded = recordedFiles.get(rel);
|
|
6953
|
-
const planned = decideCollisionSafe(
|
|
7059
|
+
const planned = decideCollisionSafe(join7(targetRepo, rel), targetRepo, content, (current) => bytesAreOurs(rel, current, recorded, writeSet));
|
|
6954
7060
|
return { ...planned, editedCollided: planned.outcome === "collided" && recorded !== void 0 };
|
|
6955
7061
|
});
|
|
6956
7062
|
}
|
|
6957
7063
|
function planVendoredBinary(targetRepo) {
|
|
6958
|
-
return decideOwned(
|
|
7064
|
+
return decideOwned(join7(targetRepo, ".halfcycle", "bin", "bin.bundle.mjs"), targetRepo, readFileSync6(resolveVendoredBinary(), "utf-8"));
|
|
6959
7065
|
}
|
|
6960
7066
|
function planSettings(targetRepo, previous, home, env) {
|
|
6961
|
-
const settingsPath =
|
|
6962
|
-
const preexisted =
|
|
7067
|
+
const settingsPath = join7(targetRepo, SETTINGS_REL);
|
|
7068
|
+
const preexisted = existsSync5(settingsPath);
|
|
6963
7069
|
const generated = JSON.parse(generateSettingsJson());
|
|
6964
7070
|
let existing;
|
|
6965
7071
|
try {
|
|
@@ -6969,7 +7075,7 @@ function planSettings(targetRepo, previous, home, env) {
|
|
|
6969
7075
|
}
|
|
6970
7076
|
const retiredOurs = retiredRulesThisInstallAdded(previous);
|
|
6971
7077
|
const statusLine = statusLineSetElsewhere(targetRepo, home, env) ? void 0 : HALFCYCLE_STATUS_LINE;
|
|
6972
|
-
const merged = mergeSettings(existing, generated, retiredOurs, statusLine);
|
|
7078
|
+
const merged = mergeSettings(existing, generated, retiredOurs, statusLine, HALFCYCLE_ATTRIBUTION);
|
|
6973
7079
|
const current = readFileOrNull(settingsPath);
|
|
6974
7080
|
const text = inLineEndingOf(current, JSON.stringify(merged, null, 2) + "\n");
|
|
6975
7081
|
const file = {
|
|
@@ -6983,11 +7089,11 @@ function planSettings(targetRepo, previous, home, env) {
|
|
|
6983
7089
|
return { file, preexisted, existing, generated, merged, retiredOurs };
|
|
6984
7090
|
}
|
|
6985
7091
|
function planHookScripts(targetRepo) {
|
|
6986
|
-
return hookScriptsThisVersionWrites().map(([rel, content]) => decideOwnedGenerated(
|
|
7092
|
+
return hookScriptsThisVersionWrites().map(([rel, content]) => decideOwnedGenerated(join7(targetRepo, rel), targetRepo, content, OWNED_GENERATED_HEADERS[rel]));
|
|
6987
7093
|
}
|
|
6988
7094
|
function planMcpRegistration(targetRepo, credential) {
|
|
6989
|
-
const mcpPath =
|
|
6990
|
-
const existing =
|
|
7095
|
+
const mcpPath = join7(targetRepo, MCP_REGISTRATION_REL);
|
|
7096
|
+
const existing = existsSync5(mcpPath) ? readFileSync6(mcpPath, "utf-8") : null;
|
|
6991
7097
|
if (credential === void 0)
|
|
6992
7098
|
return { kind: "none", existing };
|
|
6993
7099
|
if (credential === "pending")
|
|
@@ -6999,11 +7105,11 @@ function planMcpRegistration(targetRepo, credential) {
|
|
|
6999
7105
|
function putPlannedMcp(targetRepo, planned) {
|
|
7000
7106
|
if (planned.kind !== "write" || planned.outcome === "skipped")
|
|
7001
7107
|
return;
|
|
7002
|
-
writeAllowlisted(
|
|
7108
|
+
writeAllowlisted(join7(targetRepo, MCP_REGISTRATION_REL), targetRepo, planned.text, []);
|
|
7003
7109
|
}
|
|
7004
7110
|
function fileInFolderPlace(root, rel) {
|
|
7005
7111
|
for (let dir = posix2.dirname(rel); dir !== "."; dir = posix2.dirname(dir)) {
|
|
7006
|
-
const found = lstatOrNull(
|
|
7112
|
+
const found = lstatOrNull(join7(root, dir));
|
|
7007
7113
|
if (found !== null && !found.isDirectory())
|
|
7008
7114
|
return true;
|
|
7009
7115
|
}
|
|
@@ -7012,7 +7118,7 @@ function fileInFolderPlace(root, rel) {
|
|
|
7012
7118
|
function symlinkOnPath(root, rel) {
|
|
7013
7119
|
let current = root;
|
|
7014
7120
|
for (const segment of rel.split("/")) {
|
|
7015
|
-
current =
|
|
7121
|
+
current = join7(current, segment);
|
|
7016
7122
|
try {
|
|
7017
7123
|
if (lstatSync(current).isSymbolicLink())
|
|
7018
7124
|
return true;
|
|
@@ -7033,14 +7139,14 @@ function planCodexHooks(targetRepo) {
|
|
|
7033
7139
|
});
|
|
7034
7140
|
if (symlinkOnPath(targetRepo, CODEX_HOOKS_REL2))
|
|
7035
7141
|
return leave("link", null);
|
|
7036
|
-
const folder = lstatOrNull(
|
|
7142
|
+
const folder = lstatOrNull(join7(targetRepo, posix2.dirname(CODEX_HOOKS_REL2)));
|
|
7037
7143
|
if (folder !== null && !folder.isDirectory())
|
|
7038
7144
|
return leave("unreadable", null);
|
|
7039
|
-
const path =
|
|
7145
|
+
const path = join7(targetRepo, CODEX_HOOKS_REL2);
|
|
7040
7146
|
let before = null;
|
|
7041
7147
|
if (lstatOrNull(path) !== null) {
|
|
7042
7148
|
try {
|
|
7043
|
-
before =
|
|
7149
|
+
before = readFileSync6(path, "utf-8");
|
|
7044
7150
|
} catch {
|
|
7045
7151
|
return leave("unreadable", null);
|
|
7046
7152
|
}
|
|
@@ -7074,13 +7180,13 @@ function planCodexHooks(targetRepo) {
|
|
|
7074
7180
|
function putPlannedCodexHooks(targetRepo, planned) {
|
|
7075
7181
|
if (planned.outcome !== "written" || planned.text === null)
|
|
7076
7182
|
return;
|
|
7077
|
-
writeAllowlisted(
|
|
7183
|
+
writeAllowlisted(join7(targetRepo, CODEX_HOOKS_REL2), targetRepo, planned.text, []);
|
|
7078
7184
|
}
|
|
7079
7185
|
function readCodexConfig(targetRepo) {
|
|
7080
|
-
const folder = lstatOrNull(
|
|
7186
|
+
const folder = lstatOrNull(join7(targetRepo, posix2.dirname(CODEX_CONFIG_REL2)));
|
|
7081
7187
|
if (folder !== null && !folder.isDirectory())
|
|
7082
7188
|
return "unreadable";
|
|
7083
|
-
const path =
|
|
7189
|
+
const path = join7(targetRepo, CODEX_CONFIG_REL2);
|
|
7084
7190
|
const stat = lstatOrNull(path);
|
|
7085
7191
|
if (stat === null)
|
|
7086
7192
|
return { text: null };
|
|
@@ -7088,7 +7194,7 @@ function readCodexConfig(targetRepo) {
|
|
|
7088
7194
|
return "unreadable";
|
|
7089
7195
|
let bytes;
|
|
7090
7196
|
try {
|
|
7091
|
-
bytes =
|
|
7197
|
+
bytes = readFileSync6(path);
|
|
7092
7198
|
} catch {
|
|
7093
7199
|
return "unreadable";
|
|
7094
7200
|
}
|
|
@@ -7134,10 +7240,10 @@ function planCodexConfig(targetRepo, credential) {
|
|
|
7134
7240
|
function putPlannedCodexConfig(targetRepo, planned) {
|
|
7135
7241
|
if (planned.outcome !== "written" || planned.text === null)
|
|
7136
7242
|
return;
|
|
7137
|
-
writeAllowlisted(
|
|
7243
|
+
writeAllowlisted(join7(targetRepo, CODEX_CONFIG_REL2), targetRepo, planned.text, []);
|
|
7138
7244
|
}
|
|
7139
7245
|
function planCrewRoster(targetRepo) {
|
|
7140
|
-
return decideOwned(
|
|
7246
|
+
return decideOwned(join7(targetRepo, CREW_ROSTER_REL), targetRepo, renderCrewRoster());
|
|
7141
7247
|
}
|
|
7142
7248
|
function planCrewProfiles(targetRepo, previous, writeSet) {
|
|
7143
7249
|
const recordedFiles = new Map((previous?.files ?? []).filter((file) => writeSet.files.has(file.path)).map((file) => [file.path, file]));
|
|
@@ -7152,7 +7258,7 @@ function planCrewProfiles(targetRepo, previous, writeSet) {
|
|
|
7152
7258
|
});
|
|
7153
7259
|
}
|
|
7154
7260
|
function planInstallRecord(targetRepo, text) {
|
|
7155
|
-
return decideOwned(
|
|
7261
|
+
return decideOwned(join7(targetRepo, INSTALL_MANIFEST_REL), targetRepo, text);
|
|
7156
7262
|
}
|
|
7157
7263
|
function accountInstall(steps, actual) {
|
|
7158
7264
|
const { targetRepo, writeSet, pin } = steps;
|
|
@@ -7526,8 +7632,8 @@ async function requestEngagementValues(method, url, requestBody, credential, sha
|
|
|
7526
7632
|
}
|
|
7527
7633
|
|
|
7528
7634
|
// dist/setup/manifest.js
|
|
7529
|
-
import { existsSync as
|
|
7530
|
-
import { dirname as
|
|
7635
|
+
import { existsSync as existsSync6, lstatSync as lstatSync2, mkdirSync as mkdirSync5, readFileSync as readFileSync7, statSync, writeFileSync as writeFileSync5 } from "node:fs";
|
|
7636
|
+
import { dirname as dirname5, join as join8, posix as posix3 } from "node:path";
|
|
7531
7637
|
var DECLARED_ROOT_FILES = ["CLAUDE.md", "AGENTS.md", "ENGAGEMENT.md"];
|
|
7532
7638
|
var DECLARED_DIR_PREFIX = "docs";
|
|
7533
7639
|
var RESERVED_METHOD_PATH = "docs/method";
|
|
@@ -7612,7 +7718,7 @@ function firstSymlinkOnPath(targetRepo, path) {
|
|
|
7612
7718
|
const segments = path.split("/").filter((s) => s.length > 0);
|
|
7613
7719
|
let walked = targetRepo;
|
|
7614
7720
|
for (let i = 0; i < segments.length; i++) {
|
|
7615
|
-
walked =
|
|
7721
|
+
walked = join8(walked, segments[i]);
|
|
7616
7722
|
try {
|
|
7617
7723
|
if (lstatSync2(walked).isSymbolicLink())
|
|
7618
7724
|
return segments.slice(0, i + 1).join("/");
|
|
@@ -7630,30 +7736,30 @@ function symlinkEntry(path, link, isDirectoryRow) {
|
|
|
7630
7736
|
};
|
|
7631
7737
|
}
|
|
7632
7738
|
function applyFileRow(targetRepo, path, content) {
|
|
7633
|
-
const abs =
|
|
7739
|
+
const abs = join8(targetRepo, path);
|
|
7634
7740
|
const link = firstSymlinkOnPath(targetRepo, path);
|
|
7635
7741
|
if (link !== null)
|
|
7636
7742
|
return symlinkEntry(path, link, false);
|
|
7637
|
-
if (
|
|
7638
|
-
const current =
|
|
7743
|
+
if (existsSync6(abs)) {
|
|
7744
|
+
const current = readFileSync7(abs, "utf-8");
|
|
7639
7745
|
return current === content ? { path, outcome: "already-present", note: "already present and correct \u2014 nothing written" } : { path, outcome: "left-alone", note: "you already own this file \u2014 left exactly as it was" };
|
|
7640
7746
|
}
|
|
7641
|
-
mkdirSync5(
|
|
7747
|
+
mkdirSync5(dirname5(abs), { recursive: true });
|
|
7642
7748
|
writeFileSync5(abs, content, "utf-8");
|
|
7643
7749
|
return { path, outcome: "created", note: "created" };
|
|
7644
7750
|
}
|
|
7645
7751
|
function applySectionRow(targetRepo, path, heading, body) {
|
|
7646
|
-
const abs =
|
|
7752
|
+
const abs = join8(targetRepo, path);
|
|
7647
7753
|
const link = firstSymlinkOnPath(targetRepo, path);
|
|
7648
7754
|
if (link !== null)
|
|
7649
7755
|
return symlinkEntry(path, link, false);
|
|
7650
7756
|
const section = renderSection(heading, body);
|
|
7651
|
-
if (!
|
|
7652
|
-
mkdirSync5(
|
|
7757
|
+
if (!existsSync6(abs)) {
|
|
7758
|
+
mkdirSync5(dirname5(abs), { recursive: true });
|
|
7653
7759
|
writeFileSync5(abs, section, "utf-8");
|
|
7654
7760
|
return { path, outcome: "created", note: `created, with a "${heading}" section` };
|
|
7655
7761
|
}
|
|
7656
|
-
const existing =
|
|
7762
|
+
const existing = readFileSync7(abs, "utf-8");
|
|
7657
7763
|
if (hasSection(existing, heading)) {
|
|
7658
7764
|
return {
|
|
7659
7765
|
path,
|
|
@@ -7670,11 +7776,11 @@ function applySectionRow(targetRepo, path, heading, body) {
|
|
|
7670
7776
|
};
|
|
7671
7777
|
}
|
|
7672
7778
|
function applyDirectoryRow(targetRepo, path) {
|
|
7673
|
-
const abs =
|
|
7779
|
+
const abs = join8(targetRepo, path);
|
|
7674
7780
|
const link = firstSymlinkOnPath(targetRepo, path);
|
|
7675
7781
|
if (link !== null)
|
|
7676
7782
|
return symlinkEntry(path, link, true);
|
|
7677
|
-
if (
|
|
7783
|
+
if (existsSync6(abs)) {
|
|
7678
7784
|
const kind = statSync(abs).isDirectory() ? "directory" : "file";
|
|
7679
7785
|
return kind === "directory" ? { path: `${path}/`, outcome: "already-present", note: "already there \u2014 nothing written" } : { path, outcome: "left-alone", note: "you already own this path \u2014 left exactly as it was" };
|
|
7680
7786
|
}
|
|
@@ -7682,7 +7788,7 @@ function applyDirectoryRow(targetRepo, path) {
|
|
|
7682
7788
|
return { path: `${path}/`, outcome: "created", note: "created" };
|
|
7683
7789
|
}
|
|
7684
7790
|
function applySetupWrites(targetRepo, rows) {
|
|
7685
|
-
if (!
|
|
7791
|
+
if (!existsSync6(targetRepo)) {
|
|
7686
7792
|
throw new Error(`[halfcycle setup] The project directory does not exist: ${targetRepo}`);
|
|
7687
7793
|
}
|
|
7688
7794
|
assertNoConflictingDuplicates(rows);
|
|
@@ -7793,8 +7899,8 @@ function trackerHomeRow(home = FLAT_FILE_TRACKER_HOME) {
|
|
|
7793
7899
|
}
|
|
7794
7900
|
|
|
7795
7901
|
// dist/setup/discovery.js
|
|
7796
|
-
import { existsSync as
|
|
7797
|
-
import { join as
|
|
7902
|
+
import { existsSync as existsSync7, readFileSync as readFileSync8, readdirSync, statSync as statSync2 } from "node:fs";
|
|
7903
|
+
import { join as join9 } from "node:path";
|
|
7798
7904
|
function renderAnchor(anchor) {
|
|
7799
7905
|
return anchor.kind === "file" ? `${anchor.path}:${anchor.line}` : `$ ${anchor.command}`;
|
|
7800
7906
|
}
|
|
@@ -7818,8 +7924,8 @@ function lineOf(text, needle) {
|
|
|
7818
7924
|
return null;
|
|
7819
7925
|
}
|
|
7820
7926
|
function readOrNull(repo, rel) {
|
|
7821
|
-
const abs =
|
|
7822
|
-
return
|
|
7927
|
+
const abs = join9(repo, rel);
|
|
7928
|
+
return existsSync7(abs) && statSync2(abs).isFile() ? readFileSync8(abs, "utf-8") : null;
|
|
7823
7929
|
}
|
|
7824
7930
|
function anchoredIn(repo, rel, needle, value) {
|
|
7825
7931
|
const text = readOrNull(repo, rel);
|
|
@@ -7831,8 +7937,8 @@ function anchoredIn(repo, rel, needle, value) {
|
|
|
7831
7937
|
return { value, anchor: { kind: "file", path: rel, line } };
|
|
7832
7938
|
}
|
|
7833
7939
|
function presenceAnchor(repo, rel) {
|
|
7834
|
-
const abs =
|
|
7835
|
-
if (!
|
|
7940
|
+
const abs = join9(repo, rel);
|
|
7941
|
+
if (!existsSync7(abs))
|
|
7836
7942
|
return null;
|
|
7837
7943
|
return statSync2(abs).isDirectory() ? { kind: "command", command: `ls -d ${rel}`, output: `${rel}/` } : { kind: "file", path: rel, line: 1 };
|
|
7838
7944
|
}
|
|
@@ -7873,8 +7979,8 @@ function firstMatch(items, f) {
|
|
|
7873
7979
|
}
|
|
7874
7980
|
var CONTEXT_INDEX_FILES = ["AGENTS.md", "CLAUDE.md"];
|
|
7875
7981
|
function ciWorkflows(repo) {
|
|
7876
|
-
const dir =
|
|
7877
|
-
if (!
|
|
7982
|
+
const dir = join9(repo, ".github", "workflows");
|
|
7983
|
+
if (!existsSync7(dir) || !statSync2(dir).isDirectory())
|
|
7878
7984
|
return [];
|
|
7879
7985
|
return readdirSync(dir).filter((f) => f.endsWith(".yml") || f.endsWith(".yaml")).sort();
|
|
7880
7986
|
}
|
|
@@ -7882,21 +7988,21 @@ function existingLayerDocuments(repo) {
|
|
|
7882
7988
|
const found = [];
|
|
7883
7989
|
for (const paths of Object.values(LAYER_ARTEFACT_PATHS)) {
|
|
7884
7990
|
for (const p of paths ?? []) {
|
|
7885
|
-
if (
|
|
7991
|
+
if (existsSync7(join9(repo, p)))
|
|
7886
7992
|
found.push(p);
|
|
7887
7993
|
}
|
|
7888
7994
|
}
|
|
7889
7995
|
return found.sort();
|
|
7890
7996
|
}
|
|
7891
7997
|
function discoverRepository(targetRepo) {
|
|
7892
|
-
if (!
|
|
7998
|
+
if (!existsSync7(targetRepo)) {
|
|
7893
7999
|
throw new Error(`[halfcycle setup] The project directory does not exist: ${targetRepo}`);
|
|
7894
8000
|
}
|
|
7895
8001
|
const findings = [];
|
|
7896
8002
|
const questions = [];
|
|
7897
8003
|
settleOrAsk("project-language", firstMatch(MANIFESTS, ([file, needle, language]) => anchoredIn(targetRepo, file, needle, language)), "What is this project written in?", findings, questions);
|
|
7898
8004
|
settleOrAsk("package-manager", firstMatch(LOCKFILES, ([file, manager]) => anchoredByPresence(targetRepo, file, manager)), "Which package manager does this project use?", findings, questions);
|
|
7899
|
-
settleOrAsk("repo-structure", firstMatch(WORKSPACE_MARKERS, ([file, needle, shape]) => anchoredIn(targetRepo, file, needle, shape)) ?? (
|
|
8005
|
+
settleOrAsk("repo-structure", firstMatch(WORKSPACE_MARKERS, ([file, needle, shape]) => anchoredIn(targetRepo, file, needle, shape)) ?? (existsSync7(join9(targetRepo, "package.json")) || existsSync7(join9(targetRepo, "go.mod")) || existsSync7(join9(targetRepo, "pyproject.toml")) ? anchoredByPresence(targetRepo, "package.json", "a single-package repository") ?? anchoredByPresence(targetRepo, "go.mod", "a single-package repository") ?? anchoredByPresence(targetRepo, "pyproject.toml", "a single-package repository") : null), "Is this one repository for one product, or one of several?", findings, questions);
|
|
7900
8006
|
const workflows = ciWorkflows(targetRepo);
|
|
7901
8007
|
settleOrAsk("ci-strategy", workflows.length > 0 ? {
|
|
7902
8008
|
value: `GitHub Actions \u2014 ${workflows.length === 1 ? "one workflow" : `${workflows.length} workflows`}: ${workflows.join(", ")}`,
|
|
@@ -8566,11 +8672,11 @@ function renderBuildRecordV2Markdown(record2) {
|
|
|
8566
8672
|
}
|
|
8567
8673
|
|
|
8568
8674
|
// dist/build-record/write.js
|
|
8569
|
-
import { existsSync as
|
|
8570
|
-
import { basename, join as
|
|
8675
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync6, rmSync as rmSync4 } from "node:fs";
|
|
8676
|
+
import { basename, join as join10, resolve as resolve2, relative as relative2, isAbsolute as isAbsolute3 } from "node:path";
|
|
8571
8677
|
|
|
8572
8678
|
// dist/build-record/pair-io.js
|
|
8573
|
-
import { closeSync, openSync, readFileSync as
|
|
8679
|
+
import { closeSync, openSync, readFileSync as readFileSync9, renameSync as renameSync2, rmSync as rmSync3, writeSync } from "node:fs";
|
|
8574
8680
|
function createExclusiveOnDisk(path, data) {
|
|
8575
8681
|
const bytes = typeof data === "string" ? Buffer.from(data, "utf-8") : data;
|
|
8576
8682
|
const fd = openSync(path, "wx");
|
|
@@ -8601,7 +8707,7 @@ function isEexist(err) {
|
|
|
8601
8707
|
}
|
|
8602
8708
|
function readIfPresent(path) {
|
|
8603
8709
|
try {
|
|
8604
|
-
return
|
|
8710
|
+
return readFileSync9(path, "utf-8");
|
|
8605
8711
|
} catch {
|
|
8606
8712
|
return null;
|
|
8607
8713
|
}
|
|
@@ -8633,7 +8739,7 @@ function replaceOne(io, tmp, finalPath, text) {
|
|
|
8633
8739
|
}
|
|
8634
8740
|
|
|
8635
8741
|
// dist/build-record/write.js
|
|
8636
|
-
var BUILD_RECORD_DIR =
|
|
8742
|
+
var BUILD_RECORD_DIR = join10("docs", "build-records");
|
|
8637
8743
|
function assertInsideOutDir(outDir, candidate, identity) {
|
|
8638
8744
|
const rel = relative2(outDir, resolve2(candidate));
|
|
8639
8745
|
const isPlainFilename = rel !== "" && rel !== ".." && !isAbsolute3(rel) && rel === basename(rel);
|
|
@@ -8671,8 +8777,8 @@ function writeRecordPair(repoRoot, phase, rawRecord, record2, io = NODE_RECORD_P
|
|
|
8671
8777
|
}
|
|
8672
8778
|
const outDir = resolve2(repoRoot, BUILD_RECORD_DIR);
|
|
8673
8779
|
const segment = renderPhaseSegment(phase);
|
|
8674
|
-
const jsonPath =
|
|
8675
|
-
const mdPath =
|
|
8780
|
+
const jsonPath = join10(outDir, `${segment}.json`);
|
|
8781
|
+
const mdPath = join10(outDir, `${segment}.md`);
|
|
8676
8782
|
const jsonTmp = `${jsonPath}.tmp`;
|
|
8677
8783
|
const mdTmp = `${mdPath}.tmp`;
|
|
8678
8784
|
for (const candidate of [jsonPath, mdPath, jsonTmp, mdTmp]) {
|
|
@@ -8686,7 +8792,7 @@ function writeRecordPair(repoRoot, phase, rawRecord, record2, io = NODE_RECORD_P
|
|
|
8686
8792
|
}
|
|
8687
8793
|
const existingJson = readIfPresent(jsonPath);
|
|
8688
8794
|
if (existingJson === null) {
|
|
8689
|
-
return { kind: "in-the-way", path:
|
|
8795
|
+
return { kind: "in-the-way", path: existsSync8(jsonPath) ? jsonPath : mdPath };
|
|
8690
8796
|
}
|
|
8691
8797
|
if (existingJson === jsonText) {
|
|
8692
8798
|
replaceOne(io, mdTmp, mdPath, mdText);
|
|
@@ -8748,20 +8854,20 @@ function isMissingRoute(method, status, body) {
|
|
|
8748
8854
|
}
|
|
8749
8855
|
|
|
8750
8856
|
// dist/build-record/write-change.js
|
|
8751
|
-
import { basename as basename2, isAbsolute as isAbsolute4, join as
|
|
8752
|
-
var CHANGE_RECORD_DIR =
|
|
8857
|
+
import { basename as basename2, isAbsolute as isAbsolute4, join as join11, relative as relative3, resolve as resolve3 } from "node:path";
|
|
8858
|
+
var CHANGE_RECORD_DIR = join11("docs", "build-records", "changes");
|
|
8753
8859
|
|
|
8754
8860
|
// dist/change.js
|
|
8755
|
-
var CHANGE_USAGE = `${
|
|
8756
|
-
${
|
|
8757
|
-
${
|
|
8861
|
+
var CHANGE_USAGE = `${ownCommand()} change sort --request "<one line>" --touches <path>\u2026 [--contract <from>:<to>]\u2026 [--stored-data] [--standing-rule] [--new-part] [--repo <root>]
|
|
8862
|
+
${ownCommand()} change up <mini|full> (--change <id> | --phase <phase>) [--repo <root>]
|
|
8863
|
+
${ownCommand()} change done <id> [--repo <root>]`;
|
|
8758
8864
|
|
|
8759
8865
|
// dist/plan.js
|
|
8760
|
-
var PLAN_USAGE = `${
|
|
8866
|
+
var PLAN_USAGE = `${ownCommand()} plan <phase> [--show] [--repo <root>]`;
|
|
8761
8867
|
|
|
8762
8868
|
// dist/review.js
|
|
8763
|
-
var REVIEW_USAGE = `${
|
|
8764
|
-
var RV9_LINE = `halfcycle review: this project is not signed in on this computer. ${
|
|
8869
|
+
var REVIEW_USAGE = `${ownCommand()} review <phase> (--show | --answer <part> "<answer>") [--repo <root>]`;
|
|
8870
|
+
var RV9_LINE = `halfcycle review: this project is not signed in on this computer. ${hookRemedy(ownVersion())}`;
|
|
8765
8871
|
|
|
8766
8872
|
// dist/open-phase.js
|
|
8767
8873
|
var OPEN_FROM_SIZE_REFUSALS = Object.freeze({
|
|
@@ -8877,16 +8983,16 @@ async function obtainBuildRecord(credential, repoRoot, phase, io = NODE_RECORD_P
|
|
|
8877
8983
|
};
|
|
8878
8984
|
}
|
|
8879
8985
|
function refetchCommand(phase) {
|
|
8880
|
-
return `${
|
|
8986
|
+
return `${ownCommand()} build-record ${quoteForShell(phase)}`;
|
|
8881
8987
|
}
|
|
8882
8988
|
|
|
8883
8989
|
// dist/check-tasks.js
|
|
8884
|
-
var CHECK_TASKS_USAGE = `${
|
|
8990
|
+
var CHECK_TASKS_USAGE = `${ownCommand()} check-tasks <phase> [--with-task-list] [--repo <root>]`;
|
|
8885
8991
|
var MISSING_PLAN_LINE = `there is no ${TASK_PLAN_FILE} in this project yet. Run the task-graph step to write it, then run this again.`;
|
|
8886
8992
|
var SPEC_FOLDER = LAYER_ARTEFACT_PATHS.l4[0];
|
|
8887
8993
|
|
|
8888
8994
|
// dist/ready.js
|
|
8889
|
-
var READY_USAGE = `${
|
|
8995
|
+
var READY_USAGE = `${ownCommand()} ready <phase> [--cap <n>] [--repo <root>]`;
|
|
8890
8996
|
var STATES_TEXT = buildTaskStateSchema.options.join(", ");
|
|
8891
8997
|
var READY_LINES = {
|
|
8892
8998
|
start: (id, title) => `Ready to start: ${named(id, title)}`,
|