halfcycle 0.3.27 → 0.3.28

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.28",
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
 
@@ -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
@@ -491,6 +491,18 @@ var artefactRefSchema = z5.object({
491
491
  name: z5.string(),
492
492
  parent: z5.string()
493
493
  }).strict();
494
+ var coverageSchema = z5.object({
495
+ asked: z5.number().int().nonnegative(),
496
+ answered: z5.number().int().nonnegative()
497
+ }).strict().superRefine((coverage, ctx) => {
498
+ if (coverage.answered > coverage.asked) {
499
+ ctx.addIssue({
500
+ code: z5.ZodIssueCode.custom,
501
+ path: ["answered"],
502
+ message: "answered cannot be more than asked"
503
+ });
504
+ }
505
+ });
494
506
  var artefactRecordSchema = stateRecordEnvelopeSchema.extend({
495
507
  recordKind: z5.literal("artefact"),
496
508
  artefactRef: artefactRefSchema,
@@ -529,7 +541,8 @@ var gateRecordSchema = stateRecordEnvelopeSchema.extend({
529
541
  gateKind: gateKindSchema,
530
542
  verdict: gateVerdictSchema,
531
543
  actor: z5.string(),
532
- outcomeReason: z5.string().max(2e3).optional()
544
+ outcomeReason: z5.string().max(2e3).optional(),
545
+ coverage: coverageSchema.optional()
533
546
  }).strict().superRefine((rec, ctx) => {
534
547
  const issue = outcomeReasonIssue(rec.verdict, rec.outcomeReason);
535
548
  if (issue) {
@@ -542,7 +555,8 @@ var interventionRecordSchema = stateRecordEnvelopeSchema.extend({
542
555
  mechanism: mechanismSchema,
543
556
  severity: severitySchema,
544
557
  disposition: dispositionSchema,
545
- summary: z5.string().max(2e3)
558
+ summary: z5.string().max(2e3),
559
+ coverage: coverageSchema.optional()
546
560
  }).strict();
547
561
  var stateRecordSchema = z5.discriminatedUnion("recordKind", [
548
562
  artefactRecordSchema,
@@ -2840,11 +2854,27 @@ import { createServer } from "node:http";
2840
2854
  import { randomBytes, timingSafeEqual } from "node:crypto";
2841
2855
  var LOOPBACK_PORT_ENV = "HALFCYCLE_LOOPBACK_PORT";
2842
2856
  var DEFAULT_STILL_WAITING_MS = 15e3;
2843
- function completionPage(result) {
2857
+ var COMPLETION_REDIRECT_SECONDS = 2;
2858
+ function onwardTarget(engagementId) {
2859
+ if (isEngagementId(engagementId)) {
2860
+ return {
2861
+ href: `${DASHBOARD_URL}/${encodeURIComponent(engagementId)}`,
2862
+ label: "Go to your project's page"
2863
+ };
2864
+ }
2865
+ return { href: DASHBOARD_URL, label: "Go to your projects" };
2866
+ }
2867
+ function escapeHtml(value) {
2868
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2869
+ }
2870
+ function completionPage(result, engagementId) {
2844
2871
  const approved = result === LOOPBACK_RESULT.APPROVED;
2845
2872
  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>`;
2873
+ const onward = approved ? onwardTarget(engagementId) : null;
2874
+ const href = onward === null ? "" : escapeHtml(onward.href);
2875
+ const refresh = onward === null ? "" : `<meta http-equiv="refresh" content="${COMPLETION_REDIRECT_SECONDS};url=${href}">`;
2876
+ const successLink = onward === null ? "" : `<p><a href="${href}" style="color:#19d3ff">${escapeHtml(onward.label)}</a></p>`;
2877
+ 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
2878
  }
2849
2879
  function isLoopbackPeer(remoteAddress) {
2850
2880
  if (remoteAddress === void 0)
@@ -2859,7 +2889,7 @@ function statesMatch(presented, expected) {
2859
2889
  return false;
2860
2890
  return timingSafeEqual(a, b);
2861
2891
  }
2862
- function createCallbackHandler(state, onCallback) {
2892
+ function createCallbackHandler(state, onCallback, engagementId) {
2863
2893
  return (req, res) => {
2864
2894
  if (!isLoopbackPeer(req.socket.remoteAddress)) {
2865
2895
  res.writeHead(403).end();
@@ -2883,7 +2913,7 @@ function createCallbackHandler(state, onCallback) {
2883
2913
  const declared = url.searchParams.get(LOOPBACK_QUERY_RESULT);
2884
2914
  const result = declared === LOOPBACK_RESULT.DECLINED ? LOOPBACK_RESULT.DECLINED : LOOPBACK_RESULT.APPROVED;
2885
2915
  res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
2886
- res.end(completionPage(result));
2916
+ res.end(completionPage(result, engagementId));
2887
2917
  onCallback({ result });
2888
2918
  };
2889
2919
  }
@@ -2902,7 +2932,7 @@ function requestedPort(env) {
2902
2932
  }
2903
2933
  return { port };
2904
2934
  }
2905
- async function bindLoopback(env = process.env) {
2935
+ async function bindLoopback(env = process.env, engagementId) {
2906
2936
  const wanted = requestedPort(env);
2907
2937
  if ("problem" in wanted)
2908
2938
  return { bound: false, reason: wanted.problem };
@@ -2914,7 +2944,7 @@ async function bindLoopback(env = process.env) {
2914
2944
  deliver(callback);
2915
2945
  else
2916
2946
  pending = callback;
2917
- }));
2947
+ }, engagementId));
2918
2948
  const bindProblem = await new Promise((resolve4) => {
2919
2949
  const onError = (err) => {
2920
2950
  resolve4(`${err.code ?? "bind failed"} on ${LOOPBACK_REDIRECT_HOST}:${wanted.port}`);
@@ -3171,7 +3201,7 @@ async function signIn(serviceUrl, deps = {}) {
3171
3201
  }
3172
3202
  write(`[halfcycle] Halfcycle needs an account before it can ${deps.reason ?? DEFAULT_SIGN_IN_REASON} \u2014 signing you in through your browser.
3173
3203
  `);
3174
- const bind = await bindLoopback(env);
3204
+ const bind = await bindLoopback(env, deps.onwardEngagementId);
3175
3205
  let closed = false;
3176
3206
  const closeListener = async () => {
3177
3207
  if (bind.bound && !closed) {
@@ -3950,8 +3980,14 @@ async function createOwnedEngagement(serviceUrl, targetRepo, deps = {}) {
3950
3980
  }
3951
3981
  }
3952
3982
  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.";
3983
+ var JOIN_SIGN_IN_REASON = "join an existing project";
3953
3984
  async function joinPinnedEngagement(serviceUrl, engagementId, targetRepo, deps = {}) {
3954
- let credential = await confirmActingIdentity(serviceUrl, await obtainCredential(serviceUrl, deps), { kind: "join", targetRepo, engagementId }, deps);
3985
+ const signInDeps = {
3986
+ ...deps,
3987
+ reason: deps.reason ?? JOIN_SIGN_IN_REASON,
3988
+ onwardEngagementId: engagementId
3989
+ };
3990
+ let credential = await confirmActingIdentity(serviceUrl, await obtainCredential(serviceUrl, signInDeps), { kind: "join", targetRepo, engagementId }, signInDeps);
3955
3991
  for (; ; ) {
3956
3992
  try {
3957
3993
  return {
@@ -3979,7 +4015,7 @@ async function joinPinnedEngagement(serviceUrl, engagementId, targetRepo, deps =
3979
4015
  if (err.status !== 401) {
3980
4016
  throw err;
3981
4017
  }
3982
- const replacement = await replaceRefusedCredential(serviceUrl, credential, deps);
4018
+ const replacement = await replaceRefusedCredential(serviceUrl, credential, signInDeps);
3983
4019
  if (replacement === null) {
3984
4020
  throw new SignInRefused(`join-${err.status}`, `${err.serverMessage} ${refusedCredentialRemedy(credential.source)} No credential was minted and nothing was written.`);
3985
4021
  }