halfcycle 0.3.7
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 +16 -0
- package/LICENSE +21 -0
- package/README.md +187 -0
- package/bin/bin.bundle.mjs +16771 -0
- package/commands/agent-teams-orchestration.md +11 -0
- package/commands/consistency-check.md +11 -0
- package/commands/context-update.md +11 -0
- package/commands/halfcycle-design.md +11 -0
- package/commands/halfcycle-setup.md +11 -0
- package/commands/stack-assembly-worker.md +11 -0
- package/commands/task-execution.md +11 -0
- package/commands/task-graph.md +11 -0
- package/commands/write-spec.md +11 -0
- package/dist/account-credential.d.ts +94 -0
- package/dist/account-credential.d.ts.map +1 -0
- package/dist/bin.d.ts +34 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +3156 -0
- package/dist/bin.js.map +6 -0
- package/dist/build-record/assemble.d.ts +37 -0
- package/dist/build-record/assemble.d.ts.map +1 -0
- package/dist/build-record/index.d.ts +39 -0
- package/dist/build-record/index.d.ts.map +1 -0
- package/dist/build-record/sources.d.ts +80 -0
- package/dist/build-record/sources.d.ts.map +1 -0
- package/dist/build-record/template.d.ts +18 -0
- package/dist/build-record/template.d.ts.map +1 -0
- package/dist/build-record/types.d.ts +163 -0
- package/dist/build-record/types.d.ts.map +1 -0
- package/dist/build-record/write.d.ts +26 -0
- package/dist/build-record/write.d.ts.map +1 -0
- package/dist/cli-contract.d.ts +120 -0
- package/dist/cli-contract.d.ts.map +1 -0
- package/dist/close-phase.d.ts +114 -0
- package/dist/close-phase.d.ts.map +1 -0
- package/dist/create-engagement.d.ts +248 -0
- package/dist/create-engagement.d.ts.map +1 -0
- package/dist/device-signin.d.ts +255 -0
- package/dist/device-signin.d.ts.map +1 -0
- package/dist/engagement-credential.d.ts +267 -0
- package/dist/engagement-credential.d.ts.map +1 -0
- package/dist/identity.d.ts +76 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2083 -0
- package/dist/index.js.map +6 -0
- package/dist/install.d.ts +355 -0
- package/dist/install.d.ts.map +1 -0
- package/dist/loopback-signin.d.ts +173 -0
- package/dist/loopback-signin.d.ts.map +1 -0
- package/dist/mcp-endpoint.d.ts +35 -0
- package/dist/mcp-endpoint.d.ts.map +1 -0
- package/dist/merge-settings.d.ts +57 -0
- package/dist/merge-settings.d.ts.map +1 -0
- package/dist/mint-board-code.d.ts +41 -0
- package/dist/mint-board-code.d.ts.map +1 -0
- package/dist/open-phase.d.ts +107 -0
- package/dist/open-phase.d.ts.map +1 -0
- package/dist/probe-mcp.d.ts +47 -0
- package/dist/probe-mcp.d.ts.map +1 -0
- package/dist/resolve-credential.d.ts +126 -0
- package/dist/resolve-credential.d.ts.map +1 -0
- package/dist/scan.d.ts +131 -0
- package/dist/scan.d.ts.map +1 -0
- package/dist/setup/discovery.d.ts +73 -0
- package/dist/setup/discovery.d.ts.map +1 -0
- package/dist/setup/engagement.d.ts +99 -0
- package/dist/setup/engagement.d.ts.map +1 -0
- package/dist/setup/index.d.ts +17 -0
- package/dist/setup/index.d.ts.map +1 -0
- package/dist/setup/manifest.d.ts +155 -0
- package/dist/setup/manifest.d.ts.map +1 -0
- package/dist/setup/rows.d.ts +57 -0
- package/dist/setup/rows.d.ts.map +1 -0
- package/package.json +55 -0
- package/scaffolding/test/fixtures/captured/.gitkeep +0 -0
- package/scaffolding/test/fixtures/captured/manifest.json +4 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What each `halfcycle` verb REQUIRES — declared once, enforced from the
|
|
3
|
+
* declaration (walk 8 `F-5`, W8-T-02).
|
|
4
|
+
*
|
|
5
|
+
* ---
|
|
6
|
+
*
|
|
7
|
+
* **Why this file exists.** `orchestrator.ts` in the control plane printed
|
|
8
|
+
* `halfcycle close-phase <phase> --verdict <clean|defects>` as the remedy for a
|
|
9
|
+
* refusal. Wave 7 made `--actor` mandatory on that verb and did not update the
|
|
10
|
+
* sentence, so the product's own printed instruction exited 2 when a developer ran
|
|
11
|
+
* it. That is walk 6's `F-1` class — a remedy is a claim about a command, and a
|
|
12
|
+
* claim goes stale silently — recurring in the wave that had been warned about it.
|
|
13
|
+
*
|
|
14
|
+
* The fix for one string is one string. The fix for the CLASS is that the
|
|
15
|
+
* requirement stops living in `bin.ts`'s argument handling as a sequence of ad-hoc
|
|
16
|
+
* `if (!flag)` checks that nothing else can read. Here it is DATA: the verb, the
|
|
17
|
+
* flags an invocation must carry, the refusal each missing flag prints, and the
|
|
18
|
+
* conditional requirements (`--override` obliges `--reason`). `bin.ts` enforces
|
|
19
|
+
* from this table — it does not restate it — so the argument handling and the
|
|
20
|
+
* declaration cannot disagree. Adding a required flag is one edit here, and every
|
|
21
|
+
* reader of this table follows it.
|
|
22
|
+
*
|
|
23
|
+
* **The readers.** Two, and they are the two halves of a seam:
|
|
24
|
+
*
|
|
25
|
+
* 1. `bin.ts` — the CLI itself. It calls `missingRequirements` and prints the
|
|
26
|
+
* declared message. This is what makes the table the requirement rather than
|
|
27
|
+
* a description of one; a table nothing enforces is documentation, and
|
|
28
|
+
* documentation is exactly what went stale.
|
|
29
|
+
* 2. `test/remedy-string-seam.test.ts` — the regression check. It extracts every
|
|
30
|
+
* `halfcycle <verb> …` invocation the control service PRINTS and runs it
|
|
31
|
+
* through the same `missingRequirements`. A future flag change fails that
|
|
32
|
+
* test rather than an acceptance walk.
|
|
33
|
+
*
|
|
34
|
+
* **Why the seam, and which side owns the list (INV-001).** The control service
|
|
35
|
+
* writes the remedy strings; this CLI is what must accept them. `packages/bundle`
|
|
36
|
+
* is shippable and its import allowlist is exactly `{packages/core,
|
|
37
|
+
* packages/events}` — it cannot import the hosted control service (the moat check in
|
|
38
|
+
* `test/import-graph.test.ts` refuses even the NAME of a hosted package appearing in
|
|
39
|
+
* this package's source, which is why one is not written here), and control importing a
|
|
40
|
+
* published client installer to read its argument parser would invert the
|
|
41
|
+
* dependency and drag the installer into a hosted service. So the CLI owns the
|
|
42
|
+
* list, because the CLI is the thing that REJECTS: a requirement that lived
|
|
43
|
+
* anywhere else would be a copy of the enforcement rather than the enforcement.
|
|
44
|
+
* The test crosses the gap by READING control's source text off disk (an fs read,
|
|
45
|
+
* not an import — no build edge, no runtime edge, nothing shipped), the same way
|
|
46
|
+
* `test/command-set-parity.test.ts` reads a spec from the repo root.
|
|
47
|
+
*
|
|
48
|
+
* **What is deliberately NOT here.** No verb's full grammar, no optional flags, no
|
|
49
|
+
* usage text. This table answers exactly one question — *which flags must an
|
|
50
|
+
* invocation of this verb carry to be runnable* — because that is the question a
|
|
51
|
+
* remedy string makes a claim about. Widening it into a second argument-parsing
|
|
52
|
+
* home is the failure mode it exists to prevent.
|
|
53
|
+
*/
|
|
54
|
+
/** The verbs the CLI dispatches. Anything else after `halfcycle ` is unknown. */
|
|
55
|
+
export declare const CLI_VERBS: readonly ["install", "check-drift", "build-record", "open-phase", "close-phase"];
|
|
56
|
+
export type CliVerb = (typeof CLI_VERBS)[number];
|
|
57
|
+
/** A flag every invocation of the verb must carry. */
|
|
58
|
+
export interface RequiredFlag {
|
|
59
|
+
readonly flag: string;
|
|
60
|
+
/**
|
|
61
|
+
* Flags that satisfy the SAME requirement in the caller's place — the
|
|
62
|
+
* `--evidence` / `--override` choice on `open-phase`, where one basis or the
|
|
63
|
+
* other is required and neither is optional.
|
|
64
|
+
*/
|
|
65
|
+
readonly orFlags?: readonly string[];
|
|
66
|
+
/** True when the flag needs a non-blank value after it. */
|
|
67
|
+
readonly takesValue: boolean;
|
|
68
|
+
/** The refusal the CLI prints when it is missing. One home for the wording. */
|
|
69
|
+
readonly missingMessage: string;
|
|
70
|
+
}
|
|
71
|
+
/** A requirement one flag's presence creates. */
|
|
72
|
+
export interface ConditionalFlag {
|
|
73
|
+
readonly whenPresent: string;
|
|
74
|
+
readonly thenRequires: string;
|
|
75
|
+
readonly takesValue: boolean;
|
|
76
|
+
readonly missingMessage: string;
|
|
77
|
+
}
|
|
78
|
+
export interface VerbContract {
|
|
79
|
+
readonly verb: CliVerb;
|
|
80
|
+
readonly required: readonly RequiredFlag[];
|
|
81
|
+
readonly conditional: readonly ConditionalFlag[];
|
|
82
|
+
}
|
|
83
|
+
/** The contract for a verb, or `undefined` when the CLI has no such verb. */
|
|
84
|
+
export declare function contractFor(verb: string): VerbContract | undefined;
|
|
85
|
+
/** A requirement an invocation does not meet. */
|
|
86
|
+
export interface Shortfall {
|
|
87
|
+
/** The flag that is missing (or present with no usable value). */
|
|
88
|
+
readonly flag: string;
|
|
89
|
+
/** The CLI's own refusal for it. */
|
|
90
|
+
readonly message: string;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The requirements `tokens` does not meet for `verb`, in declaration order.
|
|
94
|
+
*
|
|
95
|
+
* `tokens` is an argv slice from the CLI, or a printed invocation split on
|
|
96
|
+
* whitespace — the same function serves both, which is the point: the sentence a
|
|
97
|
+
* remedy prints is checked against the rule the binary enforces, not against a
|
|
98
|
+
* second copy of it.
|
|
99
|
+
*
|
|
100
|
+
* An unknown verb yields a single shortfall naming it. A remedy that names a verb
|
|
101
|
+
* this CLI does not have fails exactly like one that omits a flag, because to the
|
|
102
|
+
* developer who runs it the two are the same event.
|
|
103
|
+
*/
|
|
104
|
+
export declare function missingRequirements(verb: string, tokens: readonly string[]): Shortfall[];
|
|
105
|
+
/**
|
|
106
|
+
* The declared refusal for one requirement — so a caller that checks a flag's
|
|
107
|
+
* VALUE (`--verdict` must be `clean` or `defects`) prints the same sentence the
|
|
108
|
+
* presence check prints, from the same place.
|
|
109
|
+
*/
|
|
110
|
+
export declare function requirementMessage(verb: CliVerb, flag: string): string;
|
|
111
|
+
/**
|
|
112
|
+
* The shortest invocation of `verb` that this CLI accepts, with `phase` filled in
|
|
113
|
+
* and every required flag named — what a refusal tells a developer to run.
|
|
114
|
+
*
|
|
115
|
+
* RENDERED, not typed out. A remedy string built by hand is the defect this file
|
|
116
|
+
* was opened for; one built here cannot omit a flag, because the flags come from
|
|
117
|
+
* the same table the binary refuses on.
|
|
118
|
+
*/
|
|
119
|
+
export declare function runnableInvocation(verb: CliVerb, phase: string): string;
|
|
120
|
+
//# sourceMappingURL=cli-contract.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli-contract.d.ts","sourceRoot":"","sources":["../src/cli-contract.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AAEH,iFAAiF;AACjF,eAAO,MAAM,SAAS,kFAAmF,CAAC;AAE1G,MAAM,MAAM,OAAO,GAAG,CAAC,OAAO,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC;AAEjD,sDAAsD;AACtD,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,2DAA2D;IAC3D,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,+EAA+E;IAC/E,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;CACjC;AAED,iDAAiD;AACjD,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;CACjC;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,SAAS,YAAY,EAAE,CAAC;IAC3C,QAAQ,CAAC,WAAW,EAAE,SAAS,eAAe,EAAE,CAAC;CAClD;AA0ED,6EAA6E;AAC7E,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAElE;AAED,iDAAiD;AACjD,MAAM,WAAW,SAAS;IACxB,kEAAkE;IAClE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,oCAAoC;IACpC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAmBD;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,EAAE,CAyBxF;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAMtE;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAMvE"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Close-phase wiring — the CLI's call to `POST {baseUrl}/engagements/:id/accept`
|
|
3
|
+
* (walk 6 F-1, INV-022's sixth occurrence).
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS EXISTS AT ALL, WHICH IS THE WHOLE POINT OF THE FINDING. Closing a phase
|
|
6
|
+
* has had a reachable route since PM-E6-T-18 and a working transition from `active`
|
|
7
|
+
* since W4-1 — but the only CLI that drove it is `phase-close-admin`, which ships
|
|
8
|
+
* inside the control plane and is therefore hosted-only (INV-001). A Connect client runs
|
|
9
|
+
* `npx halfcycle` in their own repo and can never run it. So the clean close was
|
|
10
|
+
* reachable by curl and by nothing the product ships, and every acceptance walk was
|
|
11
|
+
* pushed onto `open-phase --override` instead.
|
|
12
|
+
*
|
|
13
|
+
* `open-phase` and `close-phase` are the two halves of one cycle and now sit beside
|
|
14
|
+
* each other in one package, with one credential and one shape. That is the fix:
|
|
15
|
+
* not a new precondition, not a way to make a Studio gate satisfiable — the gate was
|
|
16
|
+
* never the problem — but the missing client half of a path that already worked.
|
|
17
|
+
*
|
|
18
|
+
* **This verb names no capability only Studio has.** No GitHub installation, no
|
|
19
|
+
* portal session, no operator picking anything up, and it never touches
|
|
20
|
+
* `delivering`. It is the same call in both topologies (`acceptPhase` is legal from
|
|
21
|
+
* `active` AND from `delivering`), which is what makes it safe to print in a refusal
|
|
22
|
+
* that both topologies can read — see `PhaseEntryConditionError`.
|
|
23
|
+
*
|
|
24
|
+
* WIRING, NOT MACHINERY — the same posture as `open-phase.ts`. The verdict is the
|
|
25
|
+
* caller's (INV-009: SOURCED from the acceptance walk, never inferred from "no guard
|
|
26
|
+
* fired"), the findings/detail consistency rule is the control plane's, and the
|
|
27
|
+
* derive-then-transition ordering is the orchestrator's. This module builds the
|
|
28
|
+
* request, sends it, and reports what came back.
|
|
29
|
+
*
|
|
30
|
+
* INV-001: this file imports the global `fetch` (Node 20), Node builtins, and
|
|
31
|
+
* `@halfcycle/events` — one of the exactly two workspace packages a shippable may
|
|
32
|
+
* import. `acceptanceOutcomeSchema` is the ONE home for the outcome's shape
|
|
33
|
+
* (INV-003), so the CLI validates what it builds against the same schema the route
|
|
34
|
+
* parses with. It never imports the control service.
|
|
35
|
+
*/
|
|
36
|
+
import { type AcceptanceOutcome, type PhaseCloseDecision } from '@halfcycle/events';
|
|
37
|
+
import type { RepoCredential } from './open-phase.js';
|
|
38
|
+
/** What `POST /engagements/:id/accept` returns on 200 (the fields the CLI reports). */
|
|
39
|
+
export interface ClosedPhase {
|
|
40
|
+
engagementId: string;
|
|
41
|
+
/** The lifecycle status after the close — `closed`. */
|
|
42
|
+
status: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* A refusal from the control plane, carried as an error the CLI can print whole.
|
|
46
|
+
*
|
|
47
|
+
* The two that matter and what they mean, because a status code alone sends people
|
|
48
|
+
* to the wrong fix:
|
|
49
|
+
* - `409` — a refusal. THREE different ones now share this code (already closed;
|
|
50
|
+
* outstanding work; the phase is not the one open), which is why `code` below
|
|
51
|
+
* exists: the status alone stopped being enough to say what happened.
|
|
52
|
+
* - `422` — the outcome is not a valid INV-009 outcome (most often the findings
|
|
53
|
+
* count disagreeing with the number of `--finding` summaries). Nothing was
|
|
54
|
+
* written: the derivation validates before any write.
|
|
55
|
+
*/
|
|
56
|
+
export declare class ClosePhaseRefused extends Error {
|
|
57
|
+
readonly status: number;
|
|
58
|
+
/**
|
|
59
|
+
* What the control plane's own store found outstanding in this phase, when the
|
|
60
|
+
* refusal was a `PhaseCloseConditionUnmet` (walk 7 `F-4`). Absent on every other
|
|
61
|
+
* refusal. Carried separately from `message` so the CLI can print the finding on
|
|
62
|
+
* its own line, the way `open-phase` prints a crossed entry finding.
|
|
63
|
+
*/
|
|
64
|
+
readonly finding?: string | undefined;
|
|
65
|
+
/**
|
|
66
|
+
* The plane's own name for the refusal (`error` on the body) — e.g.
|
|
67
|
+
* `PhaseAlreadyClosed`, `PhaseCloseConditionUnmet`, `PhaseNotOpen`.
|
|
68
|
+
*
|
|
69
|
+
* WHY IT IS CARRIED (walk 8 `F-1`). The CLI used to read a bare `409` as "this
|
|
70
|
+
* phase is already closed" and print that as advice. Once a wrong phase NAME
|
|
71
|
+
* became its own 409, that line printed underneath a refusal saying the opposite
|
|
72
|
+
* — the plane said *phase 1 is open, close that one* and the CLI said *it is
|
|
73
|
+
* already closed*. Guessing a cause from a status code is how a remedy string
|
|
74
|
+
* comes to contradict the refusal above it; the code is read, not inferred.
|
|
75
|
+
*/
|
|
76
|
+
readonly code?: string | undefined;
|
|
77
|
+
constructor(status: number, message: string,
|
|
78
|
+
/**
|
|
79
|
+
* What the control plane's own store found outstanding in this phase, when the
|
|
80
|
+
* refusal was a `PhaseCloseConditionUnmet` (walk 7 `F-4`). Absent on every other
|
|
81
|
+
* refusal. Carried separately from `message` so the CLI can print the finding on
|
|
82
|
+
* its own line, the way `open-phase` prints a crossed entry finding.
|
|
83
|
+
*/
|
|
84
|
+
finding?: string | undefined,
|
|
85
|
+
/**
|
|
86
|
+
* The plane's own name for the refusal (`error` on the body) — e.g.
|
|
87
|
+
* `PhaseAlreadyClosed`, `PhaseCloseConditionUnmet`, `PhaseNotOpen`.
|
|
88
|
+
*
|
|
89
|
+
* WHY IT IS CARRIED (walk 8 `F-1`). The CLI used to read a bare `409` as "this
|
|
90
|
+
* phase is already closed" and print that as advice. Once a wrong phase NAME
|
|
91
|
+
* became its own 409, that line printed underneath a refusal saying the opposite
|
|
92
|
+
* — the plane said *phase 1 is open, close that one* and the CLI said *it is
|
|
93
|
+
* already closed*. Guessing a cause from a status code is how a remedy string
|
|
94
|
+
* comes to contradict the refusal above it; the code is read, not inferred.
|
|
95
|
+
*/
|
|
96
|
+
code?: string | undefined);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Record a phase's acceptance and close it.
|
|
100
|
+
*
|
|
101
|
+
* @param phase the phase being closed, named explicitly — the lifecycle row has no
|
|
102
|
+
* phase column (PM-E4-DEC-7), so the caller names what closed.
|
|
103
|
+
* @param outcome the SOURCED INV-009 verdict + findings.
|
|
104
|
+
* @param close WHO is closing it and on what basis (walk 7 `F-4`). Required here
|
|
105
|
+
* for the same reason `openPhase` requires its entry decision: there
|
|
106
|
+
* is no argument-less way to move the phase axis, so an unattributed
|
|
107
|
+
* close is unreachable from any caller that compiles.
|
|
108
|
+
*
|
|
109
|
+
* @throws ClosePhaseRefused on a 4xx the plane explained, so the CLI prints the
|
|
110
|
+
* plane's own sentence and exits non-zero.
|
|
111
|
+
* @throws Error on transport failure or a body that is not the expected shape.
|
|
112
|
+
*/
|
|
113
|
+
export declare function closePhase(credential: RepoCredential, phase: string, outcome: AcceptanceOutcome, close: PhaseCloseDecision): Promise<ClosedPhase>;
|
|
114
|
+
//# sourceMappingURL=close-phase.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"close-phase.d.ts","sourceRoot":"","sources":["../src/close-phase.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,OAAO,EAGL,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EACxB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEtD,uFAAuF;AACvF,MAAM,WAAW,WAAW;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,uDAAuD;IACvD,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;GAWG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;aAExB,MAAM,EAAE,MAAM;IAE9B;;;;;OAKG;aACa,OAAO,CAAC,EAAE,MAAM;IAChC;;;;;;;;;;OAUG;aACa,IAAI,CAAC,EAAE,MAAM;gBApBb,MAAM,EAAE,MAAM,EAC9B,OAAO,EAAE,MAAM;IACf;;;;;OAKG;IACa,OAAO,CAAC,EAAE,MAAM,YAAA;IAChC;;;;;;;;;;OAUG;IACa,IAAI,CAAC,EAAE,MAAM,YAAA;CAKhC;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,UAAU,CAC9B,UAAU,EAAE,cAAc,EAC1B,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,iBAAiB,EAC1B,KAAK,EAAE,kBAAkB,GACxB,OAAO,CAAC,WAAW,CAAC,CAwDtB"}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create-engagement wiring — the installer's one HTTP call (#374).
|
|
3
|
+
*
|
|
4
|
+
* **IT IS TWO CALLS SINCE T-24 (#610), AND THEY LIVE HERE TOGETHER ON PURPOSE.**
|
|
5
|
+
* `createEngagement` starts a new engagement; `joinEngagement` obtains the caller
|
|
6
|
+
* their OWN credential for one that already exists, which is what a second developer
|
|
7
|
+
* cloning a repository needs and what nothing in this package asked for until now.
|
|
8
|
+
* The two answer the SAME question — *give me the four values this install needs* —
|
|
9
|
+
* over two routes, and the 200/201 bodies are field-for-field identical.
|
|
10
|
+
*
|
|
11
|
+
* A sibling module would therefore have had to re-decide four things that already
|
|
12
|
+
* have a home in this file: which non-2xx bodies are a plane refusal rather than a
|
|
13
|
+
* wrong address (`planeRefusal`), when the origin hint applies, that a blank-but-
|
|
14
|
+
* present identifier is refused, and what a refusal is thrown AS. Four copies is
|
|
15
|
+
* how two callers come to disagree about one contract (INV-003), and the disagreement
|
|
16
|
+
* would surface as the join path quietly accepting a body the create path refuses.
|
|
17
|
+
* So both entry points are thin, and `requestEngagementValues` below is the one home.
|
|
18
|
+
*
|
|
19
|
+
* `POST {baseUrl}/engagements` returns `{ engagementId, sessionToken, mcpUrl,
|
|
20
|
+
* guardUrl }`. This module is WIRING, not machinery: it makes that HTTP call and
|
|
21
|
+
* hands the values back. The installer then commits the id into
|
|
22
|
+
* `.halfcycle/bundle.json` (`writeBundlePin`, `install.ts`) and writes the token,
|
|
23
|
+
* the MCP origin and the id to the machine-level credential store OUTSIDE the
|
|
24
|
+
* repository (T-09, `engagement-credential.ts`).
|
|
25
|
+
*
|
|
26
|
+
* **THAT ROUTE IS NO LONGER UNAUTHENTICATED (T-21, `connect-signup-and-credential`).**
|
|
27
|
+
* This paragraph said it "ships PUBLIC and UNAUTHENTICATED"; it is now gated on a
|
|
28
|
+
* signed-in account and stamps that account as the engagement's owner, so an
|
|
29
|
+
* unauthenticated create is a 401 that creates nothing. `createEngagement` below
|
|
30
|
+
* takes an optional `credential` and sends it. **Obtaining one is `resolve-credential.ts`'s
|
|
31
|
+
* job since T-08** — `HALFCYCLE_TOKEN` in the environment, else the per-user store,
|
|
32
|
+
* else the browser handoff — and this module stays wiring: it neither resolves a
|
|
33
|
+
* credential nor decides what to do when one is refused. What it DOES own is telling
|
|
34
|
+
* a refusal apart from a wrong address (`CreateEngagementRefused` below), because
|
|
35
|
+
* that distinction is a property of the response and nothing further up the call
|
|
36
|
+
* stack can recover it from an `Error` message.
|
|
37
|
+
*
|
|
38
|
+
* **This paragraph said `.halfcycle/project.json` and that was never true**
|
|
39
|
+
* (corrected 2026-08-09, T-23 / issue #447). `writeProjectIdentity` calls
|
|
40
|
+
* `mintOrReadIdentity`, which writes `{projectId, rootCommit, remote, note}` and
|
|
41
|
+
* no engagement id — deliberately, under operator decision (au): no secrets in
|
|
42
|
+
* the committed identity. The claim was load-bearing: `services/delivery`'s MCP
|
|
43
|
+
* tool description was written against it and sent every client session to read a
|
|
44
|
+
* field that is not there, which walk 1 of the widening had to work around by
|
|
45
|
+
* hand. `note` (added W8-T-04, F-4) does not reopen (au) — it names no
|
|
46
|
+
* engagement, only that `projectId` is not one; see `identity.ts`.
|
|
47
|
+
*
|
|
48
|
+
* **THE TWO ORIGINS (T-22, #438).** `baseUrl` here is the CONTROL origin —
|
|
49
|
+
* `HALFCYCLE_SERVICE_URL`, which serves `POST /engagements` and
|
|
50
|
+
* `POST /board/enter-codes`. The MCP endpoint lives on a DIFFERENT deployed
|
|
51
|
+
* service, and control does not serve `/mcp` at all. Before T-22 one variable was
|
|
52
|
+
* read as the origin for all three routes, so no value satisfied it: pointed at
|
|
53
|
+
* control the installed `.mcp.json` named a 404 and the developer got no method
|
|
54
|
+
* context; pointed at delivery this call 404'd and no credential was ever minted.
|
|
55
|
+
* `npx halfcycle` could not produce a working install either way. The fix is that
|
|
56
|
+
* **the server declares the MCP origin** — the 201 carries `mcpUrl` — so the
|
|
57
|
+
* developer still configures exactly one address and the platform answers the
|
|
58
|
+
* question it already knows the answer to.
|
|
59
|
+
*
|
|
60
|
+
* The errors here NAME WHICH ORIGIN is wrong, in plain words. A developer whose
|
|
61
|
+
* variable points at the wrong half used to get a bare 404 and no way to tell
|
|
62
|
+
* which half of a two-service platform they had missed.
|
|
63
|
+
*
|
|
64
|
+
* INV-001: this file imports only Node builtins and the global `fetch` (Node 20).
|
|
65
|
+
* It never imports the control service — the create endpoint is an external HTTP
|
|
66
|
+
* contract, exactly like the runner is an external binary contract.
|
|
67
|
+
*/
|
|
68
|
+
/** The four values `POST /engagements` returns (Option A — the session credential
|
|
69
|
+
* crosses once on create; `mcpUrl` and `guardUrl` are the server declaring where
|
|
70
|
+
* its other two services live). */
|
|
71
|
+
export interface CreatedEngagement {
|
|
72
|
+
engagementId: string;
|
|
73
|
+
sessionToken: string;
|
|
74
|
+
/**
|
|
75
|
+
* The origin serving the method-delivery MCP endpoint, as the platform reports
|
|
76
|
+
* it. Persisted as `HALFCYCLE_MCP_URL`; `.mcp.json` appends `/mcp` to it.
|
|
77
|
+
*
|
|
78
|
+
* Trailing slashes are stripped here so the one place that composes the MCP URL
|
|
79
|
+
* cannot produce `https://host//mcp`. Nothing else about the value is
|
|
80
|
+
* re-derived client-side: whether it is a usable origin is checked ONCE, on the
|
|
81
|
+
* control plane at boot (its `originProblem`), and a second copy of that grammar
|
|
82
|
+
* in a shippable package would be a second home for one rule — and, per INV-001,
|
|
83
|
+
* this file names no hosted path, not even in prose. What the client does
|
|
84
|
+
* instead is say plainly when the address does not answer — see `bin.ts`.
|
|
85
|
+
*/
|
|
86
|
+
mcpUrl: string;
|
|
87
|
+
/**
|
|
88
|
+
* The origin serving the guard service's `/evaluate` endpoint, as the platform
|
|
89
|
+
* reports it — persisted as `GUARD_SERVICE_URL` (FW-4, F-29).
|
|
90
|
+
*
|
|
91
|
+
* A THIRD deployed service, and the installer cannot derive it any more than it
|
|
92
|
+
* could derive `mcpUrl`: control serves `/engagements`, delivery serves `/mcp`,
|
|
93
|
+
* guard serves `/evaluate`. Until the platform declared it, the installer wired
|
|
94
|
+
* a `PostToolUse` hook that runs the guard runner and wrote none of the runner's
|
|
95
|
+
* three variables — so every edit in a fresh Connect install produced a
|
|
96
|
+
* fail-open warning and no guard ever ran, while the install reported success.
|
|
97
|
+
*
|
|
98
|
+
* Trailing slashes are stripped here for the same reason they are on `mcpUrl`:
|
|
99
|
+
* the runner appends `/evaluate`, and one place composing the address cannot
|
|
100
|
+
* produce `https://host//evaluate`. Nothing else about the value is re-derived
|
|
101
|
+
* client-side — whether it is a usable origin is checked ONCE, on the control
|
|
102
|
+
* plane at boot, and a second copy of that grammar in a shippable package would
|
|
103
|
+
* be a second home for one rule.
|
|
104
|
+
*/
|
|
105
|
+
guardUrl: string;
|
|
106
|
+
/**
|
|
107
|
+
* The origin the runner's telemetry POST goes to, as the platform reports it —
|
|
108
|
+
* `CONTROL_TELEMETRY_URL` on the wire (T-10, phase decision 13). Persisted to the
|
|
109
|
+
* machine-level store; `guard-runner.sh` sources the whole file, so no generated
|
|
110
|
+
* script names this variable directly.
|
|
111
|
+
*
|
|
112
|
+
* **OPTIONAL, unlike `mcpUrl`/`guardUrl` above — deliberately.** Those two are
|
|
113
|
+
* refused when absent because the install would otherwise ship a product that
|
|
114
|
+
* does nothing or a guard hook wired to no address. Telemetry is best-effort by
|
|
115
|
+
* design (`packages/runner/src/telemetry/emit.ts`: absent → emission silently
|
|
116
|
+
* disabled, guard loop unaffected), so a plane that has not configured
|
|
117
|
+
* `CONTROL_TELEMETRY_URL` yet must not fail every install over it — that would be
|
|
118
|
+
* a STRICTER posture than the thing being configured. **The key is ABSENT, not
|
|
119
|
+
* merely `undefined`-valued, when the plane sent none** — an optional TS property
|
|
120
|
+
* rather than `string | undefined`, so the two existing "full key set" tests in
|
|
121
|
+
* `create-engagement.test.ts` (whose fixture server does not send this field) see
|
|
122
|
+
* the same four keys they always have, and only a fixture that adds the field
|
|
123
|
+
* gains the fifth.
|
|
124
|
+
*/
|
|
125
|
+
controlTelemetryUrl?: string;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* A refusal the CONTROL PLANE wrote — the origin is right, and it declined (T-08).
|
|
129
|
+
*
|
|
130
|
+
* Carried as its own error class so a caller can tell "your credential was refused"
|
|
131
|
+
* apart from "that address is not a control plane", which are the same
|
|
132
|
+
* `!res.ok` today and want completely different next actions. `bin.ts` branches on
|
|
133
|
+
* `authRefused` to decide whether signing in again can possibly help.
|
|
134
|
+
*
|
|
135
|
+
* **THE JOIN CALL THROWS THIS TOO, AND THE NAME WAS DELIBERATELY NOT CHANGED (T-24).**
|
|
136
|
+
* The class carries FACTS — a status, the plane's own sentence, the discriminant —
|
|
137
|
+
* and those are the same facts on both routes, so a second class would have been a
|
|
138
|
+
* second home for the same three fields and for `authRefused`'s derivation. Renaming
|
|
139
|
+
* it to something route-neutral was the other option and was rejected: it is an
|
|
140
|
+
* exported symbol of this package's public entry, and churning a published name to
|
|
141
|
+
* improve how one new call site reads is a poor trade. The name is create-shaped for
|
|
142
|
+
* historical reasons; what it means is *the plane refused*.
|
|
143
|
+
*/
|
|
144
|
+
export declare class CreateEngagementRefused extends Error {
|
|
145
|
+
/** The HTTP status control answered with. */
|
|
146
|
+
readonly status: number;
|
|
147
|
+
/** Control's own client-safe sentence, forwarded verbatim. */
|
|
148
|
+
readonly serverMessage: string;
|
|
149
|
+
/** The `error` discriminant, when the envelope carried one. */
|
|
150
|
+
readonly errorCode: string | undefined;
|
|
151
|
+
constructor(
|
|
152
|
+
/** The HTTP status control answered with. */
|
|
153
|
+
status: number,
|
|
154
|
+
/** Control's own client-safe sentence, forwarded verbatim. */
|
|
155
|
+
serverMessage: string,
|
|
156
|
+
/** The `error` discriminant, when the envelope carried one. */
|
|
157
|
+
errorCode: string | undefined, message: string);
|
|
158
|
+
/**
|
|
159
|
+
* Is this a refusal of the CALLER's credential — the arm signing in again can fix?
|
|
160
|
+
*
|
|
161
|
+
* `PublicStartDisabled` is excluded even though it is a 403, and that exclusion is
|
|
162
|
+
* the whole reason this is a method rather than a status comparison at the call
|
|
163
|
+
* site: the flag gates the DEPLOYMENT, identically for every caller, so no
|
|
164
|
+
* credential and no sign-in changes the answer. Treating it as an auth refusal
|
|
165
|
+
* would send a developer through a browser handoff to arrive at the same 403.
|
|
166
|
+
*
|
|
167
|
+
* **THIS IS THE CREATE ROUTE'S DERIVATION AND THE JOIN CALLER MUST NOT USE IT
|
|
168
|
+
* (T-24).** On the join route a 403 has two causes that share a status AND a
|
|
169
|
+
* discriminant (`error: 'Forbidden'`), so nothing on this object can tell them
|
|
170
|
+
* apart: the credential may be a live engagement token carrying no account, or the
|
|
171
|
+
* engagement id may not resolve. Signing in again cannot fix the second, and
|
|
172
|
+
* treating it as though it could would discard a good credential, walk the
|
|
173
|
+
* developer through a browser, and arrive at the identical 403 — a remedy that
|
|
174
|
+
* loops. `bin.ts`'s join path therefore retries on `401` ALONE, at the call site,
|
|
175
|
+
* where the reason can be written down. See `joinPinnedEngagement` there.
|
|
176
|
+
*/
|
|
177
|
+
get authRefused(): boolean;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Create a fresh engagement against the platform's public create endpoint.
|
|
181
|
+
*
|
|
182
|
+
* **THE ROUTE REQUIRES A SIGNED-IN CALLER SINCE T-21** — an unauthenticated create
|
|
183
|
+
* is a 401 and creates nothing, which is the feature's first acceptance criterion.
|
|
184
|
+
* This function therefore SENDS a credential when it has one.
|
|
185
|
+
*
|
|
186
|
+
* **OBTAINING one is `resolve-credential.ts`'s, and it EXISTS (T-08, landed).** This
|
|
187
|
+
* paragraph said it "is T-08's half", written while it was still owed — and the file
|
|
188
|
+
* header 165 lines above was corrected when T-08 landed while this one was not, so a
|
|
189
|
+
* reader who opened the function rather than the file top was told the browser
|
|
190
|
+
* handoff did not exist yet. Same file, two claims, one of them false: the same
|
|
191
|
+
* stale-claim shape this task corrected twice on the plane's own refusal messages.
|
|
192
|
+
* (INV-001 keeps this file naming no hosted path, not even in prose — the guard in
|
|
193
|
+
* `import-graph.test.ts` caught the first draft of this very sentence doing it.)
|
|
194
|
+
*
|
|
195
|
+
* @param baseUrl The CONTROL origin (e.g. `https://control.halfcycle.ai`).
|
|
196
|
+
* The request is `POST {baseUrl}/engagements`.
|
|
197
|
+
* @param name Optional project name; the server defaults a blank/absent name.
|
|
198
|
+
* @param credential Optional bearer credential, from `obtainCredential`. **EITHER
|
|
199
|
+
* credential class works**, and that is a property of control's
|
|
200
|
+
* `resolveAccount` rather than an accident here: an ACCOUNT
|
|
201
|
+
* CREDENTIAL from the browser handoff, or an ENGAGEMENT TOKEN from
|
|
202
|
+
* an install this person already has. Blank/absent sends no header
|
|
203
|
+
* at all — a missing credential must look identical to the server as
|
|
204
|
+
* no header, never `Bearer `.
|
|
205
|
+
* @returns `{ engagementId, sessionToken, mcpUrl, guardUrl }` on 201.
|
|
206
|
+
* @throws `CreateEngagementRefused` when the PLANE refused (its own envelope,
|
|
207
|
+
* no origin hint — the origin demonstrably answered); a plain `Error`
|
|
208
|
+
* when the address is in question (a 404, an unreachable host, an
|
|
209
|
+
* unreadable body), which is the only case that carries
|
|
210
|
+
* `CONTROL_ORIGIN_HINT`. Either way the installer must NOT proceed to
|
|
211
|
+
* write a half-formed credential or a `.mcp.json` pointing nowhere.
|
|
212
|
+
*/
|
|
213
|
+
export declare function createEngagement(baseUrl: string, name?: string, credential?: string): Promise<CreatedEngagement>;
|
|
214
|
+
/**
|
|
215
|
+
* Obtain a credential of the CALLER'S OWN for an engagement that already exists
|
|
216
|
+
* (T-24, #610) — `POST {baseUrl}/engagements/{engagementId}/join`.
|
|
217
|
+
*
|
|
218
|
+
* **THE SECOND DEVELOPER'S PATH, AND UNTIL NOW NOTHING IN THIS PACKAGE WALKED IT.**
|
|
219
|
+
* The route shipped with T-06 and was tested at the store and at the route; no client
|
|
220
|
+
* called it, so *"a second developer clones and just works"* was reachable by HTTP and
|
|
221
|
+
* unreachable by the product. `bin.ts` calls this when the committed pin names an
|
|
222
|
+
* engagement and this checkout holds no credential for it.
|
|
223
|
+
*
|
|
224
|
+
* WHAT COMES BACK IS THE CALLER'S, NOT A COPY OF ANYBODY'S. The plane mints a fresh
|
|
225
|
+
* secret bound to `(engagement, this account)`; the first developer's credential is a
|
|
226
|
+
* different subject and is untouched. That is a property of the plane, asserted where
|
|
227
|
+
* it can be seen — in the store, after the window that would have expired a rotated
|
|
228
|
+
* predecessor — and not something this wiring can claim for itself.
|
|
229
|
+
*
|
|
230
|
+
* **A REFUSAL HERE IS NEVER AN EXISTENCE ANSWER.** The plane answers 403 with a
|
|
231
|
+
* constant sentence both when the caller's credential carries no account and when the
|
|
232
|
+
* id resolves to nothing, so this client cannot know which — and must not compose a
|
|
233
|
+
* message that implies it found out. It forwards the plane's own sentence and adds
|
|
234
|
+
* only facts local to this machine. See `bin.ts`'s join path for the one sentence it
|
|
235
|
+
* adds and why that sentence is a constant.
|
|
236
|
+
*
|
|
237
|
+
* @param baseUrl The CONTROL origin, exactly as `createEngagement` takes it.
|
|
238
|
+
* @param engagementId The id from the committed pin. Percent-encoded into the path:
|
|
239
|
+
* it is read from a file on disk, so it is not this function's
|
|
240
|
+
* place to assume it is a well-formed uuid — a value with a `/`
|
|
241
|
+
* in it must address one route badly, never a different route.
|
|
242
|
+
* @param credential Optional bearer credential, from `obtainCredential`.
|
|
243
|
+
* @returns `{ engagementId, sessionToken, mcpUrl, guardUrl }` on 200 —
|
|
244
|
+
* the same four fields create returns, which is why the installer
|
|
245
|
+
* needs no second branch downstream of this call.
|
|
246
|
+
*/
|
|
247
|
+
export declare function joinEngagement(baseUrl: string, engagementId: string, credential?: string): Promise<CreatedEngagement>;
|
|
248
|
+
//# sourceMappingURL=create-engagement.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-engagement.d.ts","sourceRoot":"","sources":["../src/create-engagement.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkEG;AAEH;;oCAEoC;AACpC,MAAM,WAAW,iBAAiB;IAChC,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB;;;;;;;;;;;OAWG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;;;;;;;;;;;;OAiBG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;;;;;;;;;;;;;;;OAkBG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,uBAAwB,SAAQ,KAAK;IAE9C,6CAA6C;aAC7B,MAAM,EAAE,MAAM;IAC9B,8DAA8D;aAC9C,aAAa,EAAE,MAAM;IACrC,+DAA+D;aAC/C,SAAS,EAAE,MAAM,GAAG,SAAS;;IAL7C,6CAA6C;IAC7B,MAAM,EAAE,MAAM;IAC9B,8DAA8D;IAC9C,aAAa,EAAE,MAAM;IACrC,+DAA+D;IAC/C,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7C,OAAO,EAAE,MAAM;IAMjB;;;;;;;;;;;;;;;;;;OAkBG;IACH,IAAI,WAAW,IAAI,OAAO,CAGzB;CACF;AA2CD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,MAAM,EACf,IAAI,CAAC,EAAE,MAAM,EACb,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,iBAAiB,CAAC,CAO5B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAsB,cAAc,CAClC,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,EACpB,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,iBAAiB,CAAC,CAW5B"}
|