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.
Files changed (78) hide show
  1. package/.claude-plugin/plugin.json +16 -0
  2. package/LICENSE +21 -0
  3. package/README.md +187 -0
  4. package/bin/bin.bundle.mjs +16771 -0
  5. package/commands/agent-teams-orchestration.md +11 -0
  6. package/commands/consistency-check.md +11 -0
  7. package/commands/context-update.md +11 -0
  8. package/commands/halfcycle-design.md +11 -0
  9. package/commands/halfcycle-setup.md +11 -0
  10. package/commands/stack-assembly-worker.md +11 -0
  11. package/commands/task-execution.md +11 -0
  12. package/commands/task-graph.md +11 -0
  13. package/commands/write-spec.md +11 -0
  14. package/dist/account-credential.d.ts +94 -0
  15. package/dist/account-credential.d.ts.map +1 -0
  16. package/dist/bin.d.ts +34 -0
  17. package/dist/bin.d.ts.map +1 -0
  18. package/dist/bin.js +3156 -0
  19. package/dist/bin.js.map +6 -0
  20. package/dist/build-record/assemble.d.ts +37 -0
  21. package/dist/build-record/assemble.d.ts.map +1 -0
  22. package/dist/build-record/index.d.ts +39 -0
  23. package/dist/build-record/index.d.ts.map +1 -0
  24. package/dist/build-record/sources.d.ts +80 -0
  25. package/dist/build-record/sources.d.ts.map +1 -0
  26. package/dist/build-record/template.d.ts +18 -0
  27. package/dist/build-record/template.d.ts.map +1 -0
  28. package/dist/build-record/types.d.ts +163 -0
  29. package/dist/build-record/types.d.ts.map +1 -0
  30. package/dist/build-record/write.d.ts +26 -0
  31. package/dist/build-record/write.d.ts.map +1 -0
  32. package/dist/cli-contract.d.ts +120 -0
  33. package/dist/cli-contract.d.ts.map +1 -0
  34. package/dist/close-phase.d.ts +114 -0
  35. package/dist/close-phase.d.ts.map +1 -0
  36. package/dist/create-engagement.d.ts +248 -0
  37. package/dist/create-engagement.d.ts.map +1 -0
  38. package/dist/device-signin.d.ts +255 -0
  39. package/dist/device-signin.d.ts.map +1 -0
  40. package/dist/engagement-credential.d.ts +267 -0
  41. package/dist/engagement-credential.d.ts.map +1 -0
  42. package/dist/identity.d.ts +76 -0
  43. package/dist/identity.d.ts.map +1 -0
  44. package/dist/index.d.ts +25 -0
  45. package/dist/index.d.ts.map +1 -0
  46. package/dist/index.js +2083 -0
  47. package/dist/index.js.map +6 -0
  48. package/dist/install.d.ts +355 -0
  49. package/dist/install.d.ts.map +1 -0
  50. package/dist/loopback-signin.d.ts +173 -0
  51. package/dist/loopback-signin.d.ts.map +1 -0
  52. package/dist/mcp-endpoint.d.ts +35 -0
  53. package/dist/mcp-endpoint.d.ts.map +1 -0
  54. package/dist/merge-settings.d.ts +57 -0
  55. package/dist/merge-settings.d.ts.map +1 -0
  56. package/dist/mint-board-code.d.ts +41 -0
  57. package/dist/mint-board-code.d.ts.map +1 -0
  58. package/dist/open-phase.d.ts +107 -0
  59. package/dist/open-phase.d.ts.map +1 -0
  60. package/dist/probe-mcp.d.ts +47 -0
  61. package/dist/probe-mcp.d.ts.map +1 -0
  62. package/dist/resolve-credential.d.ts +126 -0
  63. package/dist/resolve-credential.d.ts.map +1 -0
  64. package/dist/scan.d.ts +131 -0
  65. package/dist/scan.d.ts.map +1 -0
  66. package/dist/setup/discovery.d.ts +73 -0
  67. package/dist/setup/discovery.d.ts.map +1 -0
  68. package/dist/setup/engagement.d.ts +99 -0
  69. package/dist/setup/engagement.d.ts.map +1 -0
  70. package/dist/setup/index.d.ts +17 -0
  71. package/dist/setup/index.d.ts.map +1 -0
  72. package/dist/setup/manifest.d.ts +155 -0
  73. package/dist/setup/manifest.d.ts.map +1 -0
  74. package/dist/setup/rows.d.ts +57 -0
  75. package/dist/setup/rows.d.ts.map +1 -0
  76. package/package.json +55 -0
  77. package/scaffolding/test/fixtures/captured/.gitkeep +0 -0
  78. 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"}