halfcycle 0.4.17 → 0.4.18

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.17",
3
+ "version": "0.4.18",
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
  {
@@ -18295,7 +18295,8 @@ var chatReviewAcceptRequestSchema = external_exports.object({
18295
18295
  act: external_exports.literal("accept"),
18296
18296
  reference: phaseIdentitySchema,
18297
18297
  reviewVersion: external_exports.number().int().min(0),
18298
- parts: external_exports.array(systemPartIdSchema).max(REVIEW_PARTS_MAX)
18298
+ parts: external_exports.array(systemPartIdSchema).max(REVIEW_PARTS_MAX),
18299
+ waitingClaims: external_exports.literal(true).optional()
18299
18300
  }).strict();
18300
18301
  function notedRequest(act) {
18301
18302
  return external_exports.object({
@@ -18310,12 +18311,20 @@ var chatReviewQuestionRequestSchema = notedRequest("question");
18310
18311
  var chatReviewSendBackRequestSchema = notedRequest("send-back");
18311
18312
  var chatReviewCarryRequestSchema = notedRequest("carry");
18312
18313
  var chatReviewDropRequestSchema = notedRequest("drop");
18314
+ var chatReviewRejectRequestSchema = external_exports.object({
18315
+ act: external_exports.literal("reject"),
18316
+ reference: phaseIdentitySchema,
18317
+ reviewVersion: external_exports.number().int().min(0),
18318
+ task: buildTaskIdSchema,
18319
+ note: reviewNoteWireSchema
18320
+ }).strict();
18313
18321
  var chatReviewRequestSchema = external_exports.discriminatedUnion("act", [
18314
18322
  chatReviewAcceptRequestSchema,
18315
18323
  chatReviewQuestionRequestSchema,
18316
18324
  chatReviewSendBackRequestSchema,
18317
18325
  chatReviewCarryRequestSchema,
18318
- chatReviewDropRequestSchema
18326
+ chatReviewDropRequestSchema,
18327
+ chatReviewRejectRequestSchema
18319
18328
  ]);
18320
18329
  function bareOutcome2(outcome) {
18321
18330
  return external_exports.object({ outcome: external_exports.literal(outcome) }).strict();
@@ -18328,7 +18337,8 @@ var chatReviewResultSchema = external_exports.discriminatedUnion("outcome", [
18328
18337
  ...reviewDecisionResultSchema.options.filter(notTheConsoleNotDecidable),
18329
18338
  bareOutcome2("show-first"),
18330
18339
  bareOutcome2("not-offered"),
18331
- chatAmbiguousSchema
18340
+ chatAmbiguousSchema,
18341
+ bareOutcome2("nothing-waiting")
18332
18342
  ]);
18333
18343
  var reviewAnswerRequestSchema = external_exports.object({
18334
18344
  part: systemPartIdSchema,
@@ -30429,6 +30439,9 @@ function reviewPhraseShape(prompt) {
30429
30439
  const line2 = prompt.trim();
30430
30440
  if (LINE_BREAK.test(line2))
30431
30441
  return null;
30442
+ const reject = REJECT_VERB.exec(line2);
30443
+ if (reject !== null)
30444
+ return rejectShape(line2.slice(reject[0].length));
30432
30445
  for (const [prefix, act] of VERBS) {
30433
30446
  const verb = prefix.exec(line2);
30434
30447
  if (verb === null)
@@ -30446,6 +30459,21 @@ function reviewPhraseShape(prompt) {
30446
30459
  }
30447
30460
  return null;
30448
30461
  }
30462
+ var REJECT_VERB = /^reject /i;
30463
+ function rejectShape(rest) {
30464
+ const colon = rest.indexOf(":");
30465
+ if (colon === -1)
30466
+ return null;
30467
+ const head = rest.slice(0, colon);
30468
+ const slash = head.indexOf("/");
30469
+ if (slash === -1)
30470
+ return null;
30471
+ const reference = head.slice(0, slash);
30472
+ const task = head.slice(slash + 1);
30473
+ if (!isValidPhaseIdentity(reference) || /\s/.test(task) || !buildTaskIdSchema.safeParse(task).success)
30474
+ return null;
30475
+ return { kind: "reject", reference, task, words: rest.slice(colon + 1).trim() };
30476
+ }
30449
30477
  function partShape(act, head, words) {
30450
30478
  const slash = head.indexOf("/");
30451
30479
  if (slash === -1)
@@ -30458,10 +30486,15 @@ function partShape(act, head, words) {
30458
30486
  }
30459
30487
  function matchReviewPhraseShape(shape, reviews) {
30460
30488
  const resolved = resolvePhaseReference(shape.reference, reviews);
30461
- const act = shape.kind === "phase" ? "accept" : shape.act;
30489
+ const act = shape.kind === "phase" ? "accept" : shape.kind === "reject" ? "reject" : shape.act;
30462
30490
  if (resolved.kind === "ambiguous")
30463
30491
  return { kind: "ambiguous", act, entries: resolved.entries };
30464
30492
  const entry = resolved.kind === "found" ? resolved.entry : null;
30493
+ if (shape.kind === "reject") {
30494
+ if (entry === null)
30495
+ return { kind: "show-first", act: "reject" };
30496
+ return { kind: "reject", entry, reference: shape.reference, task: shape.task, words: shape.words };
30497
+ }
30465
30498
  if (shape.kind === "phase") {
30466
30499
  if (entry === null)
30467
30500
  return { kind: "nothing" };
@@ -30650,6 +30683,8 @@ function reviewWrongActLine(name, built) {
30650
30683
  }
30651
30684
  function wordsActNoun(act) {
30652
30685
  switch (act) {
30686
+ case "reject":
30687
+ return "a rejection";
30653
30688
  case "question":
30654
30689
  return "a question";
30655
30690
  case "send-back":
@@ -30686,7 +30721,10 @@ function reviewPartNotListedLine(part, phase) {
30686
30721
  return line(`${NOTHING_RECORDED}: "${part}" is not a part of the review of ${namedPhase(phase)} as it was listed here.`);
30687
30722
  }
30688
30723
  function reviewNothingToAcceptLine(phase) {
30689
- return line(`${NOTHING_RECORDED}: no built part of ${namedPhase(phase)} waits for your decision.`);
30724
+ return line(`${NOTHING_RECORDED}: no built part and no check nobody ran of ${namedPhase(phase)} waits for your decision.`);
30725
+ }
30726
+ function reviewNoClaimWaitingLine(task, phase) {
30727
+ return line(`${NOTHING_RECORDED}: no check nobody ran on task "${task}" of ${namedPhase(phase)} waits for your answer. ${LIST_AGAIN} and copy the task from its reject line.`);
30690
30728
  }
30691
30729
  function reviewListedTooLongAgoLine(phase) {
30692
30730
  return line(`${NOTHING_RECORDED}: ${namedPhase(phase)} was listed an hour or more ago. ${LIST_AGAIN}, then type the line again within the hour.`);
@@ -30762,9 +30800,12 @@ function reviewRecordedLine(decided, phase, waiting) {
30762
30800
  }
30763
30801
  function reviewBulkRecordedLine(accepted, phase, waiting) {
30764
30802
  const named = namedPhase(phase);
30765
- const first = accepted === 1 ? `Recorded in your session: 1 part of ${named} accepted.` : `Recorded in your session: ${accepted} parts of ${named} accepted.`;
30803
+ const first = accepted === 0 ? `Recorded in your session: any check nobody ran of ${named} that waited is accepted.` : accepted === 1 ? `Recorded in your session: 1 part of ${named} accepted, with any check nobody ran that waited.` : `Recorded in your session: ${accepted} parts of ${named} accepted, with any check nobody ran that waited.`;
30766
30804
  return line(`${first} ${stillWaiting(waiting)}`);
30767
30805
  }
30806
+ function reviewClaimsRejectedLine(task, phase, waiting) {
30807
+ return line(`Recorded in your session: the checks nobody ran on task "${task}" are rejected, with your words. ${stillWaitingOf(phase, waiting)}`);
30808
+ }
30768
30809
  function reviewClosedLine(phase) {
30769
30810
  return `Recorded in your session. That was the last decision: phase ${namedPhase(phase)} is closed. Run its close step now, then tell the person what it says.`;
30770
30811
  }
@@ -30791,12 +30832,16 @@ function reviewLineForAnswer(context, answer) {
30791
30832
  switch (answer.outcome) {
30792
30833
  case "recorded": {
30793
30834
  if (answer.closed) {
30794
- return answer.decided.length === 0 ? reviewNoPartsClosedLine(phase) : reviewClosedLine(phase);
30835
+ return context.listedParts === 0 ? reviewNoPartsClosedLine(phase) : reviewClosedLine(phase);
30795
30836
  }
30837
+ if (context.task !== null)
30838
+ return reviewClaimsRejectedLine(context.task, phase, answer.waiting);
30796
30839
  if (context.bulk)
30797
30840
  return reviewBulkRecordedLine(answer.decided.length, phase, answer.waiting);
30798
30841
  return reviewRecordedLine(answer.decided[0], phase, answer.waiting);
30799
30842
  }
30843
+ case "nothing-waiting":
30844
+ return context.task !== null ? reviewNoClaimWaitingLine(context.task, phase) : reviewNothingToAcceptLine(phase);
30800
30845
  case "review-changed":
30801
30846
  return reviewChangedLine(phase);
30802
30847
  case "no-review":
@@ -30977,14 +31022,22 @@ async function checkAndSendReview(match, engagementId, credential, env, opts) {
30977
31022
  return reviewListedTooLongAgoLine(entry.phase);
30978
31023
  let candidate;
30979
31024
  if (match.kind === "accept") {
30980
- if (entry.parts.length > 0 && match.parts.length === 0)
30981
- return reviewNothingToAcceptLine(entry.phase);
30982
31025
  candidate = {
30983
31026
  act: "accept",
30984
31027
  reference: match.reference,
30985
31028
  reviewVersion: entry.reviewVersion,
30986
- parts: match.parts.map((p) => p.part)
31029
+ parts: match.parts.map((p) => p.part),
31030
+ waitingClaims: true
30987
31031
  };
31032
+ } else if (match.kind === "reject") {
31033
+ const words = match.words;
31034
+ if (words === "")
31035
+ return reviewNoWordsLine("reject");
31036
+ if (!reviewNoteWireSchema.safeParse(words).success)
31037
+ return reviewWordsTooLongLine();
31038
+ if (looksLikeSecret(words))
31039
+ return reviewWordsLookSecretLine();
31040
+ candidate = { act: "reject", reference: match.reference, reviewVersion: entry.reviewVersion, task: match.task, note: words };
30988
31041
  } else {
30989
31042
  const { act, part } = match;
30990
31043
  if (!reviewActsFor(part.built).includes(act))
@@ -31037,7 +31090,14 @@ async function postReview(bulk, entry, request, engagementId, credential, env, o
31037
31090
  return reviewCannotReachLine();
31038
31091
  moveListedReviewForward(engagementId, opts.home, entry, answer.reviewVersion, answer.decided);
31039
31092
  }
31040
- return reviewLineForAnswer({ act: request.act, bulk, phase: entry.phase, number: entry.number }, answer);
31093
+ return reviewLineForAnswer({
31094
+ act: request.act,
31095
+ bulk,
31096
+ phase: entry.phase,
31097
+ number: entry.number,
31098
+ listedParts: entry.parts.length,
31099
+ task: request.act === "reject" ? request.task : null
31100
+ }, answer);
31041
31101
  } catch {
31042
31102
  return reviewCannotReachLine();
31043
31103
  } finally {
package/dist/cli.js CHANGED
@@ -4851,7 +4851,8 @@ var chatReviewAcceptRequestSchema = z30.object({
4851
4851
  act: z30.literal("accept"),
4852
4852
  reference: phaseIdentitySchema,
4853
4853
  reviewVersion: z30.number().int().min(0),
4854
- parts: z30.array(systemPartIdSchema).max(REVIEW_PARTS_MAX)
4854
+ parts: z30.array(systemPartIdSchema).max(REVIEW_PARTS_MAX),
4855
+ waitingClaims: z30.literal(true).optional()
4855
4856
  }).strict();
4856
4857
  function notedRequest(act) {
4857
4858
  return z30.object({
@@ -4866,12 +4867,20 @@ var chatReviewQuestionRequestSchema = notedRequest("question");
4866
4867
  var chatReviewSendBackRequestSchema = notedRequest("send-back");
4867
4868
  var chatReviewCarryRequestSchema = notedRequest("carry");
4868
4869
  var chatReviewDropRequestSchema = notedRequest("drop");
4870
+ var chatReviewRejectRequestSchema = z30.object({
4871
+ act: z30.literal("reject"),
4872
+ reference: phaseIdentitySchema,
4873
+ reviewVersion: z30.number().int().min(0),
4874
+ task: buildTaskIdSchema,
4875
+ note: reviewNoteWireSchema
4876
+ }).strict();
4869
4877
  var chatReviewRequestSchema = z30.discriminatedUnion("act", [
4870
4878
  chatReviewAcceptRequestSchema,
4871
4879
  chatReviewQuestionRequestSchema,
4872
4880
  chatReviewSendBackRequestSchema,
4873
4881
  chatReviewCarryRequestSchema,
4874
- chatReviewDropRequestSchema
4882
+ chatReviewDropRequestSchema,
4883
+ chatReviewRejectRequestSchema
4875
4884
  ]);
4876
4885
  function bareOutcome2(outcome) {
4877
4886
  return z30.object({ outcome: z30.literal(outcome) }).strict();
@@ -4884,7 +4893,8 @@ var chatReviewResultSchema = z30.discriminatedUnion("outcome", [
4884
4893
  ...reviewDecisionResultSchema.options.filter(notTheConsoleNotDecidable),
4885
4894
  bareOutcome2("show-first"),
4886
4895
  bareOutcome2("not-offered"),
4887
- chatAmbiguousSchema
4896
+ chatAmbiguousSchema,
4897
+ bareOutcome2("nothing-waiting")
4888
4898
  ]);
4889
4899
  var reviewAnswerRequestSchema = z30.object({
4890
4900
  part: systemPartIdSchema,
@@ -5348,8 +5358,8 @@ function autonomyA13(accepted, carried) {
5348
5358
  return `Your autonomy setting accepted the ${String(accepted)} built parts and carried ${String(carried)} unbuilt parts forward. The phase is closed.`;
5349
5359
  }
5350
5360
  function autonomyA14(count, parts, acceptLine) {
5351
- const typed = acceptLine === null ? 'say "review what was built" and type the lines it gives.' : `say "review what was built", then type ${acceptLine}.`;
5352
- return `The phase waits for you: ${String(count)} tests the agents said pass were not run by Halfcycle, so ${namesOf(parts)} wait for you too. Only you can accept or reject those tests, in the console's review; that cannot be done by typing in chat. Then decide those parts there, or in your own Claude Code session: ` + typed;
5361
+ const typed = acceptLine === null ? 'say "review what was built" and type the lines it gives.' : `say "review what was built", then type ${acceptLine} to accept them with those parts, or type the reject line it gives for a task's tests.`;
5362
+ return `The phase waits for you: ${String(count)} tests the agents said pass were not run by Halfcycle, so ${namesOf(parts)} wait for you too. Only you can accept or reject those tests. In your own Claude Code session, ` + typed;
5353
5363
  }
5354
5364
  function autonomyA15(count) {
5355
5365
  return `The phase waits for you: ${String(count)} parts were built without an approved scope, and only you can decide them.`;
@@ -7413,12 +7423,13 @@ ${GENERATED_USER_PROMPT_SCOPE_HEADER} the Halfcycle installer, now also for the
7413
7423
  # own on or off, by typing one line as the whole prompt in their own Claude
7414
7424
  # Code session: "approve <phase>", "send back <phase>: <note>", "accept
7415
7425
  # <phase>/<part>", "question <phase>/<part>: <words>", "carry <phase>/<part>:
7416
- # <words>", "drop <phase>/<part>: <words>", "autonomy on" or "autonomy off".
7426
+ # <words>", "drop <phase>/<part>: <words>", "reject <phase>/<task>: <words>",
7427
+ # "autonomy on" or "autonomy off".
7417
7428
  #
7418
7429
  # AN ORDINARY PROMPT STARTS NOTHING. This script reads the hook input once and
7419
7430
  # looks only at the start of the prompt's value. Unless the prompt begins, after
7420
7431
  # any whitespace (typed, or escaped in the input), with "approve ", "send back ",
7421
- # "accept ", "question ", "carry ", "drop " or "autonomy ", in any case, it exits
7432
+ # "accept ", "question ", "carry ", "drop ", "reject " or "autonomy ", in any case, it exits
7422
7433
  # 0 with no output, and Node never starts. A quote inside any other field arrives
7423
7434
  # escaped, so no other field (the folder the session is in, for one) can read as
7424
7435
  # the start of the prompt.
@@ -7442,7 +7453,7 @@ IFS= read -r -d '' HC_INPUT || true
7442
7453
  # \\u escape, or as the UTF-8 bytes of a Unicode space. Compared byte for byte.
7443
7454
  HC_LEAD='[[:space:]]|\\\\[nrtf]|\\\\u(000[9a-d]|0020|00a0|1680|200[0-9a]|202[89f]|205f|3000|feff)'
7444
7455
  HC_LEAD="$HC_LEAD|"$'\\xc2\\xa0|\\xe1\\x9a\\x80|\\xe2\\x80[\\x80-\\x8a\\xa8\\xa9\\xaf]|\\xe2\\x81\\x9f|\\xe3\\x80\\x80|\\xef\\xbb\\xbf'
7445
- if ! printf '%s' "$HC_INPUT" | LC_ALL=C grep -Eiq "\\"prompt\\"[[:space:]]*:[[:space:]]*\\"($HC_LEAD)*(approve|send back|accept|question|carry|drop|autonomy) "; then
7456
+ if ! printf '%s' "$HC_INPUT" | LC_ALL=C grep -Eiq "\\"prompt\\"[[:space:]]*:[[:space:]]*\\"($HC_LEAD)*(approve|send back|accept|question|carry|drop|reject|autonomy) "; then
7446
7457
  exit 0
7447
7458
  fi
7448
7459
 
@@ -12927,23 +12938,36 @@ function typedLine(act, reference, part) {
12927
12938
  return `drop ${reference}/${part}: <why>`;
12928
12939
  }
12929
12940
  }
12930
- function rv7Lines(view) {
12941
+ function rv7aLine(reference) {
12942
+ return `The line accept ${reference} also accepts every check nobody ran that is listed below. Reject a task's checks first if you do not accept them.`;
12943
+ }
12944
+ function tasksWithWaitingClaims(claims) {
12945
+ return [...new Set(claims.filter((c) => c.answer === null).map((c) => c.task))];
12946
+ }
12947
+ function rv7Lines(view, claims) {
12931
12948
  const reference = typedPhaseReference(view.phase, view.number);
12932
- if (view.parts.length === 0)
12933
- return [`This phase has no parts to review. To close it, type: accept ${reference}`];
12949
+ const claimTasks = tasksWithWaitingClaims(claims);
12934
12950
  const decidable = view.parts.filter((p) => p.decidable);
12935
- if (decidable.length === 0)
12936
- return ["No part waits for your decision now."];
12951
+ if (claimTasks.length === 0) {
12952
+ if (view.parts.length === 0)
12953
+ return [`This phase has no parts to review. To close it, type: accept ${reference}`];
12954
+ if (decidable.length === 0)
12955
+ return ["No part waits for your decision now."];
12956
+ }
12937
12957
  const lines = ["To decide, type one of these lines in your own Claude Code session:"];
12938
12958
  for (const part of decidable) {
12939
12959
  for (const act of reviewActsFor(part.built))
12940
12960
  lines.push(` ${typedLine(act, reference, part.part)}`);
12941
12961
  }
12942
- if (decidable.some((p) => p.built))
12962
+ for (const task of claimTasks)
12963
+ lines.push(` reject ${reference}/${task}: <why>`);
12964
+ if (decidable.some((p) => p.built) || claimTasks.length > 0)
12943
12965
  lines.push(` accept ${reference}`);
12966
+ if (claimTasks.length > 0)
12967
+ lines.push(rv7aLine(reference));
12944
12968
  return lines;
12945
12969
  }
12946
- function reviewListingLines(view) {
12970
+ function reviewListingLines(view, claims = []) {
12947
12971
  const lines = [rv1Line(view)];
12948
12972
  if (!view.counted)
12949
12973
  lines.push(RV1A_LINE);
@@ -12951,7 +12975,7 @@ function reviewListingLines(view) {
12951
12975
  lines.push(...observedPhaseLines(view.observed));
12952
12976
  for (const part of view.parts)
12953
12977
  lines.push(...partLines(part, view));
12954
- lines.push(...rv7Lines(view));
12978
+ lines.push(...rv7Lines(view, claims));
12955
12979
  return lines;
12956
12980
  }
12957
12981
  var CALLS_HEADING2 = "Calls made without asking you";
@@ -12976,21 +13000,17 @@ var CHECKS_SUBHEADING = "An agent says these pass and Halfcycle never ran them.
12976
13000
  function uncheckedClaimLine(test) {
12977
13001
  return `Unchecked: ${test.claimedBy} says "${test.label}" passes. Halfcycle never ran it.`;
12978
13002
  }
12979
- function answerClaimsInSystemViewLine(engagementId, phase) {
12980
- const address = systemViewUrl(engagementId, phase);
12981
- const only = "Accept or reject each one in the System view; this cannot be done by typing in chat";
12982
- return address === void 0 ? `${only}.` : `${only}: ${address}`;
12983
- }
12984
- function checksSectionLines(work, engagementId) {
13003
+ var ANSWER_CLAIMS_BY_TYPING_LINE = "To answer them, type the reject or accept line listed above in your own Claude Code session.";
13004
+ function checksSectionLines(work) {
12985
13005
  const unchecked = work.tasks.flatMap((task) => task.tests.filter((t) => t.state === "unchecked"));
12986
13006
  if (unchecked.length === 0)
12987
13007
  return [];
12988
- return [CHECKS_HEADING, CHECKS_SUBHEADING, ...unchecked.map(uncheckedClaimLine), answerClaimsInSystemViewLine(engagementId, work.phase)];
13008
+ return [CHECKS_HEADING, CHECKS_SUBHEADING, ...unchecked.map(uncheckedClaimLine), ANSWER_CLAIMS_BY_TYPING_LINE];
12989
13009
  }
12990
- function workSectionLines(work, engagementId) {
13010
+ function workSectionLines(work, _engagementId) {
12991
13011
  if (work === null)
12992
13012
  return [];
12993
- return [...callsSectionLines(work.calls), ...checksSectionLines(work, engagementId)];
13013
+ return [...callsSectionLines(work.calls), ...checksSectionLines(work)];
12994
13014
  }
12995
13015
  function rv8Line(view) {
12996
13016
  if (view.state === "closed") {
@@ -13132,7 +13152,8 @@ async function runReviewShow(repoRoot, phase, deps) {
13132
13152
  if (view.state !== "open")
13133
13153
  return refused(rv8Line(view));
13134
13154
  const work = await deps.readWork(credential, phase);
13135
- const stdout = `${[...reviewListingLines(view), ...workSectionLines(work.kind === "work" ? work.work : null, credential.engagementId)].join("\n")}
13155
+ const phaseWork = work.kind === "work" ? work.work : null;
13156
+ const stdout = `${[...reviewListingLines(view, phaseWork?.uncheckedClaims ?? []), ...workSectionLines(phaseWork, credential.engagementId)].join("\n")}
13136
13157
  `;
13137
13158
  const written = deps.recordShown(credential.engagementId, shownReviewEntry(view, deps.now().toISOString()));
13138
13159
  if (!written.ok)