halfcycle 0.3.27 → 0.3.29

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.3.27",
3
+ "version": "0.3.29",
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
  {
package/README.md CHANGED
@@ -130,7 +130,7 @@ Five layers, one direction, a loop at the bottom.
130
130
  | **L1** | Decide | The product spec, drafted then interrogated, with nothing left hand-waved. |
131
131
  | **L2** | Write it down | Architecture, a diagram, and the rules the system must never break, named so later work cites them. Diagram and prose must agree. |
132
132
  | **L3** | Slice it | Phases that actually close. Oversized work gets split. Assumptions get labelled as assumptions. |
133
- | **gate** | | **The readiness audit.** Eight checks over everything decided so far, each one a command that runs against your own repository rather than an opinion. Too many red is a hard stop. |
133
+ | **gate** | | **The readiness audit.** Eight checks over everything decided so far. Your session runs each one against your own repository and Halfcycle judges the answers — the documents this review reads never leave your machine. Too many red is a hard stop. |
134
134
  | **L4** | Design it | Feature specs. Every claim about existing code anchored to a real file and line or not asserted. A fresh reviewer that has never seen the spec, never the same one twice. |
135
135
  | **L5** | Build it | Ordered work, then coordinator and worker agents in parallel. Checks on the real changes, an anti-tautology pass over the tests, reachability, and a cold walk of the deployed product by somebody who did not build it. |
136
136
 
@@ -150,7 +150,9 @@ This matters enough to be specific about.
150
150
 
151
151
  **Source excerpts are off, at both ends, for every engagement today.** The wire format does have a field for them, so the honest version of this is a conditional statement rather than a flat one, and it rests on three things rather than on our good intentions: your runner does not attach an excerpt unless you turn a flag on, and that flag is off by default; anything that does arrive for an engagement that has not opted in is stripped on our side before the checks or any model see it; and there is no way to opt in today, because nothing in the product can set that flag. If that ever changes, our privacy policy changes with it, and it is the policy — not this paragraph — that states the guarantee.
152
152
 
153
- **A promise, for the day a check does need to read something you wrote.** No route in the product does this today — there is nothing to submit anything to, and we would rather say so than let a paragraph imply otherwise. When one exists, what you send it will be read and not kept: the finding and its evidence persist, the source does not, and training or benchmarking on it is foreclosed. It is written here now, labelled as a commitment rather than a feature, so the promise is public before the mechanism is.
153
+ **What the readiness audit submits, and what it never carries.** `halfcycle_submit_findings` is how your session sends the audit's answers: a probe's status, a count, a repo-relative path, a short name. Never a file body, a diff or a source excerpt — the wire carries no field for prose, so a path or a name is the most it can carry. What Halfcycle keeps is what your session submitted, the judgement it reached, and which account and machine sign-in the submission came from; the documents themselves never reach it.
154
+
155
+ **A promise, for the day a check does need to read something you wrote:** what you send it will be read and not kept: the finding and its evidence persist, the source does not, and training or benchmarking on it is foreclosed. It is written here now, labelled as a commitment rather than a feature, so the promise is public before the mechanism is.
154
156
 
155
157
  **Nothing method-shaped is left behind.** The runbooks, templates and question sets reach the session that asks for them, scoped to the step you are on, and are never at rest in your repository.
156
158
 
@@ -14530,7 +14530,7 @@ config(en_default());
14530
14530
 
14531
14531
  // ../core/dist/guard-record.js
14532
14532
  var severitySchema = external_exports.enum(["info", "warn", "block"]);
14533
- var channelSchema = external_exports.enum(["stable", "candidate", "rented"]);
14533
+ var channelSchema = external_exports.enum(["stable", "candidate", "rented", "withdrawn"]);
14534
14534
  var matcherKindSchema = external_exports.enum(["grep", "ast", "integrationTest", "llm"]);
14535
14535
  var matcherConfigSchema = external_exports.record(external_exports.string(), external_exports.unknown());
14536
14536
  var grepMatcherSchema = external_exports.object({ kind: external_exports.literal("grep"), config: matcherConfigSchema }).strict();
@@ -14997,6 +14997,18 @@ var artefactRefSchema = external_exports.object({
14997
14997
  name: external_exports.string(),
14998
14998
  parent: external_exports.string()
14999
14999
  }).strict();
15000
+ var coverageSchema = external_exports.object({
15001
+ asked: external_exports.number().int().nonnegative(),
15002
+ answered: external_exports.number().int().nonnegative()
15003
+ }).strict().superRefine((coverage, ctx) => {
15004
+ if (coverage.answered > coverage.asked) {
15005
+ ctx.addIssue({
15006
+ code: external_exports.ZodIssueCode.custom,
15007
+ path: ["answered"],
15008
+ message: "answered cannot be more than asked"
15009
+ });
15010
+ }
15011
+ });
15000
15012
  var artefactRecordSchema = stateRecordEnvelopeSchema.extend({
15001
15013
  recordKind: external_exports.literal("artefact"),
15002
15014
  artefactRef: artefactRefSchema,
@@ -15035,7 +15047,8 @@ var gateRecordSchema = stateRecordEnvelopeSchema.extend({
15035
15047
  gateKind: gateKindSchema,
15036
15048
  verdict: gateVerdictSchema,
15037
15049
  actor: external_exports.string(),
15038
- outcomeReason: external_exports.string().max(2e3).optional()
15050
+ outcomeReason: external_exports.string().max(2e3).optional(),
15051
+ coverage: coverageSchema.optional()
15039
15052
  }).strict().superRefine((rec, ctx) => {
15040
15053
  const issue2 = outcomeReasonIssue(rec.verdict, rec.outcomeReason);
15041
15054
  if (issue2) {
@@ -15048,7 +15061,8 @@ var interventionRecordSchema = stateRecordEnvelopeSchema.extend({
15048
15061
  mechanism: mechanismSchema,
15049
15062
  severity: severitySchema,
15050
15063
  disposition: dispositionSchema,
15051
- summary: external_exports.string().max(2e3)
15064
+ summary: external_exports.string().max(2e3),
15065
+ coverage: coverageSchema.optional()
15052
15066
  }).strict();
15053
15067
  var stateRecordSchema = external_exports.discriminatedUnion("recordKind", [
15054
15068
  artefactRecordSchema,
package/dist/bin.js CHANGED
@@ -18,7 +18,7 @@ import { z as z2 } from "zod";
18
18
  // ../core/dist/guard-record.js
19
19
  import { z } from "zod";
20
20
  var severitySchema = z.enum(["info", "warn", "block"]);
21
- var channelSchema = z.enum(["stable", "candidate", "rented"]);
21
+ var channelSchema = z.enum(["stable", "candidate", "rented", "withdrawn"]);
22
22
  var matcherKindSchema = z.enum(["grep", "ast", "integrationTest", "llm"]);
23
23
  var matcherConfigSchema = z.record(z.string(), z.unknown());
24
24
  var grepMatcherSchema = z.object({ kind: z.literal("grep"), config: matcherConfigSchema }).strict();
@@ -336,6 +336,20 @@ var coeFindingResponseSchema = z4.object({
336
336
  mappedPatternRef: z4.string().nullable(),
337
337
  createdAt: utcIso86012
338
338
  }).strict();
339
+ var operatorFindingResponseSchema = z4.object({
340
+ // The finding just recorded.
341
+ finding: coeFindingResponseSchema,
342
+ // Every finding recorded for that phase, oldest first, including this one.
343
+ phaseFindings: z4.array(coeFindingResponseSchema),
344
+ // Whether the phase's finding rate was recalculated to include this finding.
345
+ figuresRecomputed: z4.boolean()
346
+ }).strict();
347
+ var phaseNotOnRecordRefusalSchema = wireErrorSchema.extend({
348
+ // Always this value for this refusal.
349
+ error: z4.literal("PhaseNotOnRecord"),
350
+ // The phases that can take a finding, sorted.
351
+ phases: z4.array(z4.string())
352
+ }).strict();
339
353
  var phaseEntryDecisionSchema = z4.discriminatedUnion("decision", [
340
354
  z4.object({
341
355
  decision: z4.literal("entry-condition-met"),
@@ -491,6 +505,18 @@ var artefactRefSchema = z5.object({
491
505
  name: z5.string(),
492
506
  parent: z5.string()
493
507
  }).strict();
508
+ var coverageSchema = z5.object({
509
+ asked: z5.number().int().nonnegative(),
510
+ answered: z5.number().int().nonnegative()
511
+ }).strict().superRefine((coverage, ctx) => {
512
+ if (coverage.answered > coverage.asked) {
513
+ ctx.addIssue({
514
+ code: z5.ZodIssueCode.custom,
515
+ path: ["answered"],
516
+ message: "answered cannot be more than asked"
517
+ });
518
+ }
519
+ });
494
520
  var artefactRecordSchema = stateRecordEnvelopeSchema.extend({
495
521
  recordKind: z5.literal("artefact"),
496
522
  artefactRef: artefactRefSchema,
@@ -529,7 +555,8 @@ var gateRecordSchema = stateRecordEnvelopeSchema.extend({
529
555
  gateKind: gateKindSchema,
530
556
  verdict: gateVerdictSchema,
531
557
  actor: z5.string(),
532
- outcomeReason: z5.string().max(2e3).optional()
558
+ outcomeReason: z5.string().max(2e3).optional(),
559
+ coverage: coverageSchema.optional()
533
560
  }).strict().superRefine((rec, ctx) => {
534
561
  const issue = outcomeReasonIssue(rec.verdict, rec.outcomeReason);
535
562
  if (issue) {
@@ -542,7 +569,8 @@ var interventionRecordSchema = stateRecordEnvelopeSchema.extend({
542
569
  mechanism: mechanismSchema,
543
570
  severity: severitySchema,
544
571
  disposition: dispositionSchema,
545
- summary: z5.string().max(2e3)
572
+ summary: z5.string().max(2e3),
573
+ coverage: coverageSchema.optional()
546
574
  }).strict();
547
575
  var stateRecordSchema = z5.discriminatedUnion("recordKind", [
548
576
  artefactRecordSchema,
@@ -1493,6 +1521,11 @@ function mcpEndpointUrl(origin) {
1493
1521
  return `${origin.trim().replace(/\/+$/, "")}${MCP_ENDPOINT_PATH}`;
1494
1522
  }
1495
1523
 
1524
+ // dist/project-page.js
1525
+ function projectPageUrl(engagementId) {
1526
+ return isEngagementId(engagementId) ? `${DASHBOARD_URL}/${encodeURIComponent(engagementId)}` : void 0;
1527
+ }
1528
+
1496
1529
  // dist/merge-settings.js
1497
1530
  function isHalfcycleEntry(entry, halfcycleCommands) {
1498
1531
  return entry.hooks.some((h) => halfcycleCommands.has(h.command));
@@ -2507,12 +2540,14 @@ function writeBundlePin(targetRepoRoot, version, engagementId, engagementType, a
2507
2540
  const previous = previousPinForEngagement(targetRepoRoot, engagementId);
2508
2541
  const carried = nonEmpty(accountId) ?? nonEmpty(previous?.accountId);
2509
2542
  const installedAt = nonEmpty(previous?.installedAt) ?? (/* @__PURE__ */ new Date()).toISOString();
2543
+ const projectPage = projectPageUrl(engagementId);
2510
2544
  const pin = {
2511
2545
  version,
2512
2546
  engagementId,
2513
2547
  engagementType,
2514
2548
  installedAt,
2515
- ...carried !== void 0 ? { accountId: carried } : {}
2549
+ ...carried !== void 0 ? { accountId: carried } : {},
2550
+ ...projectPage !== void 0 ? { projectPage } : {}
2516
2551
  };
2517
2552
  const pinPath = join5(targetRepoRoot, BUNDLE_PIN_REL);
2518
2553
  writeAllowlisted(pinPath, targetRepoRoot, JSON.stringify(pin, null, 2) + "\n", writtenPaths);
@@ -2840,11 +2875,25 @@ import { createServer } from "node:http";
2840
2875
  import { randomBytes, timingSafeEqual } from "node:crypto";
2841
2876
  var LOOPBACK_PORT_ENV = "HALFCYCLE_LOOPBACK_PORT";
2842
2877
  var DEFAULT_STILL_WAITING_MS = 15e3;
2843
- function completionPage(result) {
2878
+ var COMPLETION_REDIRECT_SECONDS = 2;
2879
+ function onwardTarget(engagementId) {
2880
+ const projectPage = projectPageUrl(engagementId);
2881
+ if (projectPage !== void 0) {
2882
+ return { href: projectPage, label: "Go to your project's page" };
2883
+ }
2884
+ return { href: DASHBOARD_URL, label: "Go to your projects" };
2885
+ }
2886
+ function escapeHtml(value) {
2887
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2888
+ }
2889
+ function completionPage(result, engagementId) {
2844
2890
  const approved = result === LOOPBACK_RESULT.APPROVED;
2845
2891
  const headline = approved ? "Signed in. You can close this tab and return to your terminal." : "Sign-in declined. Nothing was created. You can close this tab.";
2846
- const successLink = approved ? `<p><a href="${DASHBOARD_URL}" style="color:#19d3ff">Go to your projects</a></p>` : "";
2847
- return `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Halfcycle</title></head><body style="margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;background:#0a0a0a;color:#fafafa;font:16px/1.6 -apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif"><div style="max-width:26rem;padding:2rem;text-align:center"><p>${headline}</p>${successLink}</div></body></html>`;
2892
+ const onward = approved ? onwardTarget(engagementId) : null;
2893
+ const href = onward === null ? "" : escapeHtml(onward.href);
2894
+ const refresh = onward === null ? "" : `<meta http-equiv="refresh" content="${COMPLETION_REDIRECT_SECONDS};url=${href}">`;
2895
+ const successLink = onward === null ? "" : `<p><a href="${href}" style="color:#19d3ff">${escapeHtml(onward.label)}</a></p>`;
2896
+ return '<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="referrer" content="no-referrer">' + refresh + `<title>Halfcycle</title></head><body style="margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;background:#0a0a0a;color:#fafafa;font:16px/1.6 -apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif"><div style="max-width:26rem;padding:2rem;text-align:center"><p>${headline}</p>${successLink}</div></body></html>`;
2848
2897
  }
2849
2898
  function isLoopbackPeer(remoteAddress) {
2850
2899
  if (remoteAddress === void 0)
@@ -2859,7 +2908,7 @@ function statesMatch(presented, expected) {
2859
2908
  return false;
2860
2909
  return timingSafeEqual(a, b);
2861
2910
  }
2862
- function createCallbackHandler(state, onCallback) {
2911
+ function createCallbackHandler(state, onCallback, engagementId) {
2863
2912
  return (req, res) => {
2864
2913
  if (!isLoopbackPeer(req.socket.remoteAddress)) {
2865
2914
  res.writeHead(403).end();
@@ -2883,7 +2932,7 @@ function createCallbackHandler(state, onCallback) {
2883
2932
  const declared = url.searchParams.get(LOOPBACK_QUERY_RESULT);
2884
2933
  const result = declared === LOOPBACK_RESULT.DECLINED ? LOOPBACK_RESULT.DECLINED : LOOPBACK_RESULT.APPROVED;
2885
2934
  res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
2886
- res.end(completionPage(result));
2935
+ res.end(completionPage(result, engagementId));
2887
2936
  onCallback({ result });
2888
2937
  };
2889
2938
  }
@@ -2902,7 +2951,7 @@ function requestedPort(env) {
2902
2951
  }
2903
2952
  return { port };
2904
2953
  }
2905
- async function bindLoopback(env = process.env) {
2954
+ async function bindLoopback(env = process.env, engagementId) {
2906
2955
  const wanted = requestedPort(env);
2907
2956
  if ("problem" in wanted)
2908
2957
  return { bound: false, reason: wanted.problem };
@@ -2914,7 +2963,7 @@ async function bindLoopback(env = process.env) {
2914
2963
  deliver(callback);
2915
2964
  else
2916
2965
  pending = callback;
2917
- }));
2966
+ }, engagementId));
2918
2967
  const bindProblem = await new Promise((resolve4) => {
2919
2968
  const onError = (err) => {
2920
2969
  resolve4(`${err.code ?? "bind failed"} on ${LOOPBACK_REDIRECT_HOST}:${wanted.port}`);
@@ -3171,7 +3220,7 @@ async function signIn(serviceUrl, deps = {}) {
3171
3220
  }
3172
3221
  write(`[halfcycle] Halfcycle needs an account before it can ${deps.reason ?? DEFAULT_SIGN_IN_REASON} \u2014 signing you in through your browser.
3173
3222
  `);
3174
- const bind = await bindLoopback(env);
3223
+ const bind = await bindLoopback(env, deps.onwardEngagementId);
3175
3224
  let closed = false;
3176
3225
  const closeListener = async () => {
3177
3226
  if (bind.bound && !closed) {
@@ -3950,8 +3999,14 @@ async function createOwnedEngagement(serviceUrl, targetRepo, deps = {}) {
3950
3999
  }
3951
4000
  }
3952
4001
  var JOIN_REFUSED_LOCAL_REMEDY = "No credential was minted and nothing was written. This repository is pinned to that engagement by .halfcycle/bundle.json \u2014 check that the id in it is the one you meant. To start a NEW engagement here instead, remove that file and re-run.";
4002
+ var JOIN_SIGN_IN_REASON = "join an existing project";
3953
4003
  async function joinPinnedEngagement(serviceUrl, engagementId, targetRepo, deps = {}) {
3954
- let credential = await confirmActingIdentity(serviceUrl, await obtainCredential(serviceUrl, deps), { kind: "join", targetRepo, engagementId }, deps);
4004
+ const signInDeps = {
4005
+ ...deps,
4006
+ reason: deps.reason ?? JOIN_SIGN_IN_REASON,
4007
+ onwardEngagementId: engagementId
4008
+ };
4009
+ let credential = await confirmActingIdentity(serviceUrl, await obtainCredential(serviceUrl, signInDeps), { kind: "join", targetRepo, engagementId }, signInDeps);
3955
4010
  for (; ; ) {
3956
4011
  try {
3957
4012
  return {
@@ -3979,7 +4034,7 @@ async function joinPinnedEngagement(serviceUrl, engagementId, targetRepo, deps =
3979
4034
  if (err.status !== 401) {
3980
4035
  throw err;
3981
4036
  }
3982
- const replacement = await replaceRefusedCredential(serviceUrl, credential, deps);
4037
+ const replacement = await replaceRefusedCredential(serviceUrl, credential, signInDeps);
3983
4038
  if (replacement === null) {
3984
4039
  throw new SignInRefused(`join-${err.status}`, `${err.serverMessage} ${refusedCredentialRemedy(credential.source)} No credential was minted and nothing was written.`);
3985
4040
  }