halfcycle 0.4.2 → 0.4.3

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "halfcycle",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "Halfcycle Method bundle — resolution-stub slash commands, remote method-delivery registration, and governance-hook wiring for a Halfcycle engagement repo. It ships NO worker role files: a project's roles are authored from that project's own recorded decisions at orchestration kickoff (FX-2, W3-F-27).",
5
5
  "commands": [
6
6
  {
@@ -17517,11 +17517,16 @@ function deleteMarker(repoRoot, sessionId) {
17517
17517
  }
17518
17518
  }
17519
17519
  }
17520
- var NO_REPO_NOTICE_SUFFIX = ".norepo";
17521
- function firstNoRepoNoticeThisSession(folder, sessionId) {
17520
+ var SESSION_NOTICE_SUFFIX = {
17521
+ "no-repository": ".norepo",
17522
+ "credential-rejected": ".credential",
17523
+ "credential-renewed-for-this-run": ".renewed"
17524
+ };
17525
+ function firstNoticeThisSession(notice, folder, sessionId) {
17522
17526
  if (!sessionId)
17523
17527
  return true;
17524
- const path = join6(markerDir(), `${MARKER_PREFIX}${repoHash(folder)}-${sessionId}${NO_REPO_NOTICE_SUFFIX}`);
17528
+ const suffix = SESSION_NOTICE_SUFFIX[notice];
17529
+ const path = join6(markerDir(), `${MARKER_PREFIX}${repoHash(folder)}-${sessionId}${suffix}`);
17525
17530
  try {
17526
17531
  writeFileSync3(path, "", { encoding: "utf-8", flag: "wx" });
17527
17532
  return true;
@@ -17529,6 +17534,15 @@ function firstNoRepoNoticeThisSession(folder, sessionId) {
17529
17534
  return err.code !== "EEXIST";
17530
17535
  }
17531
17536
  }
17537
+ function warnOnceThisSession(notice, folder, sessionId) {
17538
+ return (message) => {
17539
+ if (firstNoticeThisSession(notice, folder, sessionId))
17540
+ process.stderr.write(message);
17541
+ };
17542
+ }
17543
+ function firstNoRepoNoticeThisSession(folder, sessionId) {
17544
+ return firstNoticeThisSession("no-repository", folder, sessionId);
17545
+ }
17532
17546
  function readDedupe(repoRoot, sessionId) {
17533
17547
  const path = dedupePath(repoRoot, sessionId);
17534
17548
  if (!existsSync4(path))
@@ -17805,7 +17819,8 @@ async function evaluateEdit(reply, readSeat) {
17805
17819
  guardServiceUrl,
17806
17820
  token: guardServiceToken,
17807
17821
  engagementId: guardEngagementId,
17808
- request: buildEvaluationRequest(guardEngagementId, changeSet, context, seatConfiguration)
17822
+ request: buildEvaluationRequest(guardEngagementId, changeSet, context, seatConfiguration),
17823
+ warn: warnOnceThisSession("credential-renewed-for-this-run", repoRoot, input.session_id)
17809
17824
  });
17810
17825
  const clientResult = evaluation.result;
17811
17826
  if (!clientResult.ok) {
@@ -17820,6 +17835,8 @@ async function evaluateEdit(reply, readSeat) {
17820
17835
  credentialRefused: failure.kind === "credential"
17821
17836
  });
17822
17837
  if (failure.kind === "credential") {
17838
+ if (!firstNoticeThisSession("credential-rejected", repoRoot, input.session_id))
17839
+ return;
17823
17840
  emitCredentialRejected(
17824
17841
  failure.statusCode,
17825
17842
  failure.message,
@@ -17981,7 +17998,8 @@ async function evaluateSession(input, kind, reply, readSeat) {
17981
17998
  guardServiceUrl,
17982
17999
  token: guardServiceToken,
17983
18000
  engagementId: guardEngagementId,
17984
- request: buildEvaluationRequest(guardEngagementId, changeSet, context, seatConfiguration)
18001
+ request: buildEvaluationRequest(guardEngagementId, changeSet, context, seatConfiguration),
18002
+ warn: warnOnceThisSession("credential-renewed-for-this-run", repoRoot, sessionId)
17985
18003
  });
17986
18004
  const clientResult = evaluation.result;
17987
18005
  if (!clientResult.ok) {
@@ -17997,6 +18015,8 @@ async function evaluateSession(input, kind, reply, readSeat) {
17997
18015
  credentialRefused: failure.kind === "credential"
17998
18016
  });
17999
18017
  if (failure.kind === "credential") {
18018
+ if (!firstNoticeThisSession("credential-rejected", repoRoot, sessionId))
18019
+ return;
18000
18020
  emitCredentialRejected(
18001
18021
  failure.statusCode,
18002
18022
  failure.message,
@@ -18411,7 +18431,7 @@ async function refreshCredentialToken(opts) {
18411
18431
  return { token: refreshed.token };
18412
18432
  }
18413
18433
  var CHECKED_SOURCES = /* @__PURE__ */ new Set(["startup", "resume"]);
18414
- var CONNECTED_SENTENCE = `Halfcycle's sign-in works and its service answered. If Halfcycle's tools are missing, turn on ${MCP_SERVER_KEY} in /mcp; if /mcp does not list it, run "claude mcp reset-project-choices" in this folder and restart Claude Code.`;
18434
+ var CONNECTED_SENTENCE = `Halfcycle's sign-in works and its service answered. If its tools are missing, turn on ${MCP_SERVER_KEY} in /mcp; if it is not listed, run "claude mcp reset-project-choices" here and restart Claude Code.`;
18415
18435
  var INITIALIZE_BODY = JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize", params: {} });
18416
18436
  function registeredMcpUrl(projectRoot) {
18417
18437
  let parsed;
@@ -1 +1 @@
1
- {"version":3,"file":"claude-version-probe.d.ts","sourceRoot":"","sources":["../src/claude-version-probe.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAMH;0DAC0D;AAC1D,MAAM,MAAM,iBAAiB,GAAG,CAC9B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,OAAO,EAAE;IACP,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC;IACtD,QAAQ,CAAC,wBAAwB,CAAC,EAAE,OAAO,CAAC;CAC7C,KACE,MAAM,CAAC;AAEZ,MAAM,WAAW,sBAAsB;IACrC,oFAAoF;IACpF,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;IACpC,sFAAsF;IACtF,QAAQ,CAAC,IAAI,CAAC,EAAE,iBAAiB,CAAC;CACnC;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,GAAE,sBAA2B,GAAG,MAAM,GAAG,SAAS,CAqB/G;AAMD;kCACkC;AAClC,MAAM,MAAM,mBAAmB,GAAG,QAAQ,GAAG,KAAK,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEvE,eAAO,MAAM,wBAAwB,mCAAmC,CAAC;AAEzE,eAAO,MAAM,gBAAgB,EAAE,QAAQ,CAAC,MAAM,CAAC,mBAAmB,EAAE,MAAM,CAAC,CAK1E,CAAC;AAaF,MAAM,WAAW,uBAAuB;IACtC,iCAAiC;IACjC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACjC,sCAAsC;IACtC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;IACpC,iGAAiG;IACjG,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IAC5C;sDACkD;IAClD,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CAC9C;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,4BAA4B,CAAC,IAAI,GAAE,uBAA4B,GAAG,mBAAmB,GAAG,SAAS,CAwBhH"}
1
+ {"version":3,"file":"claude-version-probe.d.ts","sourceRoot":"","sources":["../src/claude-version-probe.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAMH;0DAC0D;AAC1D,MAAM,MAAM,iBAAiB,GAAG,CAC9B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,OAAO,EAAE;IACP,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC;IACtD,QAAQ,CAAC,wBAAwB,CAAC,EAAE,OAAO,CAAC;CAC7C,KACE,MAAM,CAAC;AAEZ,MAAM,WAAW,sBAAsB;IACrC,oFAAoF;IACpF,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;IACpC,sFAAsF;IACtF,QAAQ,CAAC,IAAI,CAAC,EAAE,iBAAiB,CAAC;CACnC;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,GAAE,sBAA2B,GAAG,MAAM,GAAG,SAAS,CAqB/G;AAMD;kCACkC;AAClC,MAAM,MAAM,mBAAmB,GAAG,QAAQ,GAAG,KAAK,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEvE,eAAO,MAAM,wBAAwB,mCAAmC,CAAC;AAEzE,eAAO,MAAM,gBAAgB,EAAE,QAAQ,CAAC,MAAM,CAAC,mBAAmB,EAAE,MAAM,CAAC,CAK1E,CAAC;AAyBF,MAAM,WAAW,uBAAuB;IACtC,iCAAiC;IACjC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACjC,sCAAsC;IACtC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;IACpC,iGAAiG;IACjG,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IAC5C;sDACkD;IAClD,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CAC9C;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,4BAA4B,CAAC,IAAI,GAAE,uBAA4B,GAAG,mBAAmB,GAAG,SAAS,CAwBhH"}
package/dist/cli.js CHANGED
@@ -1942,8 +1942,20 @@ function projectPageUrl(engagementId) {
1942
1942
  }
1943
1943
 
1944
1944
  // dist/merge-settings.js
1945
+ function isHalfcycleCommand(hook, halfcycleCommands) {
1946
+ if (typeof hook !== "object" || hook === null)
1947
+ return false;
1948
+ const command = hook.command;
1949
+ return typeof command === "string" && halfcycleCommands.has(command);
1950
+ }
1945
1951
  function isHalfcycleEntry(entry, halfcycleCommands) {
1946
- return entry.hooks.some((h) => halfcycleCommands.has(h.command));
1952
+ return entry.hooks.some((h) => isHalfcycleCommand(h, halfcycleCommands));
1953
+ }
1954
+ function withoutHalfcycleCommands(entry, halfcycleCommands) {
1955
+ if (!isHalfcycleEntry(entry, halfcycleCommands))
1956
+ return entry;
1957
+ const remaining = entry.hooks.filter((h) => !isHalfcycleCommand(h, halfcycleCommands));
1958
+ return remaining.length > 0 ? { ...entry, hooks: remaining } : null;
1947
1959
  }
1948
1960
  function mergeSettings(existing, generated) {
1949
1961
  const merged = { ...existing };
@@ -1962,7 +1974,7 @@ function mergeSettings(existing, generated) {
1962
1974
  const mergedHooks = {};
1963
1975
  const events = /* @__PURE__ */ new Set([...Object.keys(existingHooks), ...Object.keys(generatedHooks)]);
1964
1976
  for (const event of events) {
1965
- const developerEntries = (existingHooks[event] ?? []).filter((entry) => !isHalfcycleEntry(entry, halfcycleCommands));
1977
+ const developerEntries = (existingHooks[event] ?? []).flatMap((entry) => withoutHalfcycleCommands(entry, halfcycleCommands) ?? []);
1966
1978
  const halfcycleEntries = generatedHooks[event] ?? [];
1967
1979
  mergedHooks[event] = [...developerEntries, ...halfcycleEntries];
1968
1980
  }
@@ -2130,6 +2142,7 @@ function closedWriteSet() {
2130
2142
  ownedDirs,
2131
2143
  hookCommands: new Set(HALFCYCLE_HOOK_COMMANDS),
2132
2144
  denyRuleSha256: new Set([...DENY_PATTERNS, ...RETIRED_DENY_PATTERNS].map((rule) => sha256Hex(rule))),
2145
+ settingsSchema: SETTINGS_SCHEMA,
2133
2146
  gitignoreLines: /* @__PURE__ */ new Set([GITIGNORE_HEADER, ...REQUIRED_GITIGNORE_ENTRIES, LEGACY_ENV_LOCAL_REL]),
2134
2147
  gitattributesLines: /* @__PURE__ */ new Set([
2135
2148
  GITATTRIBUTES_HEADER,
@@ -2318,6 +2331,8 @@ function copyManifestCommands(manifest, targetRepo, report, draft, writeSet) {
2318
2331
  record(report, outcome, rel);
2319
2332
  if (replacedExisting)
2320
2333
  report.replacedPaths.push(rel);
2334
+ if (outcome === "collided" && recorded !== void 0)
2335
+ report.editedCollidedPaths.push(rel);
2321
2336
  noteFile(draft, targetRepo, rel, outcome);
2322
2337
  }
2323
2338
  }
@@ -2441,6 +2456,7 @@ function settingsParseRefusal(targetRepo, err) {
2441
2456
  }
2442
2457
  return `${file} could not be read as JSON${where} \u2014 fix it and run "npx halfcycle" again.`;
2443
2458
  }
2459
+ var SETTINGS_SCHEMA = "https://json.schemastore.org/claude-code-settings.json";
2444
2460
  function generateSettingsJson() {
2445
2461
  const hooks = {};
2446
2462
  for (const { event, matcher, timeout, command } of HALFCYCLE_HOOK_WIRING) {
@@ -2450,7 +2466,7 @@ function generateSettingsJson() {
2450
2466
  });
2451
2467
  }
2452
2468
  const settings = {
2453
- $schema: "https://json.schemastore.org/claude-code-settings.json",
2469
+ $schema: SETTINGS_SCHEMA,
2454
2470
  permissions: {
2455
2471
  // deny ONLY. See DENY_PATTERNS — an allow list evaporates under bypass mode
2456
2472
  // and is the machine owner's grant to make, not ours.
@@ -2750,19 +2766,27 @@ function adoptBlockUnderHeader(before, header, claimable) {
2750
2766
  let underHeader = false;
2751
2767
  const lines = before.split("\n");
2752
2768
  for (const [i, line] of lines.entries()) {
2753
- if (line === header) {
2754
- if (i > 0 && lines[i - 1] === "")
2755
- claimed.push("");
2769
+ const key = lineKey(line);
2770
+ if (key === header) {
2771
+ if (i > 0 && lineKey(lines[i - 1]) === "")
2772
+ claimed.push(lines[i - 1]);
2756
2773
  claimed.push(line);
2757
2774
  underHeader = true;
2758
- } else if (line.trim() === "") {
2775
+ } else if (key.trim() === "") {
2759
2776
  underHeader = false;
2760
- } else if (underHeader && claimable.has(line)) {
2777
+ } else if (underHeader && claimable.has(key)) {
2761
2778
  claimed.push(line);
2762
2779
  }
2763
2780
  }
2764
2781
  return claimed.length > 0 ? claimed.join("\n") + "\n" : "";
2765
2782
  }
2783
+ function lineKey(line) {
2784
+ return line.endsWith("\r") ? line.slice(0, -1) : line;
2785
+ }
2786
+ function lineEndingOf(text) {
2787
+ const at = text.indexOf("\n");
2788
+ return at > 0 && text[at - 1] === "\r" ? "\r\n" : "\n";
2789
+ }
2766
2790
  function gitignoreCovers(content) {
2767
2791
  const lines = new Set(content.split("\n").map((l) => l.trim()));
2768
2792
  return REQUIRED_GITIGNORE_ENTRIES.every((entry) => lines.has(entry));
@@ -2781,9 +2805,8 @@ function reconcileGitignore(targetRepo) {
2781
2805
  return "skipped";
2782
2806
  const existingLines = new Set(existing.split("\n").map((l) => l.trim()));
2783
2807
  const toAdd = REQUIRED_GITIGNORE_ENTRIES.filter((entry) => !existingLines.has(entry));
2784
- const suffix = (existing.endsWith("\n") ? "" : "\n") + `
2785
- ${header}
2786
- ` + toAdd.join("\n") + "\n";
2808
+ const eol = lineEndingOf(existing);
2809
+ const suffix = (existing.endsWith("\n") ? "" : eol) + eol + [header, ...toAdd].join(eol) + eol;
2787
2810
  writeFileSync5(gitignorePath, existing + suffix, "utf-8");
2788
2811
  return "written";
2789
2812
  } catch {
@@ -2818,8 +2841,9 @@ function reconcileGitattributes(targetRepo) {
2818
2841
  const toAdd = rules.filter((rule) => !present.has(rule));
2819
2842
  if (toAdd.length === 0)
2820
2843
  return "skipped";
2821
- const separator = existing === "" ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
2822
- const suffix = separator + [GITATTRIBUTES_HEADER, ...toAdd].join("\n") + "\n";
2844
+ const eol = lineEndingOf(existing);
2845
+ const separator = existing === "" ? "" : existing.endsWith("\n") ? eol : eol + eol;
2846
+ const suffix = separator + [GITATTRIBUTES_HEADER, ...toAdd].join(eol) + eol;
2823
2847
  writeAllowlisted(path, targetRepo, existing + suffix, routed);
2824
2848
  return "written";
2825
2849
  } catch {
@@ -2839,9 +2863,9 @@ function previousPinForEngagement(targetRepoRoot, engagementId) {
2839
2863
  function nonEmpty(value) {
2840
2864
  return typeof value === "string" && value.trim() !== "" ? value : void 0;
2841
2865
  }
2842
- function writeBundlePin(targetRepoRoot, version, engagementId, engagementType, accountId, methodVersion, writtenPaths) {
2866
+ function writeBundlePin(targetRepoRoot, version, engagementId, engagementType, createdByAccountId, methodVersion, writtenPaths) {
2843
2867
  const previous = previousPinForEngagement(targetRepoRoot, engagementId);
2844
- const carried = nonEmpty(accountId) ?? nonEmpty(previous?.accountId);
2868
+ const carried = pinCreatedByAccountId(previous) ?? nonEmpty(createdByAccountId);
2845
2869
  const carriedMethodVersion = nonEmpty(methodVersion) ?? nonEmpty(previous?.methodVersion);
2846
2870
  const installedAt = nonEmpty(previous?.installedAt) ?? (/* @__PURE__ */ new Date()).toISOString();
2847
2871
  const projectPage = projectPageUrl(engagementId);
@@ -2850,7 +2874,7 @@ function writeBundlePin(targetRepoRoot, version, engagementId, engagementType, a
2850
2874
  engagementId,
2851
2875
  engagementType,
2852
2876
  installedAt,
2853
- ...carried !== void 0 ? { accountId: carried } : {},
2877
+ ...carried !== void 0 ? { createdByAccountId: carried } : {},
2854
2878
  ...carriedMethodVersion !== void 0 ? { methodVersion: carriedMethodVersion } : {},
2855
2879
  ...projectPage !== void 0 ? { projectPage } : {}
2856
2880
  };
@@ -2867,6 +2891,9 @@ function writeCrewRoster(targetRepoRoot, report) {
2867
2891
  const crewPath = join5(targetRepoRoot, CREW_ROSTER_REL);
2868
2892
  return recordOwned(report, crewPath, targetRepoRoot, rendered, CREW_ROSTER_REL);
2869
2893
  }
2894
+ function pinCreatedByAccountId(pin) {
2895
+ return nonEmpty(pin?.createdByAccountId) ?? nonEmpty(pin?.accountId);
2896
+ }
2870
2897
  function readBundlePin(targetRepoRoot) {
2871
2898
  const pinPath = join5(targetRepoRoot, BUNDLE_PIN_REL);
2872
2899
  if (!existsSync3(pinPath))
@@ -3008,7 +3035,7 @@ function migrateLegacyEnvLocal(targetRepo, stored) {
3008
3035
  }
3009
3036
  }
3010
3037
  async function install(options) {
3011
- const { targetRepo, engagementId, engagementType, credential, home, accountId } = options;
3038
+ const { targetRepo, engagementId, engagementType, credential, home, createdByAccountId } = options;
3012
3039
  assertEngagementId(engagementId);
3013
3040
  if (!existsSync3(targetRepo)) {
3014
3041
  throw new Error(`[halfcycle] Target repo does not exist: ${targetRepo}`);
@@ -3019,6 +3046,7 @@ async function install(options) {
3019
3046
  skippedPaths: [],
3020
3047
  mergedPaths: [],
3021
3048
  collidedPaths: [],
3049
+ editedCollidedPaths: [],
3022
3050
  replacedPaths: []
3023
3051
  };
3024
3052
  const writeSet = closedWriteSet();
@@ -3146,7 +3174,7 @@ async function install(options) {
3146
3174
  case "absent":
3147
3175
  break;
3148
3176
  }
3149
- writeBundlePin(targetRepo, manifest.version, engagementId, engagementType, accountId, credential?.methodVersion, report.writtenPaths);
3177
+ writeBundlePin(targetRepo, manifest.version, engagementId, engagementType, createdByAccountId, credential?.methodVersion, report.writtenPaths);
3150
3178
  noteFile(draft, targetRepo, BUNDLE_PIN_REL, "written");
3151
3179
  noteFile(draft, targetRepo, CREW_ROSTER_REL, writeCrewRoster(targetRepo, report));
3152
3180
  const scanResult = runBootstrapScan(targetRepo);
@@ -3166,6 +3194,7 @@ async function install(options) {
3166
3194
  skippedPaths: report.skippedPaths,
3167
3195
  mergedPaths: report.mergedPaths,
3168
3196
  collidedPaths: report.collidedPaths,
3197
+ editedCollidedPaths: report.editedCollidedPaths,
3169
3198
  replacedPaths: report.replacedPaths,
3170
3199
  bootstrapScanRan: scanResult.ran,
3171
3200
  legacyCredentialKept: migration.outcome === "kept" ? { keys: migration.unbacked, missing: migration.missingForAdoption } : null,
@@ -3606,7 +3635,7 @@ async function signIn(serviceUrl, deps = {}) {
3606
3635
  const url = loopbackVerificationUrl(started.verificationUrl, bind.target);
3607
3636
  const attempt = await openBrowser(url, env);
3608
3637
  if (attempt.opened) {
3609
- write(`[halfcycle] Opened your browser to finish signing in. Code: ${started.userCode}. If a browser did not open, go to: ${url}. This terminal is waiting.
3638
+ write(`[halfcycle] Opened your browser to finish signing in. Code: ${started.userCode}. If a browser did not open, go to: ${started.verificationUrl}. This terminal is waiting.
3610
3639
  `);
3611
3640
  const startedAt = now();
3612
3641
  const redirect = bind.waitForCallback({
@@ -4171,14 +4200,11 @@ async function askAccount(serviceUrl, credential, fetchImpl = fetch) {
4171
4200
  async function describeAccount(serviceUrl, credential, fetchImpl = fetch) {
4172
4201
  return (await askAccount(serviceUrl, credential, fetchImpl)).identity;
4173
4202
  }
4174
- function accountLabel(identity, storedAccountId, verbose2 = false, confirmed = true) {
4203
+ function accountLabel(identity, storedAccountId, verbose2 = false) {
4175
4204
  const accountId = identity?.accountId ?? storedAccountId;
4176
4205
  if (identity?.email !== void 0 && identity.email.trim() !== "") {
4177
4206
  return verbose2 && accountId !== void 0 ? `${identity.email} (account ${accountId})` : identity.email;
4178
4207
  }
4179
- if (!confirmed) {
4180
- return "the account this project is already set up with \u2014 this run could not reach the Halfcycle service to confirm who that is";
4181
- }
4182
4208
  if (accountId !== void 0)
4183
4209
  return `account ${accountId} (no email on record for it)`;
4184
4210
  return "the account this credential belongs to, which this service did not name";
@@ -4231,7 +4257,7 @@ function armScript(credential, act, label, plainLabel, home) {
4231
4257
  switch (credential.source) {
4232
4258
  case "browser-handoff":
4233
4259
  return {
4234
- preamble: `[halfcycle] Signed in as ${label}. This install will ${actClause(act)}.
4260
+ preamble: `[halfcycle] This install will ${actClause(act)}.
4235
4261
  `,
4236
4262
  asks: false,
4237
4263
  noTerminalNote: "",
@@ -4275,7 +4301,7 @@ function armScript(credential, act, label, plainLabel, home) {
4275
4301
  // this is printed in), and where to look for the answer once this output is gone.
4276
4302
  noTerminalNote: `[halfcycle] Nothing here to ask at (no terminal), so this is continuing as that account.
4277
4303
  [halfcycle] To run as a different one, set HALFCYCLE_TOKEN for that account, or remove ${accountStorePath(home)} and run this again with a terminal to sign in.
4278
- [halfcycle] When this install finishes, which account it used is recorded in .halfcycle/bundle.json, so it can be checked after this output is gone.
4304
+ [halfcycle] .halfcycle/bundle.json records the account that created this project, so that can be checked after this output is gone.
4279
4305
  `,
4280
4306
  switchOpening: `[halfcycle] Forgetting this machine's saved sign-in and signing you in through your browser.
4281
4307
  `,
@@ -4320,7 +4346,7 @@ async function confirmActingIdentity(serviceUrl, credential, act, deps = {}) {
4320
4346
  write(script.switchOpening);
4321
4347
  const replacement = await switchAccount(serviceUrl, deps);
4322
4348
  const replacementIdentity = await describeAccount(serviceUrl, replacement.credential, deps.fetchImpl);
4323
- write(`[halfcycle] Signed in as ${ink("keyBold", accountLabel(replacementIdentity, replacement.accountId, deps.verbose === true), colour)}. This install will ${actClause(act)}.
4349
+ write(`[halfcycle] Signed in. This install will ${actClause(act)}.
4324
4350
  `);
4325
4351
  if (script.afterSwitch !== "")
4326
4352
  write(script.afterSwitch);
@@ -4629,9 +4655,9 @@ var UPGRADE_COMMANDS = {
4629
4655
  };
4630
4656
  var INSTALL_METHOD_RULES = [
4631
4657
  { method: "winget", pattern: /[\\/]WinGet[\\/]/i },
4632
- { method: "npm", pattern: /node_modules[\\/]/i },
4658
+ { method: "npm", pattern: /node_modules[\\/]|[\\/]npm[\\/]claude(\.cmd|\.exe)?$/i },
4633
4659
  { method: "brew", pattern: /[\\/](Cellar|Caskroom|linuxbrew|homebrew)[\\/]/i },
4634
- { method: "native", pattern: /[\\/]\.local[\\/]share[\\/]claude(-code)?[\\/]/i }
4660
+ { method: "native", pattern: /[\\/]\.local[\\/](share[\\/]claude(-code)?|bin)[\\/]/i }
4635
4661
  ];
4636
4662
  function probeClaudeCodeInstallMethod(deps = {}) {
4637
4663
  const env = deps.env ?? process.env;
@@ -5836,9 +5862,6 @@ function ourHookEntries(value, commands) {
5836
5862
  }
5837
5863
  return found;
5838
5864
  }
5839
- function isOurHook(hook, commands) {
5840
- return isPlainObject2(hook) && typeof hook["command"] === "string" && commands.has(hook["command"]);
5841
- }
5842
5865
  function ourMcpServers(value) {
5843
5866
  const servers = value["mcpServers"];
5844
5867
  return isPlainObject2(servers) && MCP_SERVER_KEY in servers ? servers : null;
@@ -5867,9 +5890,9 @@ function unmergeSettings(value, rec, writeSet) {
5867
5890
  continue;
5868
5891
  }
5869
5892
  touched = true;
5870
- const remaining = entry.hooks.filter((h) => !isOurHook(h, commands));
5871
- if (remaining.length > 0)
5872
- next.push({ ...entry, hooks: remaining });
5893
+ const remaining = withoutHalfcycleCommands(entry, commands);
5894
+ if (remaining !== null)
5895
+ next.push(remaining);
5873
5896
  }
5874
5897
  if (touched)
5875
5898
  changed = true;
@@ -5908,16 +5931,12 @@ function unmergeSettings(value, rec, writeSet) {
5908
5931
  changed = true;
5909
5932
  }
5910
5933
  }
5911
- if ("schemaBefore" in rec) {
5912
- if (rec.schemaBefore === null) {
5913
- if ("$schema" in value) {
5914
- delete value["$schema"];
5915
- changed = true;
5916
- }
5917
- } else if (value["$schema"] !== rec.schemaBefore) {
5934
+ if ("schemaBefore" in rec && value["$schema"] === writeSet.settingsSchema) {
5935
+ if (rec.schemaBefore === null)
5936
+ delete value["$schema"];
5937
+ else
5918
5938
  value["$schema"] = rec.schemaBefore;
5919
- changed = true;
5920
- }
5939
+ changed = true;
5921
5940
  }
5922
5941
  return { changed, refused };
5923
5942
  }
@@ -5957,36 +5976,70 @@ function gitattributesRules(writeSet) {
5957
5976
  function trimAppendedBlocks(text, rec, rules) {
5958
5977
  const keptLines = [];
5959
5978
  const foreignLines = [];
5979
+ const eol = lineEndingOf(text);
5960
5980
  let current = text;
5961
5981
  for (const block of [...rec.appended].reverse()) {
5962
- const lines = block.split("\n").filter((line) => line !== "");
5982
+ const lines = block.split("\n").map(lineKey).filter((line) => line !== "");
5963
5983
  const foreign = lines.filter((line) => !rules.ourLines.has(line));
5964
5984
  foreignLines.push(...foreign);
5965
5985
  const ours = lines.filter((line) => rules.ourLines.has(line));
5966
5986
  const entries = ours.filter((line) => line !== rules.header);
5967
5987
  const kept = entries.filter((line) => rules.keepLine(line));
5968
5988
  keptLines.push(...kept);
5969
- if (kept.length === 0 && foreign.length === 0 && current.includes(block)) {
5970
- const at = current.lastIndexOf(block);
5971
- const head = current.slice(0, at);
5972
- const tail = current.slice(at + block.length);
5973
- current = head + (head !== "" && tail !== "" && !head.endsWith("\n") ? "\n" : "") + tail;
5989
+ const found = kept.length === 0 && foreign.length === 0 ? lastOccurrence(current, block) : null;
5990
+ if (found !== null) {
5991
+ const head = current.slice(0, found.start);
5992
+ const tail = current.slice(found.end);
5993
+ current = head + (head !== "" && tail !== "" && !head.endsWith("\n") ? eol : "") + tail;
5974
5994
  continue;
5975
5995
  }
5976
- const separated = block.startsWith("\n") || block.includes("\n\n");
5996
+ const flat = withoutLineEndCr(block).text;
5997
+ const separated = flat.startsWith("\n") || flat.includes("\n\n");
5977
5998
  const removable = ours.filter((line) => line === rules.header ? kept.length === 0 && foreign.length === 0 : !kept.includes(line));
5978
5999
  for (const line of removable) {
5979
6000
  const fileLines = current.split("\n");
5980
- const at = fileLines.lastIndexOf(line);
6001
+ const keys = fileLines.map(lineKey);
6002
+ const at = keys.lastIndexOf(line);
5981
6003
  if (at < 0)
5982
6004
  continue;
5983
- const withSeparator = line === rules.header && separated && at > 0 && fileLines[at - 1] === "";
6005
+ const withSeparator = line === rules.header && separated && at > 0 && keys[at - 1] === "";
5984
6006
  fileLines.splice(withSeparator ? at - 1 : at, withSeparator ? 2 : 1);
5985
6007
  current = fileLines.join("\n");
5986
6008
  }
5987
6009
  }
5988
6010
  return { text: current, keptLines, foreignLines };
5989
6011
  }
6012
+ function withoutLineEndCr(text) {
6013
+ let out = "";
6014
+ const at = [];
6015
+ let offset = 0;
6016
+ const lines = text.split("\n");
6017
+ for (const [i, line] of lines.entries()) {
6018
+ const key = lineKey(line);
6019
+ for (let j = 0; j < key.length; j++)
6020
+ at.push(offset + j);
6021
+ out += key;
6022
+ offset += line.length;
6023
+ if (i < lines.length - 1) {
6024
+ out += "\n";
6025
+ at.push(offset);
6026
+ offset += 1;
6027
+ }
6028
+ }
6029
+ return { text: out, at };
6030
+ }
6031
+ function lastOccurrence(text, block) {
6032
+ const needle = withoutLineEndCr(block).text;
6033
+ if (needle === "")
6034
+ return null;
6035
+ const hay = withoutLineEndCr(text);
6036
+ const found = hay.text.lastIndexOf(needle);
6037
+ if (found < 0)
6038
+ return null;
6039
+ const first = hay.at[found];
6040
+ const start = first > 0 && text[first] === "\n" && text[first - 1] === "\r" ? first - 1 : first;
6041
+ return { start, end: hay.at[found + needle.length - 1] + 1 };
6042
+ }
5990
6043
  function coveredPathExists(targetRepo, line) {
5991
6044
  const path = line.trim().replace(/^\//, "").replace(/\/$/, "");
5992
6045
  return path !== "" && lexists(join10(targetRepo, path));
@@ -6555,6 +6608,14 @@ ${USAGE}`);
6555
6608
  process.exit(1);
6556
6609
  return;
6557
6610
  }
6611
+ const onInstallSigint = () => {
6612
+ const kept = readFolderId(targetRepo) !== void 0 ? `${KEPT_FOLDER_ID_LINE}
6613
+ ` : "";
6614
+ process.stderr.write(`halfcycle: interrupted.
6615
+ ${kept}`);
6616
+ process.exit(1);
6617
+ };
6618
+ process.once("SIGINT", onInstallSigint);
6558
6619
  try {
6559
6620
  const controlOrigin = resolveControlOrigin(process.env);
6560
6621
  const serviceUrl = controlOrigin.origin;
@@ -6575,13 +6636,14 @@ ${USAGE}`);
6575
6636
  let credential;
6576
6637
  let notThisAccountMessage;
6577
6638
  let actingAccountId;
6639
+ let createdByAccountId;
6578
6640
  let actingIdentity;
6579
6641
  let foundLine = "";
6580
6642
  let reusedWithoutFreshAnswer = false;
6581
6643
  if (reusable !== void 0 && pinned !== null) {
6582
6644
  engagementId = pinned.engagementId;
6583
6645
  credential = reusable;
6584
- actingAccountId = readBundlePin(targetRepo)?.accountId;
6646
+ actingAccountId = heldAnswer?.identity?.accountId;
6585
6647
  foundLine = `already set up here \u2014 reusing engagement ${shortId(pinned.engagementId)}`;
6586
6648
  reusedWithoutFreshAnswer = true;
6587
6649
  if (verbose) {
@@ -6631,6 +6693,7 @@ ${USAGE}`);
6631
6693
  `);
6632
6694
  }
6633
6695
  } else {
6696
+ const hadUsableIdBeforeThisRun = readFolderId(targetRepo) !== void 0;
6634
6697
  const projectId = runPreCreateChecks(targetRepo);
6635
6698
  const created = await createOwnedEngagement(serviceUrl, targetRepo, projectId, { assumeYes, verbose });
6636
6699
  if (isProjectOwnedElsewhere(created)) {
@@ -6642,6 +6705,7 @@ ${PROJECT_RESET_LINE}
6642
6705
  }
6643
6706
  engagementId = created.engagementId;
6644
6707
  actingAccountId = created.actingAccountId;
6708
+ createdByAccountId = created.actingAccountId;
6645
6709
  actingIdentity = created.actingIdentity;
6646
6710
  credential = {
6647
6711
  serviceUrl,
@@ -6661,7 +6725,7 @@ ${PROJECT_RESET_LINE}
6661
6725
  consoleUrl: created.consoleUrl,
6662
6726
  methodVersion: created.methodVersion
6663
6727
  };
6664
- foundLine = `no Halfcycle project here yet \u2014 creating one (${shortId(created.engagementId)})`;
6728
+ foundLine = hadUsableIdBeforeThisRun ? `picking up this folder's project (${shortId(created.engagementId)})` : `no Halfcycle project here yet \u2014 creating one (${shortId(created.engagementId)})`;
6665
6729
  if (verbose) {
6666
6730
  process.stdout.write(`[halfcycle] Created engagement ${created.engagementId}
6667
6731
  `);
@@ -6672,7 +6736,7 @@ ${PROJECT_RESET_LINE}
6672
6736
  engagementId,
6673
6737
  engagementType: engagementTypeRaw,
6674
6738
  credential,
6675
- accountId: actingAccountId
6739
+ createdByAccountId
6676
6740
  });
6677
6741
  if (verbose) {
6678
6742
  process.stdout.write(`[halfcycle] Installed v${result.version} into ${targetRepo}
@@ -6688,8 +6752,14 @@ ${PROJECT_RESET_LINE}
6688
6752
  `);
6689
6753
  }
6690
6754
  }
6691
- if (result.collidedPaths.length > 0) {
6692
- process.stdout.write(`[halfcycle] Collided (a name you already use \u2014 NOT overwritten): ${result.collidedPaths.join(", ")}
6755
+ const edited = new Set(result.editedCollidedPaths);
6756
+ const trueCollisions = result.collidedPaths.filter((path) => !edited.has(path));
6757
+ if (result.editedCollidedPaths.length > 0) {
6758
+ process.stdout.write(`[halfcycle] Kept (your edited copy of an older Halfcycle file \u2014 NOT overwritten): ${result.editedCollidedPaths.join(", ")}
6759
+ `);
6760
+ }
6761
+ if (trueCollisions.length > 0) {
6762
+ process.stdout.write(`[halfcycle] Collided (a name you already use \u2014 NOT overwritten): ${trueCollisions.join(", ")}
6693
6763
  `);
6694
6764
  }
6695
6765
  if (result.replacedPaths.length > 0) {
@@ -6744,8 +6814,10 @@ ${PROJECT_RESET_LINE}
6744
6814
  const identity = actingIdentity ?? heldAnswer?.identity ?? (credential && heldAnswer === void 0 ? await describeAccount(credential.serviceUrl, credential.token) : void 0);
6745
6815
  const written = result.writtenPaths.length;
6746
6816
  const merged = result.mergedPaths.length;
6747
- process.stdout.write(`[halfcycle] Signed in as ${accountLabel(identity, actingAccountId, verbose, controlOriginConfirmed)}
6817
+ if (identity !== void 0) {
6818
+ process.stdout.write(`[halfcycle] Signed in as ${accountLabel(identity, actingAccountId, verbose)}
6748
6819
  `);
6820
+ }
6749
6821
  process.stdout.write(`[halfcycle] Found: ${foundLine}
6750
6822
  `);
6751
6823
  process.stdout.write(`[halfcycle] Set up: ${written} file${written === 1 ? "" : "s"} written${merged > 0 ? ` (${merged} merged)` : ""}, ${credential ? "guards armed on every edit" : "not checked \u2014 this copy is not this account\u2019s project"}
@@ -6757,7 +6829,7 @@ ${PROJECT_RESET_LINE}
6757
6829
  process.stdout.write(`[halfcycle] Commit the files Halfcycle set up here, including ${BUNDLE_PIN_REL} and ${PROJECT_IDENTITY_REL}, so every copy of this project joins it and runs the same checks.
6758
6830
  `);
6759
6831
  } else if (credential) {
6760
- process.stdout.write(process.platform !== "win32" && !gitIsInstalled() ? `[halfcycle] Halfcycle needs Git, and it is not installed on this machine \u2014 get it from https://git-scm.com/downloads, then run "git init" here to get checks on every edit.
6832
+ process.stdout.write(process.platform !== "win32" && !gitIsInstalled() ? `[halfcycle] Halfcycle needs Git, and it is not installed on this machine \u2014 get it from https://git-scm.com/downloads to get checks on every edit.
6761
6833
  ` : `[halfcycle] This folder is not a git repository. Halfcycle's checks compare against your git history, so run "git init" here to get checks on every edit.
6762
6834
  `);
6763
6835
  }