halfcycle 0.5.2 → 0.5.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.5.2",
3
+ "version": "0.5.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
  {
@@ -18103,6 +18103,30 @@ var phaseReviewViewSchema = external_exports.discriminatedUnion("state", [
18103
18103
  phaseReviewOpenSchema,
18104
18104
  phaseReviewClosedSchema
18105
18105
  ]);
18106
+ function reviewPartAwaitingAnswer(part) {
18107
+ return part.questions.some((q) => q.agentAnswer === void 0);
18108
+ }
18109
+ function reviewPartSentBackIn(part, round) {
18110
+ return part.sentBack.some((s) => s.round === round);
18111
+ }
18112
+ function shownReviewEntryOf(view, shownAt) {
18113
+ const parts = view.parts.map((p) => ({
18114
+ part: p.part,
18115
+ name: p.name,
18116
+ built: p.built,
18117
+ decidable: p.decidable,
18118
+ awaitingAnswer: reviewPartAwaitingAnswer(p),
18119
+ sentBack: reviewPartSentBackIn(p, view.round)
18120
+ }));
18121
+ return {
18122
+ phase: view.phase,
18123
+ number: view.number,
18124
+ round: view.round,
18125
+ reviewVersion: view.reviewVersion,
18126
+ shownAt,
18127
+ parts
18128
+ };
18129
+ }
18106
18130
 
18107
18131
  // ../events/dist/client-version.js
18108
18132
  var CLIENT_VERSION_HEADER = "x-halfcycle-client";
@@ -18563,6 +18587,42 @@ var ownerWaitDeclarationSchema = external_exports.object({
18563
18587
  var ownerWaitDeclarationsSchema = external_exports.object({ declarations: external_exports.array(ownerWaitDeclarationSchema) }).strict();
18564
18588
  var OWNER_WAIT_TOO_MANY = `This project already has ${String(OWNER_WAIT_ADDITIONS_MAX)} things added to what waits for the owner. Ask the owner to take one off on their settings page in the console first; it cannot be done by typing in chat.`;
18565
18589
 
18590
+ // ../events/dist/owner-wait-answer.js
18591
+ var OWNER_WAIT_SHORT_ID_LENGTH = 8;
18592
+ var ownerWaitShortIdSchema = external_exports.string().regex(/^[0-9a-f]{8}$/);
18593
+ function ownerWaitShortIdOf(declarationId) {
18594
+ return declarationId.slice(0, OWNER_WAIT_SHORT_ID_LENGTH).toLowerCase();
18595
+ }
18596
+ var OWNER_WAIT_ANSWER_ACTS = ["approve", "decline"];
18597
+ var ownerWaitAnswerActSchema = external_exports.enum(OWNER_WAIT_ANSWER_ACTS);
18598
+ function ownerWaitApproveLineOf(declarationId) {
18599
+ return `approve wait ${ownerWaitShortIdOf(declarationId)}`;
18600
+ }
18601
+ function ownerWaitDeclineLineOf(declarationId) {
18602
+ return `decline wait ${ownerWaitShortIdOf(declarationId)}: <why>`;
18603
+ }
18604
+ var ownerWaitChatRequestSchema = external_exports.discriminatedUnion("act", [
18605
+ external_exports.object({ act: external_exports.literal("approve"), wait: ownerWaitShortIdSchema }).strict(),
18606
+ external_exports.object({ act: external_exports.literal("decline"), wait: ownerWaitShortIdSchema, note: scopeNoteWireSchema }).strict()
18607
+ ]);
18608
+ function bareOutcome3(outcome) {
18609
+ return external_exports.object({ outcome: external_exports.literal(outcome) }).strict();
18610
+ }
18611
+ var ownerWaitChatResultSchema = external_exports.discriminatedUnion("outcome", [
18612
+ external_exports.object({
18613
+ outcome: external_exports.literal("recorded"),
18614
+ act: ownerWaitAnswerActSchema,
18615
+ question: external_exports.string().min(1)
18616
+ }).strict(),
18617
+ external_exports.object({ outcome: external_exports.literal("refused"), problems: external_exports.array(systemDescriptionProblemSchema).min(1) }).strict(),
18618
+ bareOutcome3("not-owner"),
18619
+ bareOutcome3("no-owner"),
18620
+ bareOutcome3("not-found"),
18621
+ bareOutcome3("ambiguous"),
18622
+ bareOutcome3("not-offered"),
18623
+ bareOutcome3("unavailable")
18624
+ ]);
18625
+
18566
18626
  // ../events/dist/pacing.js
18567
18627
  var CLAUDE_PLANS = Object.freeze([
18568
18628
  Object.freeze({ id: "pro", words: "Pro", startingAgents: 2 }),
@@ -18765,6 +18825,8 @@ var autonomyActivityRowSchema = external_exports.object({
18765
18825
  // whether the agent says it can be undone
18766
18826
  how: statementText(STATEMENT_MAX).optional(),
18767
18827
  // how to undo it, when it can be
18828
+ wait: ownerWaitShortIdSchema.optional(),
18829
+ // the short id the owner types to answer a waiting decision
18768
18830
  at: utcIso8601
18769
18831
  // when it happened
18770
18832
  }).strict().superRefine((row, ctx) => {
@@ -18791,6 +18853,9 @@ var autonomyActivityRowSchema = external_exports.object({
18791
18853
  });
18792
18854
  }
18793
18855
  }
18856
+ if (row.kind !== "waiting" && row.wait !== void 0) {
18857
+ ctx.addIssue({ code: "custom", message: "only a waiting decision is answered by typing", path: ["wait"] });
18858
+ }
18794
18859
  if (row.kind === "waiting" && (row.undoable !== void 0 || row.how !== void 0)) {
18795
18860
  ctx.addIssue({ code: "custom", message: "a waiting decision has not been made yet", path: ["undoable"] });
18796
18861
  }
@@ -31287,6 +31352,14 @@ function partShape(act, head, words) {
31287
31352
  return null;
31288
31353
  return { kind: "part", act, reference, part, words };
31289
31354
  }
31355
+ function bulkAcceptTargetOf(reference, record2) {
31356
+ const phases = /* @__PURE__ */ new Map();
31357
+ for (const shown2 of [...record2.reviews, ...record2.entries]) {
31358
+ if (!phases.has(shown2.phase))
31359
+ phases.set(shown2.phase, { phase: shown2.phase, number: shown2.number });
31360
+ }
31361
+ return resolvePhaseReference(reference, [...phases.values()]);
31362
+ }
31290
31363
  function matchReviewPhraseShape(shape, reviews) {
31291
31364
  const resolved = resolvePhaseReference(shape.reference, reviews);
31292
31365
  const act = shape.kind === "phase" ? "accept" : shape.kind === "reject" ? "reject" : shape.act;
@@ -31377,6 +31450,28 @@ function moveListedReviewForward(engagementId, home, listed, reviewVersion, deci
31377
31450
  }
31378
31451
  }
31379
31452
 
31453
+ // dist/hooks/wait-phrase.js
31454
+ var APPROVE = /^approve wait ([0-9a-f]{8})$/i;
31455
+ var DECLINE = /^decline wait ([0-9a-f]{8})(?:\s*:(.*))?$/is;
31456
+ function waitPhraseShape(prompt) {
31457
+ if (typeof prompt !== "string")
31458
+ return null;
31459
+ const line2 = prompt.trim();
31460
+ if (LINE_BREAK.test(line2))
31461
+ return null;
31462
+ const approve = APPROVE.exec(line2);
31463
+ if (approve !== null)
31464
+ return shapeOf("approve", approve[1], null);
31465
+ const decline = DECLINE.exec(line2);
31466
+ if (decline !== null)
31467
+ return shapeOf("decline", decline[1], decline[2] === void 0 ? null : decline[2].trim());
31468
+ return null;
31469
+ }
31470
+ function shapeOf(act, typed, words) {
31471
+ const wait = typed.toLowerCase();
31472
+ return ownerWaitShortIdSchema.safeParse(wait).success ? { act, wait, words } : null;
31473
+ }
31474
+
31380
31475
  // dist/hooks/user-prompt-submit-lines.js
31381
31476
  var TELL_THE_PERSON = "Tell the person.";
31382
31477
  function line(text) {
@@ -31744,6 +31839,69 @@ function codexReviewLine(act) {
31744
31839
  function codexAutonomyLine() {
31745
31840
  return codexLine(`${AUTONOMY_NOTHING_CHANGED}: ${CODEX_TYPING_NEVER_COUNTS}. Turn it on or off with "${AUTONOMY_A1_TITLE}" in the console's settings.`);
31746
31841
  }
31842
+ function waitTypedLine(context) {
31843
+ return context.act === "approve" ? ownerWaitApproveLineOf(context.wait) : ownerWaitDeclineLineOf(context.wait);
31844
+ }
31845
+ function waitRecordedLine(act, question) {
31846
+ return act === "approve" ? line(`Recorded in your session: you approved "${question}". The agent can go ahead with it.`) : line(`Recorded in your session: you declined "${question}", with your words. The agent must not go ahead with it.`);
31847
+ }
31848
+ function waitNotAttendedLine() {
31849
+ return line(`${NOTHING_RECORDED}: an answer counts only when a person types it into an open session.`);
31850
+ }
31851
+ function waitNotSignedInLine() {
31852
+ return line(`${NOTHING_RECORDED}: this project is not signed in on this computer. ${HOOK_REMEDY}`);
31853
+ }
31854
+ function waitNotOwnerLine() {
31855
+ return line(`${NOTHING_RECORDED}: only the project's owner can answer a decision that waits for them.`);
31856
+ }
31857
+ function waitNoOwnerLine() {
31858
+ return line(`${NOTHING_RECORDED}: this project has no owner, so nobody can answer its decisions.`);
31859
+ }
31860
+ function waitNotFoundLine(context) {
31861
+ return line(`${NOTHING_RECORDED}: no decision waiting for you has the id ${context.wait}. Type the line the agent gave you with its question, such as ${waitTypedLine(context)}.`);
31862
+ }
31863
+ function waitAmbiguousLine(context) {
31864
+ return line(`${NOTHING_RECORDED}: more than one decision waiting for you starts with ${context.wait}. Ask the agent to put the question to you again.`);
31865
+ }
31866
+ function waitNoWordsLine(context) {
31867
+ return line(`${NOTHING_RECORDED}: a decline needs your reason after the colon, as in ${ownerWaitDeclineLineOf(context.wait)}.`);
31868
+ }
31869
+ function waitRefusedLine() {
31870
+ return line(`${NOTHING_RECORDED}: Halfcycle did not accept the words. Reword them in plain words and type the line again.`);
31871
+ }
31872
+ function waitSwitchedOffLine() {
31873
+ return line(`${NOTHING_RECORDED}. ${TYPED_APPROVAL_OFF_SENTENCE}`);
31874
+ }
31875
+ function waitCannotReachLine() {
31876
+ return line(`${NOTHING_RECORDED}: Halfcycle could not be reached just now. Type the line again in a minute.`);
31877
+ }
31878
+ function codexWaitLine(context) {
31879
+ return codexLine(`${NOTHING_RECORDED}: ${CODEX_TYPING_NEVER_COUNTS}. To answer, type ${waitTypedLine(context)} in a Claude Code session on this project.`);
31880
+ }
31881
+ function waitLineForAnswer(context, answer) {
31882
+ switch (answer.outcome) {
31883
+ case "recorded":
31884
+ return waitRecordedLine(answer.act, answer.question);
31885
+ case "refused":
31886
+ return waitRefusedLine();
31887
+ case "not-owner":
31888
+ return waitNotOwnerLine();
31889
+ case "no-owner":
31890
+ return waitNoOwnerLine();
31891
+ case "not-found":
31892
+ return waitNotFoundLine(context);
31893
+ case "ambiguous":
31894
+ return waitAmbiguousLine(context);
31895
+ case "not-offered":
31896
+ return waitSwitchedOffLine();
31897
+ case "unavailable":
31898
+ return waitCannotReachLine();
31899
+ default: {
31900
+ const unreachable = answer;
31901
+ return unreachable;
31902
+ }
31903
+ }
31904
+ }
31747
31905
 
31748
31906
  // dist/hooks/user-prompt-submit.js
31749
31907
  var CHAT_REQUEST_TIMEOUT_MS = 6e3;
@@ -31768,6 +31926,7 @@ function agentOf(opts) {
31768
31926
  async function decideLine(opts) {
31769
31927
  let act = null;
31770
31928
  let autonomyAct = null;
31929
+ let waitRecognised = false;
31771
31930
  const review = { recognised: false };
31772
31931
  try {
31773
31932
  const prompt = promptOf(opts.input);
@@ -31778,6 +31937,12 @@ async function decideLine(opts) {
31778
31937
  if (autonomyShape !== null) {
31779
31938
  autonomyAct = autonomyShape;
31780
31939
  return `${await decideAutonomy(autonomyShape, opts)}
31940
+ `;
31941
+ }
31942
+ const waitShape = waitPhraseShape(prompt);
31943
+ if (waitShape !== null) {
31944
+ waitRecognised = true;
31945
+ return `${await decideWait(waitShape, opts)}
31781
31946
  `;
31782
31947
  }
31783
31948
  const reviewShape = reviewPhraseShape(prompt);
@@ -31795,6 +31960,9 @@ async function decideLine(opts) {
31795
31960
  } catch {
31796
31961
  if (autonomyAct !== null)
31797
31962
  return `${autonomyCannotReachLine()}
31963
+ `;
31964
+ if (waitRecognised)
31965
+ return `${waitCannotReachLine()}
31798
31966
  `;
31799
31967
  if (review.recognised)
31800
31968
  return `${reviewCannotReachLine()}
@@ -31840,6 +32008,57 @@ async function decideAutonomy(act, opts) {
31840
32008
  const { guardServiceToken, guardEngagementId } = read.config;
31841
32009
  return postAutonomy(act, guardEngagementId, guardServiceToken, env, opts.fetchFn);
31842
32010
  }
32011
+ async function decideWait(shape, opts) {
32012
+ const env = opts.env ?? process.env;
32013
+ const context = { act: shape.act, wait: shape.wait };
32014
+ const typed = whoTyped(opts);
32015
+ if (typed === "cannot-tell")
32016
+ return codexWaitLine(context);
32017
+ if (typed === "not-attended")
32018
+ return waitNotAttendedLine();
32019
+ const read = readConfig(env);
32020
+ if (read.config === null) {
32021
+ return readNeverConfiguredReason(env) === "not-this-account" ? waitNotOwnerLine() : waitNotSignedInLine();
32022
+ }
32023
+ let candidate = { act: "approve", wait: shape.wait };
32024
+ if (shape.act === "decline") {
32025
+ const words = shape.words ?? "";
32026
+ if (words === "")
32027
+ return waitNoWordsLine(context);
32028
+ if (!scopeNoteWireSchema.safeParse(words).success)
32029
+ return reviewWordsTooLongLine();
32030
+ if (looksLikeSecret(words))
32031
+ return reviewWordsLookSecretLine();
32032
+ candidate = { act: "decline", wait: shape.wait, note: words };
32033
+ }
32034
+ const request = ownerWaitChatRequestSchema.parse(candidate);
32035
+ const { guardServiceToken, guardEngagementId } = read.config;
32036
+ const url2 = `${readControlOrigin(env).origin}/engagements/${encodeURIComponent(guardEngagementId)}/owner-waits/chat`;
32037
+ const controller = new AbortController();
32038
+ const timer = setTimeout(() => controller.abort(), CHAT_REQUEST_TIMEOUT_MS);
32039
+ try {
32040
+ const response = await (opts.fetchFn ?? globalThis.fetch)(url2, {
32041
+ method: "POST",
32042
+ headers: { Authorization: `Bearer ${guardServiceToken}`, "Content-Type": "application/json" },
32043
+ body: JSON.stringify(request),
32044
+ signal: controller.signal
32045
+ });
32046
+ if (response.status === 401)
32047
+ return waitNotSignedInLine();
32048
+ if (response.status === 429 || response.status >= 500)
32049
+ return waitCannotReachLine();
32050
+ const parsed = ownerWaitChatResultSchema.safeParse(await response.json());
32051
+ if (!parsed.success)
32052
+ return waitCannotReachLine();
32053
+ if (parsed.data.outcome === "recorded" && response.status !== 200)
32054
+ return waitCannotReachLine();
32055
+ return waitLineForAnswer(context, parsed.data);
32056
+ } catch {
32057
+ return waitCannotReachLine();
32058
+ } finally {
32059
+ clearTimeout(timer);
32060
+ }
32061
+ }
31843
32062
  async function postAutonomy(act, engagementId, credential, env, fetchFn = globalThis.fetch) {
31844
32063
  const url2 = `${readControlOrigin(env).origin}/engagements/${encodeURIComponent(engagementId)}/autonomy/chat`;
31845
32064
  const controller = new AbortController();
@@ -31873,8 +32092,12 @@ async function decideReview(shape, opts, progress) {
31873
32092
  const read2 = readConfig(env);
31874
32093
  if (read2.config === null)
31875
32094
  return "";
31876
- const match2 = matchReviewPhraseShape(shape, readShownRecord(read2.config.guardEngagementId, opts.home).reviews);
31877
- if (match2.kind === "nothing")
32095
+ const { guardEngagementId: engagementId, guardServiceToken: credential } = read2.config;
32096
+ const record2 = readShownRecord(engagementId, opts.home);
32097
+ const listed = matchReviewPhraseShape(shape, record2.reviews);
32098
+ const fresh = listed.kind === "accept" && listedRecently(listed.entry.shownAt, (opts.now ?? Date.now)());
32099
+ const target = listed.kind === "ambiguous" || fresh ? null : bulkAcceptTargetOf(shape.reference, record2);
32100
+ if (target?.kind === "none")
31878
32101
  return "";
31879
32102
  progress.recognised = true;
31880
32103
  const typed2 = whoTyped(opts);
@@ -31882,7 +32105,11 @@ async function decideReview(shape, opts, progress) {
31882
32105
  return codexReviewLine("accept");
31883
32106
  if (typed2 === "not-attended")
31884
32107
  return reviewNotAttendedLine();
31885
- return checkAndSendReview(match2, read2.config.guardEngagementId, read2.config.guardServiceToken, env, opts);
32108
+ if (target === null)
32109
+ return checkAndSendReview(listed, engagementId, credential, env, opts);
32110
+ if (target.kind === "ambiguous")
32111
+ return reviewAmbiguousLine(target.entries.map((e) => e.phase));
32112
+ return readThenAccept(shape, target.entry.phase, engagementId, credential, env, opts);
31886
32113
  }
31887
32114
  progress.recognised = true;
31888
32115
  const typed = whoTyped(opts);
@@ -31897,6 +32124,38 @@ async function decideReview(shape, opts, progress) {
31897
32124
  const match = matchReviewPhraseShape(shape, readShownRecord(read.config.guardEngagementId, opts.home).reviews);
31898
32125
  return checkAndSendReview(match, read.config.guardEngagementId, read.config.guardServiceToken, env, opts);
31899
32126
  }
32127
+ var REVIEW_READ_TIMEOUT_MS = 3e3;
32128
+ async function readThenAccept(shape, phase, engagementId, credential, env, opts) {
32129
+ const fetchFn = opts.fetchFn ?? globalThis.fetch;
32130
+ const url2 = `${readControlOrigin(env).origin}/engagements/${encodeURIComponent(engagementId)}/phases/${encodeURIComponent(phase)}/review`;
32131
+ const controller = new AbortController();
32132
+ const timer = setTimeout(() => controller.abort(), REVIEW_READ_TIMEOUT_MS);
32133
+ let raw;
32134
+ try {
32135
+ const response = await fetchFn(url2, {
32136
+ method: "GET",
32137
+ headers: { Authorization: `Bearer ${credential}` },
32138
+ signal: controller.signal
32139
+ });
32140
+ if (response.status === 401)
32141
+ return reviewNotSignedInLine();
32142
+ if (response.status !== 200)
32143
+ return reviewCannotReachLine();
32144
+ raw = await response.json();
32145
+ } catch {
32146
+ return reviewCannotReachLine();
32147
+ } finally {
32148
+ clearTimeout(timer);
32149
+ }
32150
+ const parsed = phaseReviewViewSchema.safeParse(raw);
32151
+ if (!parsed.success)
32152
+ return reviewCannotReachLine();
32153
+ const view = parsed.data;
32154
+ if (view.state !== "open")
32155
+ return reviewNoReviewLine(phase);
32156
+ const entry = shownReviewEntryOf(view, new Date((opts.now ?? Date.now)()).toISOString());
32157
+ return checkAndSendReview(matchReviewPhraseShape(shape, [entry]), engagementId, credential, env, opts);
32158
+ }
31900
32159
  async function checkAndSendReview(match, engagementId, credential, env, opts) {
31901
32160
  if (match.kind === "nothing" || match.kind === "show-first")
31902
32161
  return reviewShowFirstLine();