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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +4 -2
- package/bin/bin.bundle.mjs +17 -3
- package/dist/bin.js +69 -14
- package/dist/bin.js.map +3 -3
- package/dist/device-signin.d.ts +17 -10
- package/dist/device-signin.d.ts.map +1 -1
- package/dist/index.js +131 -14
- package/dist/index.js.map +3 -3
- package/dist/install.d.ts +19 -0
- package/dist/install.d.ts.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/dist/project-page.d.ts +24 -0
- package/dist/project-page.d.ts.map +1 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "halfcycle",
|
|
3
|
-
"version": "0.3.
|
|
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
|
|
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
|
@@ -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
|
-
|
|
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, "&").replace(/"/g, """).replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">");
|
|
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
|
|
2847
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
}
|