halfcycle 0.4.5 → 0.4.7

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/dist/index.js CHANGED
@@ -118,6 +118,7 @@ var VENDORED_BIN_REL = ".halfcycle/bin/bin.bundle.mjs";
118
118
  var SESSION_START_MARKER_REL = ".claude/hooks/session-start-marker.sh";
119
119
  var GUARD_RUNNER_REL = ".claude/hooks/guard-runner.sh";
120
120
  var USER_PROMPT_REMINDER_REL = ".claude/hooks/user-prompt-reminder.sh";
121
+ var USER_PROMPT_SCOPE_REL = ".claude/hooks/user-prompt-scope.sh";
121
122
  var SESSION_END_MARKER_REL = ".claude/hooks/session-end-marker.sh";
122
123
  var WRITE_CAPABLE_TOOLS = ["Edit", "MultiEdit", "NotebookEdit", "Write"];
123
124
  function hookCommand(script, verb) {
@@ -146,6 +147,13 @@ var HALFCYCLE_HOOK_WIRING = [
146
147
  command: hookCommand(USER_PROMPT_REMINDER_REL),
147
148
  script: USER_PROMPT_REMINDER_REL
148
149
  },
150
+ {
151
+ event: "UserPromptSubmit",
152
+ matcher: null,
153
+ timeout: 10,
154
+ command: hookCommand(USER_PROMPT_SCOPE_REL),
155
+ script: USER_PROMPT_SCOPE_REL
156
+ },
149
157
  {
150
158
  event: "PostToolUse",
151
159
  matcher: WRITE_CAPABLE_TOOLS.join("|"),
@@ -227,6 +235,9 @@ function quoteForShell(value) {
227
235
  }
228
236
  var HALFCYCLE_COMMAND = "npx halfcycle@latest";
229
237
 
238
+ // ../core/dist/release-version.js
239
+ var RELEASE_VERSION_PATTERN = /^\d+\.\d+\.\d+$/;
240
+
230
241
  // ../events/dist/result.js
231
242
  import { z as z2 } from "zod";
232
243
  var firedGuardSchema = z2.object({
@@ -293,15 +304,12 @@ function hasLoneSurrogate(text) {
293
304
  }
294
305
  var LONE_SURROGATE_MESSAGE = "must be valid text: it holds half of a character pair with no other half";
295
306
 
296
- // ../events/dist/build-record.js
297
- import { z as z4 } from "zod";
298
-
299
307
  // ../events/dist/state.js
300
308
  import { z as z3 } from "zod";
301
309
  var utcIso8601 = z3.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/, "must be a UTC ISO-8601 timestamp ending in Z");
302
310
  var STATE_RECORD_FORMAT = "halfcycle-state-record/v1";
303
311
  var stepIdSchema = z3.string().regex(/^(l[0-5](-l[0-5])?|xl)\.[a-z0-9-]+$/);
304
- var methodVersionSchema = z3.string().regex(/^\d+\.\d+\.\d+$/);
312
+ var methodVersionSchema = z3.string().regex(RELEASE_VERSION_PATTERN);
305
313
  var anchorPathRegex = /^(?!\/)(?!.*#)(?!method\/)(?!(.*\/)?docs\/method\/).+$/;
306
314
  var recordKindSchema = z3.enum(["artefact", "step-run", "gate", "intervention"]);
307
315
  var methodLayerSchema = z3.enum(["l0", "l1", "l2", "l3", "l3-l4", "l4", "l5", "xl"]);
@@ -478,6 +486,9 @@ var stateRecordSchema = z3.discriminatedUnion("recordKind", [
478
486
  interventionRecordSchema
479
487
  ]);
480
488
 
489
+ // ../events/dist/build-record.js
490
+ import { z as z4 } from "zod";
491
+
481
492
  // ../events/dist/control-characters.js
482
493
  var CONTROL_CHARACTER_CLASS = "\\p{Cc}";
483
494
  var CONTROL_CHARACTER = new RegExp(CONTROL_CHARACTER_CLASS, "u");
@@ -581,9 +592,11 @@ function statementText(max) {
581
592
  });
582
593
  }
583
594
  var closeActorSchema = plainText(ACTOR_MAX);
595
+ var PHASE_NAME_RULE = `must be a phase name: not empty, no slash or backslash, not starting with a dot, no control characters, at most ${PHASE_NAME_MAX_BYTES} bytes`;
584
596
  var phaseNameSchema = z4.string().refine((v) => isValidPhaseIdentity(v), {
585
- message: `must be a phase name: not empty, no slash or backslash, not starting with a dot, no control characters, at most ${PHASE_NAME_MAX_BYTES} bytes`
597
+ message: PHASE_NAME_RULE
586
598
  });
599
+ var phaseIdentitySchema = z4.union([z4.string(), z4.number()], { error: "must be a phase name or a whole number" }).refine((v) => isValidPhaseIdentity(v), { message: PHASE_NAME_RULE });
587
600
  var closeStatementsSchema = z4.object({
588
601
  thesis: statementText(STATEMENT_MAX).nullable(),
589
602
  outcome: statementText(STATEMENT_MAX).nullable(),
@@ -1356,7 +1369,7 @@ function createProjectIdentityFile(targetRepoRoot) {
1356
1369
  }
1357
1370
 
1358
1371
  // dist/install-manifest.js
1359
- import { Buffer } from "node:buffer";
1372
+ import { Buffer as Buffer2 } from "node:buffer";
1360
1373
  import { createHash } from "node:crypto";
1361
1374
  var INSTALL_MANIFEST_REL = ".halfcycle/install-manifest.json";
1362
1375
  var INSTALL_MANIFEST_FORMAT = "halfcycle-install-manifest/v1";
@@ -1367,7 +1380,7 @@ function lineKey(line) {
1367
1380
  return line.endsWith("\r") ? line.slice(0, -1) : line;
1368
1381
  }
1369
1382
  function fileSha256(bytes) {
1370
- const text = typeof bytes === "string" ? bytes : Buffer.from(bytes).toString("utf-8");
1383
+ const text = typeof bytes === "string" ? bytes : Buffer2.from(bytes).toString("utf-8");
1371
1384
  return sha256Hex(text.split("\n").map(lineKey).join("\n"));
1372
1385
  }
1373
1386
  function canonicalJson(value) {
@@ -1813,6 +1826,8 @@ var WRITE_ALLOWLIST = [
1813
1826
  BUNDLE_PIN_REL,
1814
1827
  ".halfcycle/bin",
1815
1828
  "docs/build-records",
1829
+ // Architecture pictures the draw command writes from Halfcycle's answer. The installer never writes here; keep the entry so this path stays declared as one Halfcycle writes.
1830
+ "docs/diagrams",
1816
1831
  "test/fixtures/captured",
1817
1832
  MCP_REGISTRATION_REL,
1818
1833
  ".gitattributes",
@@ -2381,6 +2396,54 @@ printf '%s\\n' '${USER_PROMPT_REMINDER_SENTENCE}'
2381
2396
  exit 0
2382
2397
  `;
2383
2398
  }
2399
+ var GENERATED_USER_PROMPT_SCOPE_HEADER = "# Typed scope approval \u2014 generated by";
2400
+ function generateUserPromptScopeHook() {
2401
+ return `#!/usr/bin/env bash
2402
+ ${GENERATED_USER_PROMPT_SCOPE_HEADER} the Halfcycle installer.
2403
+ #
2404
+ # Lets the owner of this project approve a phase's scope, or send it back, by
2405
+ # typing "approve <phase>" or "send back <phase>: <note>" as the whole prompt in
2406
+ # their own Claude Code session.
2407
+ #
2408
+ # AN ORDINARY PROMPT STARTS NOTHING. This script reads the hook input once and
2409
+ # looks only at the start of the prompt's value. Unless the prompt begins, after
2410
+ # any whitespace (typed, or escaped in the input), with "approve " or
2411
+ # "send back " in any case, it exits 0 with no output, and Node never starts.
2412
+ # A quote inside any other field arrives escaped, so no other field (the folder
2413
+ # the session is in, for one) can read as the start of the prompt.
2414
+ #
2415
+ # Otherwise it finds the project root from its own location, never from the
2416
+ # folder the session has moved to, and hands the same input to the guard
2417
+ # wrapper beside it. That decides whether the line counts and prints at most
2418
+ # one plain line. Nothing here ever blocks a prompt.
2419
+ #
2420
+ # FAILS OPEN up to that hand-over: any error before it exits 0 with no output.
2421
+ # After it, the wrapper's own exit stands.
2422
+ trap 'exit 0' ERR
2423
+ set -e
2424
+
2425
+ # Read with the shell's own builtin, so the check below is the only program an
2426
+ # ordinary prompt starts.
2427
+ HC_INPUT=""
2428
+ IFS= read -r -d '' HC_INPUT || true
2429
+
2430
+ # What may come before the phrase: whitespace as typed, as a short escape, as a
2431
+ # \\u escape, or as the UTF-8 bytes of a Unicode space. Compared byte for byte.
2432
+ HC_LEAD='[[:space:]]|\\\\[nrtf]|\\\\u(000[9a-d]|0020|00a0|1680|200[0-9a]|202[89f]|205f|3000|feff)'
2433
+ 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'
2434
+ if ! printf '%s' "$HC_INPUT" | LC_ALL=C grep -Eiq "\\"prompt\\"[[:space:]]*:[[:space:]]*\\"($HC_LEAD)*(approve|send back) "; then
2435
+ exit 0
2436
+ fi
2437
+
2438
+ HC_SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
2439
+ HC_PROJECT_ROOT=$(CDPATH= cd -- "$HC_SCRIPT_DIR/../.." && pwd)
2440
+ export CLAUDE_PROJECT_DIR="$HC_PROJECT_ROOT"
2441
+
2442
+ trap - ERR
2443
+ set +e
2444
+ exec bash "$HC_PROJECT_ROOT/.claude/hooks/guard-runner.sh" hook user-prompt-submit <<<"$HC_INPUT"
2445
+ `;
2446
+ }
2384
2447
  var CI_STANZA_KNOWN_HEADER_HASHES = [
2385
2448
  "b225800b2d699811f32ac213238cbf81c35b5e6a74114bdf55e39cd9d7e2928f",
2386
2449
  "674181735460f051af7c157222cf851fc3000fb3b8af3a10ea0bde9fe5eaba1f",
@@ -2456,6 +2519,7 @@ function removeRetiredMcpHeadersHelper(targetRepo) {
2456
2519
  var OWNED_GENERATED_HEADERS = {
2457
2520
  ".claude/hooks/guard-runner.sh": GENERATED_GUARD_RUNNER_HEADER,
2458
2521
  ".claude/hooks/user-prompt-reminder.sh": GENERATED_USER_PROMPT_REMINDER_HEADER,
2522
+ ".claude/hooks/user-prompt-scope.sh": GENERATED_USER_PROMPT_SCOPE_HEADER,
2459
2523
  ".claude/hooks/session-end-marker.sh": GENERATED_SESSION_END_HEADER,
2460
2524
  ".claude/hooks/session-start-marker.sh": GENERATED_SESSION_START_HEADER
2461
2525
  };
@@ -2464,6 +2528,7 @@ function hookScriptsThisVersionWrites() {
2464
2528
  [".claude/hooks/guard-runner.sh", generateGuardRunnerWrapper()],
2465
2529
  [".claude/hooks/session-end-marker.sh", generateSessionEndMarker()],
2466
2530
  [".claude/hooks/user-prompt-reminder.sh", generateUserPromptReminderHook()],
2531
+ [".claude/hooks/user-prompt-scope.sh", generateUserPromptScopeHook()],
2467
2532
  [".claude/hooks/session-start-marker.sh", generateSessionStartMarker()]
2468
2533
  ];
2469
2534
  }
@@ -3857,7 +3922,7 @@ function renderBuildRecordV2Markdown(record2) {
3857
3922
  `Phase: ${phase} (the name the client gave it)`,
3858
3923
  OBSERVED_LABEL,
3859
3924
  [
3860
- `- Method version: ${p.text(record2.observed.methodVersion)}. The Halfcycle method is updated from time to time, and each update has its own version number. This is the one the project was run under; it does not change during a project.`,
3925
+ `- Method version: ${p.text(record2.observed.methodVersion)}. The Halfcycle method is updated from time to time, and each update has its own version number. This is the one the phase was run under. A project moves to a newer version only between phases.`,
3861
3926
  `- Project identifier: ${p.text(record2.observed.engagementId)}. Halfcycle keeps this project's records under this identifier.`,
3862
3927
  `- Record format: ${p.text(record2.format)}. This names the layout this file and the .json file beside it follow, for software that reads them.`
3863
3928
  ].join("\n"),
@@ -3889,10 +3954,14 @@ function assertInsideOutDir(outDir, candidate, identity) {
3889
3954
  throw new InvalidPhaseIdentityError(identity, BUILD_RECORD_DIR);
3890
3955
  }
3891
3956
  }
3892
- function createExclusiveOnDisk(path, text) {
3957
+ function createExclusiveOnDisk(path, data) {
3958
+ const bytes = typeof data === "string" ? Buffer.from(data, "utf-8") : data;
3893
3959
  const fd = openSync(path, "wx");
3894
3960
  try {
3895
- writeSync(fd, text);
3961
+ let written = 0;
3962
+ while (written < bytes.length) {
3963
+ written += writeSync(fd, bytes, written, bytes.length - written);
3964
+ }
3896
3965
  } catch (err) {
3897
3966
  closeSync(fd);
3898
3967
  rmSync3(path, { force: true });
@@ -4009,6 +4078,22 @@ function replaceOne(io, tmp, finalPath, text) {
4009
4078
 
4010
4079
  // dist/build-record/obtain.js
4011
4080
  import { relative as relative3 } from "node:path";
4081
+
4082
+ // dist/missing-route.js
4083
+ var MESSAGE = {
4084
+ GET: /^Route GET .* not found\.$/s,
4085
+ POST: /^Route POST .* not found\.$/s
4086
+ };
4087
+ function isMissingRoute(method, status, body) {
4088
+ if (status !== 404 || typeof body !== "object" || body === null)
4089
+ return false;
4090
+ const { error, message } = body;
4091
+ if (error !== "Not Found" || typeof message !== "string")
4092
+ return false;
4093
+ return MESSAGE[method].test(message);
4094
+ }
4095
+
4096
+ // dist/build-record/obtain.js
4012
4097
  var REFUSAL_FOLLOW_UP = {
4013
4098
  "still-open": "Nothing was written. The record is produced when the phase closes.",
4014
4099
  "reopened": "Nothing was written. A new record is produced when the phase closes again.",
@@ -4020,12 +4105,6 @@ var REFUSAL_REASONS = phaseRecordRefusalReasonSchema.options;
4020
4105
  function buildRecordUrl(credential, phase) {
4021
4106
  return `${credential.serviceUrl}/engagements/${encodeURIComponent(credential.engagementId)}/phases/${encodeURIComponent(phase)}/build-record`;
4022
4107
  }
4023
- function isMissingRoute(status, body) {
4024
- if (status !== 404 || typeof body !== "object" || body === null)
4025
- return false;
4026
- const { error, message } = body;
4027
- return error === "Not Found" && typeof message === "string" && /^Route GET .* not found\.$/s.test(message);
4028
- }
4029
4108
  async function obtainBuildRecord(credential, repoRoot, phase, io = NODE_RECORD_PAIR_IO) {
4030
4109
  const url = buildRecordUrl(credential, phase);
4031
4110
  let res;
@@ -4082,7 +4161,7 @@ async function obtainBuildRecord(credential, repoRoot, phase, io = NODE_RECORD_P
4082
4161
  headline: latestCloseHeadline(answer.record)
4083
4162
  };
4084
4163
  }
4085
- if (isMissingRoute(res.status, raw)) {
4164
+ if (isMissingRoute("GET", res.status, raw)) {
4086
4165
  return { kind: "unavailable", why: "this Halfcycle service does not provide Build Records yet." };
4087
4166
  }
4088
4167
  const message = typeof raw === "object" && raw !== null && typeof raw.message === "string" ? raw.message : null;