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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +4 -2
- package/bin/bin.bundle.mjs +16 -2
- package/dist/bin.js +48 -12
- package/dist/bin.js.map +1 -1
- package/dist/device-signin.d.ts +17 -10
- package/dist/device-signin.d.ts.map +1 -1
- package/dist/index.js +16 -2
- package/dist/index.js.map +1 -1
- package/dist/loopback-signin.d.ts +25 -5
- package/dist/loopback-signin.d.ts.map +1 -1
- package/dist/own-engagement.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "halfcycle",
|
|
3
|
-
"version": "0.3.
|
|
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
|
|
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
|
-
**
|
|
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
|
|
package/bin/bin.bundle.mjs
CHANGED
|
@@ -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
|
-
|
|
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, "&").replace(/"/g, """).replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">");
|
|
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
|
|
2847
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
}
|