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,255 @@
1
+ /**
2
+ * The BROWSER HANDOFF, client half — `npx halfcycle` signing a machine in (T-08,
3
+ * feature `connect-signup-and-credential`; **loopback added by T-25, #614**).
4
+ *
5
+ * The CLI mints a device code, opens a browser, and waits until a signed-in human
6
+ * approves or refuses. That direction — CLI mints, browser redeems — is the same as
7
+ * the board's enter-code flow.
8
+ *
9
+ * ---------------------------------------------------------------------------
10
+ * SINCE T-25 THERE ARE TWO WAYS THE ANSWER COMES BACK, and `signIn` below is where
11
+ * the choice is made:
12
+ *
13
+ * LOOPBACK (the default) — the CLI listens on `127.0.0.1`, the page redirects the
14
+ * browser back to it, and NO CODE IS SHOWN. `loopback-signin.ts` owns the listener.
15
+ *
16
+ * DEVICE CODE (the fallback) — the code is printed, the human compares it with the
17
+ * page, and this file polls. Unchanged, and the right answer whenever the browser
18
+ * is not on this machine.
19
+ *
20
+ * The fallback is chosen from two MEASUREMENTS — a bind that failed, or a browser
21
+ * opener that failed — and never from reading the environment and guessing. See
22
+ * `signIn` and `openBrowser`.
23
+ *
24
+ * ---------------------------------------------------------------------------
25
+ * THE WIRE SHAPES ARE IMPORTED, NEVER RE-DECLARED. Every payload and every `status`
26
+ * value comes from `@halfcycle/events`, which is where this contract lives since
27
+ * this task (INV-003, INV-001 — `packages/events` is on the shippable allowlist).
28
+ * A hand-written `interface StartResponse` here would be a second home for a shape
29
+ * whose other end is a deployed service, and the two would agree until the first
30
+ * time one of them changed.
31
+ *
32
+ * The responses are PARSED, not cast. A cast asserts nothing: a plane that answered
33
+ * `{}` would satisfy `as DeviceAuthStartResponse` and produce a CLI that opened
34
+ * `undefined` in a browser and polled with an empty code for fifteen minutes.
35
+ *
36
+ * ---------------------------------------------------------------------------
37
+ * FOUR WAYS THIS ENDS, and each is a different sentence and a non-zero exit:
38
+ *
39
+ * `declined` — a human refused at the page. Nothing was created.
40
+ * `expired` — the code's window closed on CONTROL's clock before anyone acted.
41
+ * `unknown` — the server is not waiting on this code (never minted, or spent).
42
+ * unreachable — the plane stopped answering mid-poll. This one is OURS to word,
43
+ * because control cannot send a sentence about a request it never
44
+ * received.
45
+ *
46
+ * The first three are control's own words, forwarded verbatim from
47
+ * `@halfcycle/events`, because they were written for exactly this reader and each
48
+ * already names what to do next. Re-wording them here would give the terminal and
49
+ * the confirmation page two different accounts of one event.
50
+ *
51
+ * ---------------------------------------------------------------------------
52
+ * THE EXPIRY IS THE SERVER'S. The poll runs until control answers a terminal state
53
+ * or until the local clock passes the `expiresAt` control declared, plus one poll
54
+ * interval of slack. The client never decides that a code has expired while control
55
+ * still considers it live — it keeps asking, and control's `expired` is what ends
56
+ * the wait. The local deadline exists only so an unreachable plane cannot make this
57
+ * loop unbounded; crossing it is reported as a TIMEOUT, in its own words, and never
58
+ * as "expired", which would be a claim about a server state nobody observed.
59
+ *
60
+ * INV-001: Node builtins, the global `fetch` (Node 20), and `@halfcycle/events`.
61
+ */
62
+ import { type DeviceAuthStartResponse, type DevicePollApproved } from '@halfcycle/events';
63
+ /**
64
+ * A sign-in that ended without a credential.
65
+ *
66
+ * `status` carries control's own discriminant when control produced one, and the
67
+ * local `'timed-out'` when the plane stopped answering. It is exposed so a caller
68
+ * can branch (`bin.ts` does not, today — it prints and exits — but a refusal a
69
+ * caller cannot tell apart is a refusal that gets retried in a loop by the next
70
+ * person who tries).
71
+ */
72
+ export declare class SignInRefused extends Error {
73
+ readonly status: string;
74
+ constructor(status: string, message: string);
75
+ }
76
+ /** The local status for "the plane stopped answering". Not a control status. */
77
+ export declare const SIGN_IN_TIMED_OUT = "timed-out";
78
+ /**
79
+ * The sentence for a poll that outlived its local deadline without control ever
80
+ * answering. Distinct from `DEVICE_EXPIRED_MESSAGE` on purpose: that one is a fact
81
+ * control asserted, this one is a fact only the client observed, and telling a
82
+ * developer their code expired when what actually happened is that the server went
83
+ * away sends them to re-run instead of to check their connection.
84
+ */
85
+ export declare const SIGN_IN_TIMEOUT_MESSAGE: string;
86
+ /**
87
+ * The sentence for a non-interactive run with no credential in the environment.
88
+ *
89
+ * A CI job has no browser and nobody to look at a code, so minting one and waiting
90
+ * fifteen minutes for a human who does not exist is the worst available failure: it
91
+ * burns the job's time and then reports something that reads like a platform
92
+ * outage. It refuses immediately instead, and names the ONE thing a workflow can
93
+ * actually do.
94
+ */
95
+ export declare const SIGN_IN_NON_INTERACTIVE_MESSAGE: string;
96
+ /** Everything this module touches that a test may need to hold still. */
97
+ export interface SignInDeps {
98
+ /** Where the CLI writes its narration. Defaults to stdout. */
99
+ write?: (text: string) => void;
100
+ /** Sleep between polls. Defaults to a real timer. */
101
+ sleep?: (ms: number) => Promise<void>;
102
+ /** The clock the local deadline is measured on. Defaults to `Date.now`. */
103
+ now?: () => number;
104
+ }
105
+ /**
106
+ * The platform command that opens a URL in the user's browser, as `{command, args}`.
107
+ *
108
+ * A PURE FUNCTION so the argv can be asserted on every platform from any platform —
109
+ * the alternative is a test that only ever checks the branch it happens to be
110
+ * running on, which on this repo's CI would mean the Windows and Linux arms were
111
+ * never looked at.
112
+ *
113
+ * NO SHELL, EVER. The URL is an argv element, so a value containing shell
114
+ * metacharacters is an odd URL and not a command. `start` on Windows is the one
115
+ * that would need a shell, so it is invoked through `cmd /c start ""` with an empty
116
+ * title argument — without that empty string, `start` treats a quoted URL as the
117
+ * window title and opens nothing.
118
+ */
119
+ export declare function browserOpenCommand(os: string, url: string): {
120
+ command: string;
121
+ args: string[];
122
+ };
123
+ /**
124
+ * Is this URL safe to hand to the platform opener?
125
+ *
126
+ * The address comes off the wire, so it is a value a compromised or misconfigured
127
+ * plane controls. Only `http`/`https` are opened; anything else is printed instead.
128
+ * The opener takes argv rather than a shell string, so the exposure was bounded
129
+ * already — this closes the rest of it, where a `file://` URL would open a local
130
+ * document and a handler-scheme URL would launch whatever claims that scheme.
131
+ */
132
+ export declare function isOpenableUrl(url: string): boolean;
133
+ /** What was MEASURED when this run tried to open a browser. */
134
+ export interface BrowserOpenAttempt {
135
+ /** A browser was launched. Nobody claims the human looked at it. */
136
+ opened: boolean;
137
+ /**
138
+ * Why not, in a clause that fits inside one sentence. Present iff `opened` is
139
+ * false, and it always names an OBSERVATION — an exit code, a spawn error, an
140
+ * explicit instruction — never an inference about the kind of machine this is.
141
+ */
142
+ reason?: string;
143
+ }
144
+ /**
145
+ * Open a browser, and MEASURE whether that worked.
146
+ *
147
+ * ---------------------------------------------------------------------------
148
+ * THIS FUNCTION IS ACCEPTANCE CRITERION 3. The loopback flow needs to know whether a
149
+ * browser actually came up, because that is half of what decides whether the sign-in
150
+ * can complete without a code — and the criterion says the decision must come from a
151
+ * measurement, not from sniffing the environment and guessing.
152
+ *
153
+ * **The previous version could not answer the question, and returned `true` as
154
+ * though it could.** It spawned `detached`, attached an empty `error` sink and
155
+ * returned `true` the instant `spawn` did not throw synchronously — but `spawn`
156
+ * reports a missing binary ASYNCHRONOUSLY, on the `error` event, and reports a
157
+ * non-zero exit later still. So `openBrowser('…')` on a box with no `xdg-open`
158
+ * returned `true`. That was harmless while the answer only chose between two
159
+ * printed sentences and the poll ran either way; it is not harmless now, because a
160
+ * wrong `true` here is a terminal that waits in silence for a browser that never
161
+ * opened.
162
+ *
163
+ * So the child is spawned ATTACHED and watched for whichever comes first:
164
+ * - `error` → could not run it at all (`ENOENT` — no `xdg-open` on PATH).
165
+ * - `exit(≠0)` → it ran and refused (`xdg-open` with no display).
166
+ * - `exit(0)` → handed off (macOS/Windows).
167
+ * - still up after `BROWSER_OPEN_GRACE_MS` → it is the browser (Linux).
168
+ *
169
+ * On the last arm the child is `unref`'d, so a browser that outlives the install
170
+ * cannot hold this process open.
171
+ *
172
+ * ---------------------------------------------------------------------------
173
+ * `HALFCYCLE_NO_BROWSER` IS AN INSTRUCTION, NOT A HEURISTIC, and the distinction is
174
+ * exactly the one criterion 3 draws. It is the developer saying *do not take my
175
+ * browser over*; honouring it is not a guess about the machine, and the reason
176
+ * reported says so in those words. Nothing else in this flow reads the environment
177
+ * to decide which path to take.
178
+ */
179
+ export declare function openBrowser(url: string, env?: NodeJS.ProcessEnv): Promise<BrowserOpenAttempt>;
180
+ /** True iff this run has no human at the keyboard. */
181
+ export declare function isNonInteractive(env?: NodeJS.ProcessEnv): boolean;
182
+ /** `POST /auth/device/start` — mint a code. Throws `SignInRefused` on a refusal. */
183
+ export declare function startDeviceAuth(serviceUrl: string): Promise<DeviceAuthStartResponse>;
184
+ /**
185
+ * Poll until control answers a terminal state, or until the local deadline.
186
+ *
187
+ * A TRANSIENT NETWORK FAILURE IS NOT A REFUSAL. A dropped packet mid-poll gets
188
+ * another attempt at the next interval, because the developer is standing in front
189
+ * of a browser they have already signed into and the credential is waiting on the
190
+ * server. Only the deadline ends the loop — and it is reported as a timeout, in the
191
+ * client's own words, never as one of control's three refusals.
192
+ */
193
+ export declare function pollForCredential(serviceUrl: string, started: DeviceAuthStartResponse, deps?: SignInDeps): Promise<DevicePollApproved>;
194
+ /** What a completed sign-in yields. */
195
+ export interface SignedIn {
196
+ accountId: string;
197
+ credential: string;
198
+ /** Control's issue-time expiry, UTC ISO-8601. Information, never a local gate. */
199
+ expiresAt: string;
200
+ }
201
+ /**
202
+ * The sentence for a loopback sign-in whose browser never came back.
203
+ *
204
+ * DISTINCT FROM `SIGN_IN_TIMEOUT_MESSAGE`, which is about a plane that stopped
205
+ * answering. Here the plane was fine and the terminal was waiting on a redirect from
206
+ * the browser — so the recovery is different, and it names the flow that does not
207
+ * need one.
208
+ */
209
+ export declare const LOOPBACK_TIMEOUT_MESSAGE: string;
210
+ /**
211
+ * The whole terminal experience — **loopback first, the device code as the fallback**
212
+ * (T-25, #614).
213
+ *
214
+ * ---------------------------------------------------------------------------
215
+ * THE TWO PATHS, AND HOW THE CHOICE BETWEEN THEM IS MADE.
216
+ *
217
+ * LOOPBACK — bind `127.0.0.1:0`, put that port in the URL we open, and wait for
218
+ * the confirmation page to send the browser back. **No code is displayed at all**
219
+ * (criterion 1): there is nothing for the human to compare, so printing a code
220
+ * would be printing a value with no use.
221
+ *
222
+ * DEVICE CODE — print the code, open (or print) the page, and poll. Unchanged from
223
+ * T-08, still tested, and still correct for every machine whose browser is not the
224
+ * one this process can talk to: SSH, containers, remote dev boxes, Codespaces.
225
+ *
226
+ * **THE FALLBACK IS CHOSEN BY TWO MEASUREMENTS AND NOTHING ELSE** (criterion 3):
227
+ * `bindLoopback` tried to listen and reports the `EADDRINUSE`/`EACCES` it got, and
228
+ * `openBrowser` spawned the platform opener and reports its exit code or spawn error.
229
+ * Nothing here reads `SSH_CONNECTION`, `DISPLAY`, `SSH_TTY`, `TERM`, `container` or
230
+ * any other variable to infer what kind of machine this is — an inference that is
231
+ * wrong in both directions, since a container with a forwarded browser has no
232
+ * `DISPLAY` and works, and a kiosked desktop has one and does not. The single
233
+ * environment variable consulted, `HALFCYCLE_NO_BROWSER`, is the developer's own
234
+ * instruction not to open a browser, and it is honoured rather than interpreted.
235
+ *
236
+ * **ORDER MATTERS AND IS DELIBERATE.** The listener is bound BEFORE the code is
237
+ * minted, so a machine that cannot listen has burned no code by the time it falls
238
+ * back; and the code is minted once for both paths, so a fallback is never a second
239
+ * `POST /auth/device/start`.
240
+ *
241
+ * **THE FALLBACK OPENS THE UNDECORATED URL.** `started.verificationUrl` is what
242
+ * control composed; the loopback parameters are added only for the loopback attempt.
243
+ * Handing the page a port nothing is listening on would make it redirect the browser
244
+ * to a connection error after a successful approval — a page that looks broken for a
245
+ * sign-in that worked.
246
+ *
247
+ * WHAT IS PRINTED AND WHAT IS NOT, on the device-code path: the USER code is printed
248
+ * — it is a selector a human compares between two screens and yields nothing to
249
+ * whoever reads it, since only the device code can claim the credential. The DEVICE
250
+ * code and the credential are printed nowhere, ever, and appear in no URL.
251
+ */
252
+ export declare function signIn(serviceUrl: string, deps?: SignInDeps & {
253
+ env?: NodeJS.ProcessEnv;
254
+ }): Promise<SignedIn>;
255
+ //# sourceMappingURL=device-signin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"device-signin.d.ts","sourceRoot":"","sources":["../src/device-signin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4DG;AAIH,OAAO,EAKL,KAAK,uBAAuB,EAC5B,KAAK,kBAAkB,EACxB,MAAM,mBAAmB,CAAC;AAG3B;;;;;;;;GAQG;AACH,qBAAa,aAAc,SAAQ,KAAK;aAEpB,MAAM,EAAE,MAAM;gBAAd,MAAM,EAAE,MAAM,EAC9B,OAAO,EAAE,MAAM;CAKlB;AAED,gFAAgF;AAChF,eAAO,MAAM,iBAAiB,cAAc,CAAC;AAE7C;;;;;;GAMG;AACH,eAAO,MAAM,uBAAuB,QAGV,CAAC;AAE3B;;;;;;;;GAQG;AACH,eAAO,MAAM,+BAA+B,QAGsC,CAAC;AAQnF,yEAAyE;AACzE,MAAM,WAAW,UAAU;IACzB,8DAA8D;IAC9D,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,qDAAqD;IACrD,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,2EAA2E;IAC3E,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAUD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAChC,EAAE,EAAE,MAAM,EACV,GAAG,EAAE,MAAM,GACV;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,CAIrC;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAOlD;AAaD,+DAA+D;AAC/D,MAAM,WAAW,kBAAkB;IACjC,oEAAoE;IACpE,MAAM,EAAE,OAAO,CAAC;IAChB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAsB,WAAW,CAC/B,GAAG,EAAE,MAAM,EACX,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,OAAO,CAAC,kBAAkB,CAAC,CAmE7B;AAED,sDAAsD;AACtD,wBAAgB,gBAAgB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAG9E;AAED,oFAAoF;AACpF,wBAAsB,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAsC1F;AAYD;;;;;;;;GAQG;AACH,wBAAsB,iBAAiB,CACrC,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,uBAAuB,EAChC,IAAI,GAAE,UAAe,GACpB,OAAO,CAAC,kBAAkB,CAAC,CAmD7B;AAED,uCAAuC;AACvC,MAAM,WAAW,QAAQ;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,kFAAkF;IAClF,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,wBAAwB,QAIF,CAAC;AAiEpC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,wBAAsB,MAAM,CAC1B,UAAU,EAAE,MAAM,EAClB,IAAI,GAAE,UAAU,GAAG;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAA;CAAO,GAClD,OAAO,CAAC,QAAQ,CAAC,CAmFnB"}
@@ -0,0 +1,267 @@
1
+ /**
2
+ * THE PER-ENGAGEMENT CREDENTIAL STORE — `~/.halfcycle/engagements/<id>/env`
3
+ * (T-09, feature `connect-signup-and-credential`).
4
+ *
5
+ * This is the credential that authenticates a REPOSITORY to the Halfcycle plane:
6
+ * the session token minted once on create, under the two names its two consumers
7
+ * read, plus the three origins and the engagement id. Until T-09 it lived in the
8
+ * client tree at `.env.local`, gitignored. It now lives OUTSIDE every repository,
9
+ * owner-only, keyed on the engagement.
10
+ *
11
+ * ---------------------------------------------------------------------------
12
+ * WHY OUT OF THE TREE AT ALL. Gitignoring a secret is a discipline; putting it
13
+ * where the repository cannot reach it is a structure. `.gitignore` is a tracked
14
+ * file a developer can edit, a `git add -f` overrides it, an editor's "commit all"
15
+ * has ignored it before, and a zip export carries the file regardless. Studio has
16
+ * never had this exposure because its layout put the per-engagement token at
17
+ * machine level from the start (`docs/features/mac-workspace-provider.md:27`),
18
+ * which is described there as making the moat *"structural, not disciplinary"*.
19
+ * This module adopts that layout for Connect. Nothing here is new design; it is
20
+ * one topology's answer applied to the other (INV-022 — Connect reaches it with a
21
+ * home directory and nothing else).
22
+ *
23
+ * ---------------------------------------------------------------------------
24
+ * IT IS AN ENV FILE AND NOT JSON, AND THAT IS FORCED — not a preference.
25
+ *
26
+ * Two consumers read this credential and both are POSIX `sh`:
27
+ *
28
+ * - `.claude/hooks/guard-runner.sh` SOURCES it (`set -a; . "$f"; set +a`) so the
29
+ * runner, which reads only `process.env`, receives its three variables. That is
30
+ * the INV-001 seam: env loading lives in the hook wiring, never in the binary.
31
+ * - `.halfcycle/mcp-headers.sh` READS one `KEY=` line out of it.
32
+ *
33
+ * Neither can parse JSON — there is no `jq` in this environment and `node` is not
34
+ * dependable on a desktop-launched session's PATH (the reason `mcp-headers.sh` is
35
+ * `sh` and not node in the first place). So the machine-level file is `KEY=value`
36
+ * lines, exactly as Studio's is, and the ONE field that must come out of the
37
+ * committed pin — the engagement id — is cut out of JSON with `sed`. See
38
+ * `engagementResolutionShell()` for that, and for why `tr -d '\n'` precedes it.
39
+ *
40
+ * ---------------------------------------------------------------------------
41
+ * THE SEVEN — NOW EIGHT (T-10) — KEYS ARE FOUR THINGS, AND THE COUNT DECIDES WHAT
42
+ * MAY BE COMMITTED. Four public origins (the fourth, `CONTROL_TELEMETRY_URL`,
43
+ * joined at T-10), the engagement id twice, and ONE token under two names —
44
+ * `HALFCYCLE_TOKEN` and `GUARD_SERVICE_TOKEN` are byte-identical, one credential
45
+ * under the two names its two consumers read. So six of the eight keys are not
46
+ * secret at all. **They still do not go into the committed pin.** An earlier draft
47
+ * of the feature spec counted six non-secret keys out of SEVEN, which would have
48
+ * sent `GUARD_SERVICE_TOKEN` to a tracked file — the exact outcome this work exists
49
+ * to prevent — and the safe reading is the one that keeps the credential's key set
50
+ * whole and in one place: everything the shells need is here, and the pin
51
+ * contributes only the id that names this directory. `CONTROL_TELEMETRY_URL`
52
+ * joining the non-secret count does not reopen that count: it is a fourth origin
53
+ * added after the draft that got the arithmetic wrong, not a recount of the same
54
+ * seven.
55
+ *
56
+ * ---------------------------------------------------------------------------
57
+ * OWNER-ONLY, AND WHAT THAT MEANS WHERE IT MEANS NOTHING (phase decision 6).
58
+ *
59
+ * - macOS and Linux: the directory is created `0700` and the file written `0600`,
60
+ * then `chmodSync`'d explicitly AFTER the write — `writeFileSync`'s `mode` is
61
+ * honoured only when the file is CREATED, so a store that already existed at
62
+ * `0644` would keep those bits forever, a permission bug with no symptom.
63
+ * - Windows: POSIX mode bits are meaningless and `chmod` is a near no-op, so the
64
+ * call is SKIPPED rather than issued-and-ignored. Nothing here reads as a
65
+ * protection that is not there. What Windows does get is the part that matters
66
+ * most and is platform-independent: the credential is in `%USERPROFILE%`, not
67
+ * in the repository, so it cannot be committed, pushed or zipped with the
68
+ * product. Per-user ACLs on `%USERPROFILE%` are the OS's own default and are
69
+ * not something this installer sets or can honestly claim to have set. The two
70
+ * generated consumers are POSIX `sh` and run under Git Bash / WSL there.
71
+ *
72
+ * The same posture, for the same reasons, as `account-credential.ts` — which is
73
+ * why `halfcycleHome()` is imported from it rather than re-derived.
74
+ *
75
+ * ---------------------------------------------------------------------------
76
+ * TWO HOMES FOR THIS PATH SHAPE ACROSS THE REPO, AND IT IS A PRESENT CONSTRAINT,
77
+ * NOT AN OVERSIGHT. `packages/mac-workspace-provider/src/registry.ts` has its own
78
+ * `engagementEnvPath()` for the identical path — Studio's writer. `packages/bundle`
79
+ * is shippable and its import allowlist is exactly `{packages/core, packages/events}`
80
+ * (INV-001), and the provider is hosted-only, so the two cannot share a function
81
+ * today. `packages/core` is the one package both may import and is where this
82
+ * belongs eventually; that is the same direction `GUARD_ENV_KEYS`'s docblock in
83
+ * `install.ts` records as #476, and it is deliberately not done mid-wave. What
84
+ * binds the two meanwhile is a test that reads the provider's source text off disk
85
+ * and asserts the shapes agree (`test/credential-out-of-repo.test.ts`) — an `fs`
86
+ * read, not an import, so there is no build edge and nothing crosses the moat.
87
+ *
88
+ * INV-001: Node builtins and this package's own modules only.
89
+ */
90
+ /**
91
+ * The field the committed pin (`.halfcycle/bundle.json`) carries the engagement id
92
+ * under. Named once here because BOTH readers of it — `readEngagementCredential`'s
93
+ * TS caller and the generated POSIX `sed` — must agree on it, and the pin's writer
94
+ * (`writeBundlePin`) is in `install.ts`. A test installs, then runs the generated
95
+ * shell against the real written pin and compares its answer to
96
+ * `engagementEnvPath()`, so writer and both readers are asserted to agree rather
97
+ * than left to match by eye.
98
+ */
99
+ export declare const PIN_ENGAGEMENT_ID_FIELD = "engagementId";
100
+ /** `~/.halfcycle/engagements/<id>/` — this engagement's machine-level state dir. */
101
+ export declare function engagementStateDir(engagementId: string, home?: string): string;
102
+ /** `~/.halfcycle/engagements/<id>/env` — the owner-only per-engagement env file. */
103
+ export declare function engagementEnvPath(engagementId: string, home?: string): string;
104
+ /**
105
+ * The three variables the generated guard hook requires — **the bundle's one home
106
+ * for those names.** Anything in this package that needs to name them (the write
107
+ * set below, the installer's report in `bin.ts`) reads them from here rather than
108
+ * retyping them into a string.
109
+ *
110
+ * **This is the SECOND copy across the repo — a PRESENT constraint, not a permanent
111
+ * one (see #476).** The authoritative list is `packages/runner/src/config.ts`, the
112
+ * runner's sole `process.env` read site, and a shippable may not import the runner,
113
+ * which is wired as an external binary contract — so today the two cannot share a
114
+ * constant. **`packages/core` is the one package both shippables may import under
115
+ * INV-001**, so moving this constant there would remove the duplication rather than
116
+ * test around it; that is #476, deliberately not done mid-wave, because a
117
+ * shared-package change during an active wave is a class this repo has been burned
118
+ * by. Do not read the paragraph below as an argument that the duplication is fine.
119
+ *
120
+ * What binds the two copies meanwhile is **structural, not coincidental**:
121
+ * `readConfig()` is a conjunction over the exact required set, and AC1's assertion
122
+ * in `install-guard-config.test.ts` sits strictly downstream of it — it fires the
123
+ * REAL installed wrapper at a recording guard service and asserts the evaluation
124
+ * ARRIVES, through a wrapper that names no variable. So any element the bundle
125
+ * fails to write breaks the conjunction observably, and — the row that decides it —
126
+ * **the runner adding a fourth required variable is caught with the bundle knowing
127
+ * nothing about it**, which a name-to-name comparison cannot do: that would pass on
128
+ * two lists agreeing with each other and with nothing. The one gap is superset
129
+ * drift (the bundle writes a name the runner ignores), which is the harmless
130
+ * direction and is partly caught by the `.env.example` subset check.
131
+ *
132
+ * (Moved here from `install.ts` at T-09 with its reasoning intact: the write set it
133
+ * feeds moved out of the repository, and the names belong beside the file they are
134
+ * written into.)
135
+ */
136
+ export declare const GUARD_ENV_KEYS: readonly ["GUARD_SERVICE_URL", "GUARD_SERVICE_TOKEN", "GUARD_ENGAGEMENT_ID"];
137
+ /**
138
+ * The eight keys the engagement credential owns in the machine-level env file.
139
+ * These are the ONLY keys the reconcile adds or updates — see `reconcileEnvText`.
140
+ *
141
+ * `CONTROL_TELEMETRY_URL` JOINED THIS LIST AT T-10 (phase decision 13): without it
142
+ * no Connect engagement emitted telemetry, which is why `guard_results` was empty
143
+ * on prod. **It is written as `''` (never omitted) when the plane reports none** —
144
+ * `writeEngagementCredential` in `install.ts` keys this record on
145
+ * `EngagementEnvKey`, so a missing entry is a compile error rather than a silent
146
+ * gap, and `toCredential` there treats a blank value as absent. It is deliberately
147
+ * NOT in `CREDENTIAL_REQUIRED_KEYS` (`install.ts`) — that conjunction decides
148
+ * whether a checkout is treated as holding a credential at all, and putting an
149
+ * eighth, telemetry-only key into it would make every repository installed before
150
+ * this task (which has the other seven and not this one) read as credential-less
151
+ * on its next run and silently join a fresh one. See `DeviceCredential`'s docblock.
152
+ */
153
+ export declare const ENGAGEMENT_ENV_KEYS: readonly ["HALFCYCLE_SERVICE_URL", "HALFCYCLE_MCP_URL", "HALFCYCLE_TOKEN", "HALFCYCLE_ENGAGEMENT_ID", "GUARD_SERVICE_URL", "GUARD_SERVICE_TOKEN", "GUARD_ENGAGEMENT_ID", "CONTROL_TELEMETRY_URL"];
154
+ export type EngagementEnvKey = (typeof ENGAGEMENT_ENV_KEYS)[number];
155
+ /**
156
+ * Parse `KEY=value` lines. **PARSES, NEVER SOURCES** — a credential store is not
157
+ * code, and `.` would execute whatever the file happens to contain. An optional
158
+ * leading `export ` is tolerated because the shells that source this file accept
159
+ * one, so a hand-edited store must read the same both ways, and one layer of
160
+ * matching quotes is stripped for the same reason (see the encoding note above).
161
+ *
162
+ * (The one exception is `guard-runner.sh`, which genuinely sources rather than
163
+ * parses: it must EXPORT the values into a child process, which is a thing only the
164
+ * shell can do. That file is generated by us, written `0600` outside every
165
+ * repository, and is Studio's adopted contract — a narrower surface than the
166
+ * developer-editable `.env.local` it replaces.)
167
+ */
168
+ export declare function parseEnvText(raw: string): Record<string, string>;
169
+ /**
170
+ * Reconcile our keys into an existing env-file body BY KEY.
171
+ *
172
+ * Every pre-existing line survives byte-for-byte: a `KEY=value` line whose key is
173
+ * one of ours is updated IN PLACE (so key order is preserved and no duplicate is
174
+ * introduced); one of ours that is absent is appended under the header; and any
175
+ * other line — comments, blanks, a key some other Halfcycle writer put here — is
176
+ * copied through untouched.
177
+ *
178
+ * **`export KEY=` counts as that key.** `parseEnvText` and the installer's legacy
179
+ * migration both tolerate the prefix, and this function did not — so an existing
180
+ * `export HALFCYCLE_TOKEN=old` was left alone and a bare `HALFCYCLE_TOKEN=new`
181
+ * appended below it. Last-assignment-wins meant nothing observably broke, which is
182
+ * why it survived: a superseded token simply stayed on disk, in the one feature
183
+ * whose whole subject is where a credential is allowed to be. The prefix is
184
+ * preserved on the rewritten line rather than normalised away, because "every
185
+ * pre-existing line survives" is the contract and `export` is the developer's.
186
+ *
187
+ * Values are written `shq`-quoted — see the encoding note above; the file is
188
+ * SOURCED by `guard-runner.sh`, so an unquoted value carrying a space executes
189
+ * its own remainder.
190
+ *
191
+ * **The non-destructiveness is not vestigial after the move out of the tree.** This
192
+ * path is Studio's too: `packages/mac-workspace-provider` writes
193
+ * `CONTROL_TELEMETRY_TOKEN` and `HALFCYCLE_GUARD_EVAL_LOG_DIR` into the same file for
194
+ * an engagement it manages. A whole-file overwrite here would silently delete them on
195
+ * any machine where both ever touch one engagement. Reconciling by key costs nothing
196
+ * and removes that whole class.
197
+ *
198
+ * **`CONTROL_TELEMETRY_URL` is deliberately NOT named alongside those two** — this
199
+ * said `CONTROL_TELEMETRY_*` (the whole family) until a T-10 follow-up correction.
200
+ * `CONTROL_TELEMETRY_URL` is one of OURS since T-10 (`ENGAGEMENT_ENV_KEYS` above), so
201
+ * it is reconciled the same as `HALFCYCLE_TOKEN` — updated in place by a Connect
202
+ * install, not preserved as a foreign writer's key. A Studio-written value for it is
203
+ * legitimately overwritten on the next Connect install; only `CONTROL_TELEMETRY_TOKEN`
204
+ * is still genuinely foreign.
205
+ */
206
+ export declare function reconcileEnvText(existing: string | null, values: Record<string, string>, keys?: readonly string[]): string;
207
+ /**
208
+ * Read this engagement's machine-level credential, or `null` when this machine
209
+ * holds none for it.
210
+ *
211
+ * A missing or unreadable file is `null`, never a throw: the commonest reason to
212
+ * have none is a fresh clone of an engaged repository, which is an ordinary state
213
+ * the caller reports in words rather than a fault.
214
+ */
215
+ export declare function readEngagementEnv(engagementId: string, home?: string): Record<string, string> | null;
216
+ /** What a write did, in the installer's report vocabulary. */
217
+ export type EnvWriteOutcome = 'written' | 'skipped';
218
+ /**
219
+ * Write this engagement's credential to the machine-level store, owner-only.
220
+ *
221
+ * Idempotent: identical content is `skipped`, so a re-install writes nothing here
222
+ * and the installer's *"a second install writes nothing but the bundle pin"*
223
+ * invariant survives the move out of the tree.
224
+ *
225
+ * The mode is re-applied on the skipped path too — a store that already existed at
226
+ * the wrong mode is exactly the case `writeFileSync`'s `mode` option cannot fix,
227
+ * and a re-install is the natural moment to repair it.
228
+ */
229
+ export declare function writeEngagementEnv(engagementId: string, values: Record<string, string>, home?: string): EnvWriteOutcome;
230
+ /**
231
+ * The shell function both generated consumers embed to find this file.
232
+ *
233
+ * **ONE authoring home for the resolution, two generated copies**, and that is the
234
+ * deliberate choice over a third installed script the other two would source: a
235
+ * shared script is one more file that can be missing, collide with a developer's
236
+ * own, or be lost by an export — and its absence would fail BOTH consumers at once,
237
+ * silently, which is the failure mode this whole feature is closing. Both copies
238
+ * come from this function, so there is nothing to keep in step by hand, and the
239
+ * path shape itself is composed from the same constants `engagementEnvPath()` uses.
240
+ *
241
+ * **Why `tr -d '\n'` before `sed`.** The pin is written pretty-printed
242
+ * (`JSON.stringify(pin, null, 2)`), so the key and its value are on one line today —
243
+ * but `sed` is line-oriented and a pin reformatted by any other tool would put them
244
+ * on two, at which point a line-wise match silently finds nothing and the developer
245
+ * gets "no credential" on a repository that has one. Flattening first makes the
246
+ * match independent of the file's line breaks. (This repo has paid twice for a
247
+ * pattern that wrapped across lines reading as absent.)
248
+ *
249
+ * **Why not `node`.** `mcp-headers.sh` is `sh` and not node precisely because a
250
+ * desktop-launched session's PATH typically has no version-manager `node`. Using it
251
+ * here would put that back.
252
+ *
253
+ * **IT SETS VARIABLES AND PRINTS NOTHING, and that is not a style choice.** The
254
+ * obvious form — print the path and let the caller take it with `$(…)` — puts the
255
+ * function in a SUBSHELL, so the `HALFCYCLE_ENV_PROBLEM` it sets on the failure
256
+ * path never reaches the caller. Measured: under `set -u` (which `mcp-headers.sh`
257
+ * runs with) the caller then dies on `HALFCYCLE_ENV_PROBLEM: unbound variable`
258
+ * instead of printing the reason it was carrying. So the function assigns
259
+ * `HALFCYCLE_ENV_FILE` / `HALFCYCLE_ENV_PROBLEM` / `HALFCYCLE_ENV_ENGAGEMENT` in
260
+ * the CURRENT shell and returns a status, and callers use `if halfcycle_env_file …`.
261
+ *
262
+ * `$HALFCYCLE_ENV_PROBLEM` names which step failed — `no-pin`, `no-id`, `no-home`,
263
+ * `no-credential` — so each caller says the true thing for its own surface instead
264
+ * of printing one sentence for four different situations.
265
+ */
266
+ export declare function engagementResolutionShell(): string;
267
+ //# sourceMappingURL=engagement-credential.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engagement-credential.d.ts","sourceRoot":"","sources":["../src/engagement-credential.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwFG;AAiBH;;;;;;;;GAQG;AACH,eAAO,MAAM,uBAAuB,iBAAiB,CAAC;AAEtD,oFAAoF;AACpF,wBAAgB,kBAAkB,CAAC,YAAY,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAE9E;AAED,oFAAoF;AACpF,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAE7E;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,eAAO,MAAM,cAAc,8EAIjB,CAAC;AAEX;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,mBAAmB,mMAOtB,CAAC;AAEX,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,CAAC,CAAC;AAmEpE;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAUhE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,GAAG,IAAI,EACvB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,IAAI,GAAE,SAAS,MAAM,EAAwB,GAC5C,MAAM,CA8BR;AAMD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,YAAY,EAAE,MAAM,EACpB,IAAI,CAAC,EAAE,MAAM,GACZ,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAM/B;AAED,8DAA8D;AAC9D,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,SAAS,CAAC;AAEpD;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAChC,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,IAAI,CAAC,EAAE,MAAM,GACZ,eAAe,CAkBjB;AA4BD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAgB,yBAAyB,IAAI,MAAM,CA0ClD"}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Project identity — mint or read `.halfcycle/project.json`.
3
+ *
4
+ * A repository is not a machine. Two developers cloning one repository, or one
5
+ * developer with five git worktrees, are ONE project. The identity is committed
6
+ * (it arrives with the code) and carries the repository's root commit hash and
7
+ * remote — the most stable identifier a git repository has, identical across
8
+ * every clone, fork and worktree. The remote travels with it so a fork carrying
9
+ * an inherited identity can be detected rather than silently joining someone
10
+ * else's board. The per-device credential is NOT here, and since T-09 it is not
11
+ * anywhere in this repository — it lives at machine level, outside every checkout
12
+ * (`engagement-credential.ts`). See docs/features/client-install-surface.md
13
+ * (operator decision (au), whose committed-identity-versus-device-credential split
14
+ * survives; only the credential's location changed).
15
+ *
16
+ * INV-001: this file imports only Node builtins.
17
+ */
18
+ /**
19
+ * The disambiguation note stamped into every `project.json` (W8-T-04, F-4).
20
+ *
21
+ * `projectId` sits in the same directory as `bundle.json`'s `engagementId`, and a
22
+ * cold session reading `.halfcycle/` top-down took `projectId` for the engagement
23
+ * id — it is a real uuid, so nothing about the VALUE flagged the mistake. The tool
24
+ * description already names the right file (`services/delivery/src/routes/mcp.ts`,
25
+ * #447), but that only reaches a session that calls the tool rather than reading
26
+ * the directory by hand, which is exactly what happened. This note is read
27
+ * whether or not the session ever calls a tool.
28
+ *
29
+ * It does NOT carry the engagement id itself — operator decision (au) keeps that
30
+ * out of the committed identity, and a comment in `create-engagement.ts` records
31
+ * that adding it here would re-open that decision. So the note can only say what
32
+ * this id is *not*, and where the right one lives; it cannot answer the question
33
+ * directly.
34
+ */
35
+ export declare const PROJECT_IDENTITY_NOTE: string;
36
+ /** The committed project identity: minted id plus the repo's root fingerprint. */
37
+ export interface ProjectIdentity {
38
+ /** A minted, stable project id (uuid). Assigned once, at first install. */
39
+ projectId: string;
40
+ /**
41
+ * The repository's root (first) commit hash, or null when the tree is not a
42
+ * git repository / has no commits yet. Identical across every clone, fork and
43
+ * worktree; survives renames and remote changes.
44
+ */
45
+ rootCommit: string | null;
46
+ /** The `origin` remote URL, or null when there is no remote. */
47
+ remote: string | null;
48
+ /**
49
+ * A fixed disambiguation string, always `PROJECT_IDENTITY_NOTE` — see there.
50
+ * Present so a session reading this file directly (not through a tool
51
+ * description) sees the warning too. Reconciled on every install: if an
52
+ * install written before this field existed re-installs, the note is
53
+ * backfilled (`writeProjectIdentity` rewrites iff the serialized bytes
54
+ * differ) — `projectId`/`rootCommit`/`remote` are never touched by that.
55
+ */
56
+ note: string;
57
+ }
58
+ /** Read the repository's root commit hash (the first commit, reachable from HEAD). */
59
+ export declare function readRootCommit(targetRepoRoot: string): string | null;
60
+ /** Read the `origin` remote URL. */
61
+ export declare function readRemote(targetRepoRoot: string): string | null;
62
+ /**
63
+ * Return the existing committed identity if `.halfcycle/project.json` is present,
64
+ * otherwise mint a fresh one. Minting never overwrites the MINTED fields
65
+ * (`projectId`/`rootCommit`/`remote`): an existing identity's own values are
66
+ * returned verbatim so re-install and every clone share one project. `note` is
67
+ * the one exception — it is always stamped to the CURRENT `PROJECT_IDENTITY_NOTE`,
68
+ * on both the mint and the read path, so an install from before this field
69
+ * existed picks up the disambiguation text on its next install rather than
70
+ * staying silent forever.
71
+ */
72
+ export declare function mintOrReadIdentity(targetRepoRoot: string): {
73
+ identity: ProjectIdentity;
74
+ minted: boolean;
75
+ };
76
+ //# sourceMappingURL=identity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identity.d.ts","sourceRoot":"","sources":["../src/identity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAOH;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,qBAAqB,QAGF,CAAC;AAEjC,kFAAkF;AAClF,MAAM,WAAW,eAAe;IAC9B,2EAA2E;IAC3E,SAAS,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,gEAAgE;IAChE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB;;;;;;;OAOG;IACH,IAAI,EAAE,MAAM,CAAC;CACd;AAgBD,sFAAsF;AACtF,wBAAgB,cAAc,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAMpE;AAED,oCAAoC;AACpC,wBAAgB,UAAU,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAEhE;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,cAAc,EAAE,MAAM,GAAG;IAAE,QAAQ,EAAE,eAAe,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,CAmBzG"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @halfcycle/bundle — public API.
3
+ *
4
+ * The bundle is the Halfcycle Method as an installable Claude Code plugin.
5
+ * It exports the install/wiring generator and the drift-check function.
6
+ * Method text (agents, commands, skills, scaffolding) is shipped as static
7
+ * files in the package; this module exports only the programmatic API.
8
+ *
9
+ * INV-001: imports only from @halfcycle/core, @halfcycle/events, or Node builtins.
10
+ */
11
+ export { install, checkDrift, readBundlePin, ZONE_B_DIR } from './install.js';
12
+ export type { InstallOptions, InstallResult, BundlePin, EngagementType, DeviceCredential } from './install.js';
13
+ export { mintOrReadIdentity, readRootCommit, readRemote } from './identity.js';
14
+ export type { ProjectIdentity } from './identity.js';
15
+ export { createEngagement, CreateEngagementRefused } from './create-engagement.js';
16
+ export type { CreatedEngagement } from './create-engagement.js';
17
+ export { mintBoardEnterCode } from './mint-board-code.js';
18
+ export type { MintedBoardCode } from './mint-board-code.js';
19
+ export { mergeSettings } from './merge-settings.js';
20
+ export type { ClaudeSettings, HookEntry, HookCommand } from './merge-settings.js';
21
+ export { runBootstrapScan, scanLayers, mergeWithRecords, HALFCYCLE_STATE_FORMAT } from './scan.js';
22
+ export type { HalfcycleStateFile, ScannedLayerEntry, ScannedLayerValue } from './scan.js';
23
+ export * from './setup/index.js';
24
+ export * from './build-record/index.js';
25
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC9E,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,SAAS,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAC/G,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC/E,YAAY,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AACnF,YAAY,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAyBhE,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,YAAY,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,YAAY,EAAE,cAAc,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClF,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAC;AACnG,YAAY,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAI1F,cAAc,kBAAkB,CAAC;AAKjC,cAAc,yBAAyB,CAAC"}