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,35 @@
1
+ /**
2
+ * ONE HOME for the composition `MCP origin → MCP endpoint URL` (T-24, #446).
3
+ *
4
+ * The control plane reports an ORIGIN (`mcpUrl` on the create-engagement 201,
5
+ * persisted as `HALFCYCLE_MCP_URL`); the method-delivery service serves MCP at
6
+ * `/mcp` on it. Two callers need the composed endpoint and they must not disagree:
7
+ *
8
+ * - `install.ts` — writes it into `.mcp.json` as a LITERAL url (T-24).
9
+ * - `probe-mcp.ts` — calls it at install time to say whether that origin answers.
10
+ *
11
+ * Until T-24 the registration carried `${HALFCYCLE_MCP_URL}/mcp` — a template the
12
+ * client expanded — so `probe-mcp.ts` held the only executable copy of this rule
13
+ * and `install.ts` held a textual one. Now that the installer writes the composed
14
+ * address itself, both copies would be real, and a probe that appended `/mcp`
15
+ * while the registration appended something else would report an origin reachable
16
+ * and still install a registration that reaches nothing. That is the two-homes
17
+ * failure `create-engagement.ts` already names in prose ("the one place that
18
+ * composes the MCP URL"); this module is that one place.
19
+ *
20
+ * INV-001: no imports at all — not even Node builtins.
21
+ */
22
+ /** The path the method-delivery service serves MCP on. Stated once. */
23
+ export declare const MCP_ENDPOINT_PATH = "/mcp";
24
+ /**
25
+ * Compose the MCP endpoint URL for an origin.
26
+ *
27
+ * Trailing slashes on the origin are stripped, so an origin reported as
28
+ * `https://host/` cannot compose `https://host//mcp`. `createEngagement` already
29
+ * normalises what it returns, but `install()` is also called programmatically, so
30
+ * the normalisation lives with the composition rather than only at the boundary.
31
+ *
32
+ * @param origin The MCP origin (no path), e.g. `https://halfcycle-delivery.fly.dev`.
33
+ */
34
+ export declare function mcpEndpointUrl(origin: string): string;
35
+ //# sourceMappingURL=mcp-endpoint.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp-endpoint.d.ts","sourceRoot":"","sources":["../src/mcp-endpoint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,uEAAuE;AACvE,eAAO,MAAM,iBAAiB,SAAS,CAAC;AAExC;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAErD"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Settings merge — the one function whose bug destroys client work.
3
+ *
4
+ * `.claude/settings.json` is MERGED, never replaced (operator decision (aq)).
5
+ * Every key the developer has is kept at its existing value, `$schema` excepted.
6
+ * Halfcycle's three hook entries are appended beside the developer's — possible
7
+ * without conflict because Claude Code runs every hook registered for an event.
8
+ *
9
+ * The merge is UPSERT-BY-IDENTITY, not append: a Halfcycle-authored entry already
10
+ * present is replaced in place, so a second install does not fire the guard twice
11
+ * per edit. Identity is decided by the hook command string, which is the stable,
12
+ * generated marker of a Halfcycle-authored entry (each of our three commands is a
13
+ * distinct `bash ./.claude/hooks/...` invocation).
14
+ *
15
+ * See docs/features/client-install-surface.md, criterion 4.
16
+ * INV-001: this file imports only Node builtins (none needed).
17
+ */
18
+ /** A single hook command as Claude Code's settings schema records it. */
19
+ export interface HookCommand {
20
+ type?: string;
21
+ command: string;
22
+ timeout?: number;
23
+ }
24
+ /** A matcher-scoped group of hook commands for one event. */
25
+ export interface HookEntry {
26
+ matcher?: string;
27
+ hooks: HookCommand[];
28
+ }
29
+ /**
30
+ * The permission rules Claude Code evaluates, deny → ask → allow, first match
31
+ * wins. Only `deny` is ever generated by Halfcycle (F-23 decision (b)); the other
32
+ * two appear here because a developer may own them and they must survive.
33
+ */
34
+ export interface ClaudePermissions {
35
+ deny?: string[];
36
+ allow?: string[];
37
+ ask?: string[];
38
+ [key: string]: unknown;
39
+ }
40
+ /** The subset of `.claude/settings.json` this merge reasons about. */
41
+ export interface ClaudeSettings {
42
+ $schema?: string;
43
+ hooks?: Record<string, HookEntry[]>;
44
+ permissions?: ClaudePermissions;
45
+ [key: string]: unknown;
46
+ }
47
+ /**
48
+ * Merge Halfcycle's generated hook entries into an existing settings object.
49
+ *
50
+ * @param existing the developer's parsed `.claude/settings.json` (may be `{}`).
51
+ * @param generated Halfcycle's fresh settings object (from `generateSettingsJson`).
52
+ * @returns a new settings object: every developer key preserved, `$schema` taken
53
+ * from `generated`, and for each event Halfcycle's entries upserted
54
+ * beside the developer's — exactly one Halfcycle entry per event.
55
+ */
56
+ export declare function mergeSettings(existing: ClaudeSettings, generated: ClaudeSettings): ClaudeSettings;
57
+ //# sourceMappingURL=merge-settings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge-settings.d.ts","sourceRoot":"","sources":["../src/merge-settings.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,yEAAyE;AACzE,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,6DAA6D;AAC7D,MAAM,WAAW,SAAS;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,WAAW,EAAE,CAAC;CACtB;AAED;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC;IACf,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,sEAAsE;AACtE,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC;IACpC,WAAW,CAAC,EAAE,iBAAiB,CAAC;IAChC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAYD;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,cAAc,EAAE,SAAS,EAAE,cAAc,GAAG,cAAc,CAwCjG"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Mint the board's first-visit code — the installer's second HTTP call (T-19,
3
+ * #363; `client-mission-control` criterion 4, step 2).
4
+ *
5
+ * `POST {baseUrl}/board/enter-codes` with the engagement's own credential as a
6
+ * bearer and an empty body: the engagement is resolved from the credential and
7
+ * never read from the body. The control plane answers
8
+ * `{ enterCode, engagementId, boardUrl, expiresAt }`. The installer prints the
9
+ * address and the code; the developer pastes the code once, in a browser.
10
+ *
11
+ * The installer is the ONE minting site this phase builds. A standing re-mint
12
+ * command belongs to the installer spec's second-run work; until it exists, an
13
+ * expired or spent code is recovered by re-running the installer — which the
14
+ * printed lines say rather than leave to be discovered.
15
+ *
16
+ * INV-001: this file imports only the global `fetch` (Node 20). It never imports
17
+ * the control service — the mint is an external HTTP contract, exactly as
18
+ * `create-engagement.ts` treats `POST /engagements`.
19
+ */
20
+ /** What `POST /board/enter-codes` returns on 201. */
21
+ export interface MintedBoardCode {
22
+ /** The single-use plaintext, printed once and never stored by the installer. */
23
+ enterCode: string;
24
+ engagementId: string;
25
+ /** The first-visit address — a plain URL carrying no credential. */
26
+ boardUrl: string;
27
+ /** UTC ISO-8601 absolute expiry. */
28
+ expiresAt: string;
29
+ }
30
+ /**
31
+ * Mint a first-visit board code for the engagement the credential resolves to.
32
+ *
33
+ * @param baseUrl The platform origin (`HALFCYCLE_SERVICE_URL`).
34
+ * @param sessionToken The engagement's own credential, as a bearer.
35
+ * @throws on transport failure, a non-2xx status, or a body missing
36
+ * any of the four fields. The caller decides what a failure
37
+ * means; for the installer it means printing a recoverable
38
+ * note, never failing an install that already succeeded.
39
+ */
40
+ export declare function mintBoardEnterCode(baseUrl: string, sessionToken: string): Promise<MintedBoardCode>;
41
+ //# sourceMappingURL=mint-board-code.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mint-board-code.d.ts","sourceRoot":"","sources":["../src/mint-board-code.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,qDAAqD;AACrD,MAAM,WAAW,eAAe;IAC9B,gFAAgF;IAChF,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,oEAAoE;IACpE,QAAQ,EAAE,MAAM,CAAC;IACjB,oCAAoC;IACpC,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;GASG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,eAAe,CAAC,CAwC1B"}
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Open-phase wiring — the CLI's call to `POST {baseUrl}/engagements/:id/phase`
3
+ * (FX-11, operator decision D-2).
4
+ *
5
+ * `openPhase` has had a real entry gate since FW-7 — it refuses an unmet entry
6
+ * condition, demands an explicit override, and records who crossed what — and no
7
+ * surface at all, so the only way anyone has ever opened a phase is a scratch script
8
+ * that ran inside the control plane. This module is the CLIENT half of the answer:
9
+ * the developer whose repo the phase belongs to can open it from their own repo,
10
+ * through the gate, with their own credential.
11
+ *
12
+ * WIRING, NOT MACHINERY. Every rule lives on the control plane: what makes a decision
13
+ * substantive, what the store can evaluate for itself, what an override crosses. This
14
+ * module builds the request, sends it, and reports what came back — including the
15
+ * refusal, in the plane's own words. It re-checks nothing, because a second copy of
16
+ * the entry rule in a shippable package is a copy that can disagree with the one that
17
+ * decides.
18
+ *
19
+ * INV-001: this file imports the global `fetch` (Node 20), Node builtins, and
20
+ * `@halfcycle/events` — one of the exactly two workspace packages a shippable may
21
+ * import. `phaseEntryDecisionSchema` is the ONE home for the decision's shape
22
+ * (INV-003), so the CLI validates what it builds against the same schema the route
23
+ * parses with rather than a second hand-written copy. It never imports the control
24
+ * service; the route is an external HTTP contract, exactly as `createEngagement` and
25
+ * `mintBoardEnterCode` treat theirs.
26
+ */
27
+ import { type PhaseEntryDecision } from '@halfcycle/events';
28
+ /** What `POST /engagements/:id/phase` returns on 200 (the fields the CLI reports). */
29
+ export interface OpenedPhase {
30
+ /** What is open now. `null` is project scope. */
31
+ phase: string | null;
32
+ /** What was open before. `null` is project scope. */
33
+ previousPhase: string | null;
34
+ decision: {
35
+ decisionId: string;
36
+ decision: string;
37
+ actor: string;
38
+ justification: string;
39
+ /** The finding this decision crossed, or `null` when it crossed none. */
40
+ blockingFinding: string | null;
41
+ decidedAt: string;
42
+ };
43
+ }
44
+ /**
45
+ * A refusal from the control plane, carried as an error the CLI can print whole.
46
+ *
47
+ * The `finding` is the point. A `409` here means the phase the engagement is in was
48
+ * never accepted, and the plane says WHICH phase and WHY — the developer needs that
49
+ * sentence, not a status code. It rides its own field so the CLI never parses prose.
50
+ */
51
+ export declare class OpenPhaseRefused extends Error {
52
+ readonly status: number;
53
+ readonly finding: string | null;
54
+ constructor(status: number, message: string, finding: string | null);
55
+ }
56
+ /**
57
+ * The three credential keys this call needs. They are the installer's own keys —
58
+ * `install.ts` writes them and `.mcp.json`/the guard hook read them — and this list
59
+ * is deliberately a SUBSET of that write set, asserted as one in the tests rather
60
+ * than left to two lists agreeing by eye.
61
+ */
62
+ export declare const OPEN_PHASE_ENV_KEYS: readonly ["HALFCYCLE_SERVICE_URL", "HALFCYCLE_TOKEN", "HALFCYCLE_ENGAGEMENT_ID"];
63
+ export interface RepoCredential {
64
+ serviceUrl: string;
65
+ token: string;
66
+ engagementId: string;
67
+ }
68
+ /**
69
+ * Read the credential for the engagement this repository is installed against.
70
+ *
71
+ * **WHERE IT LOOKS CHANGED AT T-09, AND THE REPO ROOT IS STILL THE ARGUMENT.** The
72
+ * credential used to sit in the tree at `.env.local`. It now lives at machine level
73
+ * (`engagement-credential.ts`), keyed on the engagement — so the repo root is what
74
+ * names the engagement (via the committed `.halfcycle/bundle.json` pin) rather than
75
+ * what holds the secret. The parameter kept its meaning: *the repository whose
76
+ * engagement this is*.
77
+ *
78
+ * READ, NEVER SOURCED — a credential store is not code, and this parses `KEY=value`
79
+ * lines exactly as the generated `mcp-headers.sh` does. The process environment WINS
80
+ * when it carries a key, so an operator can point one command at a different plane
81
+ * without editing a file, and a CI job with no per-user store works by exporting
82
+ * them.
83
+ *
84
+ * Why any file is read at all: nothing loads a credential into a shell. A verb that
85
+ * only read `process.env` would work for whoever had already exported three
86
+ * variables and fail, confusingly, for the developer whose install wrote them thirty
87
+ * seconds earlier.
88
+ *
89
+ * @throws with the missing key NAMED, and the file it was looked for in. "Not
90
+ * configured" is the one message a developer cannot act on.
91
+ */
92
+ export declare function readRepoCredential(repoRoot: string, env?: NodeJS.ProcessEnv, home?: string): RepoCredential;
93
+ /**
94
+ * Open a phase on the engagement (or, with `phase: null`, return it to project
95
+ * scope).
96
+ *
97
+ * The decision is a REQUIRED parameter, mirroring `openPhase`'s own signature and the
98
+ * wire schema's required `entry`: there is no argument-less form here either, at any
99
+ * layer, by construction rather than by review.
100
+ *
101
+ * @throws OpenPhaseRefused on a 4xx the plane explained (an unmet entry condition, a
102
+ * decision that says nothing, a bad credential), so the CLI prints the
103
+ * plane's own sentence and exits non-zero.
104
+ * @throws Error on transport failure or a body that is not the expected shape.
105
+ */
106
+ export declare function openPhase(credential: RepoCredential, phase: string | null, entry: PhaseEntryDecision): Promise<OpenedPhase>;
107
+ //# sourceMappingURL=open-phase.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"open-phase.d.ts","sourceRoot":"","sources":["../src/open-phase.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAIH,OAAO,EAA4B,KAAK,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAOtF,sFAAsF;AACtF,MAAM,WAAW,WAAW;IAC1B,iDAAiD;IACjD,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,qDAAqD;IACrD,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,QAAQ,EAAE;QACR,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,MAAM,CAAC;QACjB,KAAK,EAAE,MAAM,CAAC;QACd,aAAa,EAAE,MAAM,CAAC;QACtB,yEAAyE;QACzE,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;QAC/B,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;CACH;AAED;;;;;;GAMG;AACH,qBAAa,gBAAiB,SAAQ,KAAK;aAEvB,MAAM,EAAE,MAAM;aAEd,OAAO,EAAE,MAAM,GAAG,IAAI;gBAFtB,MAAM,EAAE,MAAM,EAC9B,OAAO,EAAE,MAAM,EACC,OAAO,EAAE,MAAM,GAAG,IAAI;CAKzC;AAED;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,kFAItB,CAAC;AAEX,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,IAAI,CAAC,EAAE,MAAM,GACZ,cAAc,CA0BhB;AAuBD;;;;;;;;;;;;GAYG;AACH,wBAAsB,SAAS,CAC7B,UAAU,EAAE,cAAc,EAC1B,KAAK,EAAE,MAAM,GAAG,IAAI,EACpB,KAAK,EAAE,kBAAkB,GACxB,OAAO,CAAC,WAAW,CAAC,CA0DtB"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Reach the MCP origin the platform reported, so the installer can say WHICH of
3
+ * the two origins is wrong (T-22, #438 — `client-install-surface` criterion 5).
4
+ *
5
+ * **The failure this exists to make legible.** The install writes two addresses:
6
+ * `HALFCYCLE_SERVICE_URL` (the control plane, which the developer configures) and
7
+ * `HALFCYCLE_MCP_URL` (the method-delivery service, which the platform reports).
8
+ * A bad control origin already fails loudly, at the create call. A bad MCP origin
9
+ * failed SILENTLY: the install completed, the tree was correct, `.mcp.json` named
10
+ * an address, and the developer found out when `/halfcycle-setup` did nothing in
11
+ * their editor — a 404 with no clue which half of a two-service platform was
12
+ * wrong. This turns that into one sentence at install time.
13
+ *
14
+ * **It never fails the install.** The tree is already correct and the credential
15
+ * is already written; refusing an install over a service that may simply be
16
+ * restarting would be worse than saying so. Same degrade posture as the board
17
+ * mint in `bin.ts`.
18
+ *
19
+ * **`initialize` is the probe** because it is the one MCP call that needs no
20
+ * credential and identifies the server: a wrong origin 404s, a right one answers
21
+ * with its `serverInfo`. The alternative — a plain `GET /health` — would prove an
22
+ * origin is alive without proving it serves MCP, which is exactly the distinction
23
+ * that was missing.
24
+ *
25
+ * INV-001: Node builtins and the global `fetch` only. No hosted import.
26
+ */
27
+ export type McpProbe =
28
+ /** `POST {origin}/mcp` answered as an MCP server. `serverName` is what it called itself. */
29
+ {
30
+ reached: true;
31
+ serverName: string;
32
+ }
33
+ /** It did not. `problem` is a plain-words sentence naming what happened. */
34
+ | {
35
+ reached: false;
36
+ problem: string;
37
+ };
38
+ /**
39
+ * Ask the MCP origin to identify itself.
40
+ *
41
+ * @param mcpUrl The ORIGIN (no path) the create response reported. The endpoint
42
+ * is composed by `mcpEndpointUrl` — the SAME function that composes
43
+ * the literal url written into `.mcp.json` (T-24), so this probe
44
+ * cannot report an address the registration does not use.
45
+ */
46
+ export declare function probeMcpOrigin(mcpUrl: string): Promise<McpProbe>;
47
+ //# sourceMappingURL=probe-mcp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"probe-mcp.d.ts","sourceRoot":"","sources":["../src/probe-mcp.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAQH,MAAM,MAAM,QAAQ;AAClB,4FAA4F;AAC1F;IAAE,OAAO,EAAE,IAAI,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE;AACvC,4EAA4E;GAC1E;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAExC;;;;;;;GAOG;AACH,wBAAsB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAuCtE"}
@@ -0,0 +1,126 @@
1
+ /**
2
+ * THE CREDENTIAL RESOLVER — environment variable → per-user store → browser
3
+ * handoff (T-08, feature `connect-signup-and-credential`).
4
+ *
5
+ * One home for the question *"what credential does this run present to control, and
6
+ * where did it come from?"*. `bin.ts` asks it once, before anything is written.
7
+ *
8
+ * ---------------------------------------------------------------------------
9
+ * THE ORDER IS AN ACCEPTANCE CRITERION, NOT A CONVENIENCE. *"CI works with no
10
+ * browser and no per-user store: `HALFCYCLE_TOKEN` in the environment is accepted
11
+ * and TAKES PRECEDENCE over the stored credential."* A workflow that exports a
12
+ * credential has said which identity this run is, and a stored credential left
13
+ * behind on a shared or long-lived runner must not silently win over it — the
14
+ * symptom of that inversion is a job that quietly does its work as the wrong
15
+ * account, which nothing about the run's output would reveal.
16
+ *
17
+ * The environment arm also DOES NOT TOUCH THE STORE AT ALL: it neither reads it nor
18
+ * writes it. A CI job must not leave a credential behind on a runner's home
19
+ * directory, and a developer who exports the variable for one command must not find
20
+ * their saved sign-in replaced by it.
21
+ *
22
+ * ---------------------------------------------------------------------------
23
+ * WHY THE SOURCE IS RETURNED AND NOT JUST THE SECRET. When control refuses a
24
+ * credential, the remedy depends entirely on where it came from: a stored one is
25
+ * replaced by signing in again (which this CLI can just do), an environment one
26
+ * cannot be — the variable still takes precedence, so "run it again" would loop
27
+ * forever presenting the same dead value. That is the *remedy naming something the
28
+ * reader cannot act on* defect (`docs/topology-boundary.md` rule 2), and returning
29
+ * the source is what lets the refusal path avoid it.
30
+ *
31
+ * ---------------------------------------------------------------------------
32
+ * WHAT "REFRESH" MEANS HERE, because the client half of it is smaller than it
33
+ * sounds and the difference matters. The 30-day window is refreshed SERVER-SIDE, on
34
+ * every successful use: `resolveAccountCredential` extends `expires_at` in the same
35
+ * statement that validates the credential, and it is the one home of that number. No
36
+ * response tells this client the extended value, so there is nothing to write back
37
+ * and nothing to re-time locally — using the credential IS the refresh. The client
38
+ * half is therefore exactly two things: keep presenting the stored credential, and
39
+ * when control finally refuses one, treat that as *sign in again* rather than as
40
+ * something to retry. Anything more — a locally-tracked expiry, a client-side TTL, a
41
+ * pre-emptive rotate call — would be a second home for a lifetime the operator set
42
+ * in one place, and there is no client-reachable rotate route to call anyway
43
+ * (`rotateToken`'s only non-test caller is an operator script).
44
+ *
45
+ * INV-001: Node builtins, `@halfcycle/events` (transitively, via `device-signin`),
46
+ * and bundle-internal modules.
47
+ */
48
+ import { type SignInDeps } from './device-signin.js';
49
+ /** Where the credential this run presents came from. */
50
+ export type CredentialSource =
51
+ /** `HALFCYCLE_TOKEN` in the process environment — the CI path, and it wins. */
52
+ 'environment'
53
+ /** `~/.halfcycle/account.json` — this machine has signed in before. */
54
+ | 'stored'
55
+ /** The browser handoff just ran, and the result has been stored. */
56
+ | 'browser-handoff';
57
+ export interface ResolvedCredential {
58
+ credential: string;
59
+ source: CredentialSource;
60
+ /**
61
+ * The account the credential speaks for, when it is known. ABSENT for the
62
+ * environment arm — a bare variable carries no identity and this CLI does not go
63
+ * and ask, because the only way to find out is to spend a call, and control will
64
+ * tell us by accepting or refusing it a moment later anyway.
65
+ */
66
+ accountId?: string;
67
+ }
68
+ export interface ResolveOptions extends SignInDeps {
69
+ /** The process environment to read `HALFCYCLE_TOKEN` and `CI` from. */
70
+ env?: NodeJS.ProcessEnv;
71
+ /** The home directory the per-user store lives under. Defaults to `os.homedir()`. */
72
+ home?: string;
73
+ }
74
+ /**
75
+ * Resolve a credential for `serviceUrl`, signing in through the browser if this
76
+ * machine has none.
77
+ *
78
+ * A successful handoff is STORED before this returns, so the next run skips it —
79
+ * that is what makes *"an already-signed-in machine skips all of it"* true. It is
80
+ * stored BEFORE the credential is used for anything, deliberately: a credential
81
+ * obtained and then lost because the create call failed would send the developer
82
+ * back through the browser for a reason that had nothing to do with signing in.
83
+ */
84
+ export declare function obtainCredential(serviceUrl: string, opts?: ResolveOptions): Promise<ResolvedCredential>;
85
+ /**
86
+ * Control refused the credential we presented. Replace it, if replacing it is a
87
+ * thing this CLI is allowed to do.
88
+ *
89
+ * Returns the new credential, or `null` when there is nothing to replace — in which
90
+ * case the caller must print the refusal and exit non-zero rather than retry.
91
+ *
92
+ * THE THREE ARMS, and why only one of them re-signs-in:
93
+ *
94
+ * `environment` → null. The variable takes precedence over anything this
95
+ * function could store, so signing in again would produce a
96
+ * credential the very next resolve would ignore. The operator
97
+ * who set the variable is the only one who can fix it.
98
+ * `stored` → forget it and sign in again. There is no grace window on
99
+ * this credential class and nothing rotates it, so a refusal
100
+ * is terminal for that value: keeping it would mean every
101
+ * later run presenting something that can only fail. This is
102
+ * the 30-day window closing on a machine that sat idle — the
103
+ * *"sends the developer back through the browser once"* half
104
+ * of operator decision 4.
105
+ * `browser-handoff` → null. The credential was minted seconds ago and control has
106
+ * already refused it; signing in again would mint another one
107
+ * from the same route and hand back the same refusal, one
108
+ * browser round-trip later. A loop that ends where it started
109
+ * is worse than a message, so this reports rather than retries.
110
+ */
111
+ export declare function replaceRefusedCredential(serviceUrl: string, refused: ResolvedCredential, opts?: ResolveOptions): Promise<ResolvedCredential | null>;
112
+ /**
113
+ * The sentence for a refusal this CLI cannot recover from, chosen by where the
114
+ * credential came from.
115
+ *
116
+ * ONE HOME for that selection, and it exists because the alternative already shipped
117
+ * once in this repository: a single remedy written for all three situations, printed
118
+ * to whoever hit any of them. `bin.ts` already selects its guard-off remedy this way
119
+ * for the same reason (walk 9 `F-2` — a line that told a developer to set a variable
120
+ * that was already set, on the invocation that printed it).
121
+ *
122
+ * The `message` control sent is carried through in every arm, because control's
123
+ * refusal is the fact and this is the local advice on top of it.
124
+ */
125
+ export declare function refusedCredentialRemedy(source: CredentialSource): string;
126
+ //# sourceMappingURL=resolve-credential.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-credential.d.ts","sourceRoot":"","sources":["../src/resolve-credential.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AAOH,OAAO,EAAU,KAAK,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAE7D,wDAAwD;AACxD,MAAM,MAAM,gBAAgB;AAC1B,+EAA+E;AAC7E,aAAa;AACf,uEAAuE;GACrE,QAAQ;AACV,oEAAoE;GAClE,iBAAiB,CAAC;AAEtB,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,gBAAgB,CAAC;IACzB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,cAAe,SAAQ,UAAU;IAChD,uEAAuE;IACvE,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,qFAAqF;IACrF,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;GASG;AACH,wBAAsB,gBAAgB,CACpC,UAAU,EAAE,MAAM,EAClB,IAAI,GAAE,cAAmB,GACxB,OAAO,CAAC,kBAAkB,CAAC,CA0B7B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAsB,wBAAwB,CAC5C,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,kBAAkB,EAC3B,IAAI,GAAE,cAAmB,GACxB,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAmBpC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,CAqBxE"}
package/dist/scan.d.ts ADDED
@@ -0,0 +1,131 @@
1
+ /**
2
+ * The bootstrap scan — INV-021's one permitted repository read.
3
+ *
4
+ * Writes `.halfcycle/state.json`, a client-side, gitignored cache of DERIVED
5
+ * position. It is not a state record and it is not a record store: the
6
+ * authoritative position is computed entirely hosted-side, in-process, from
7
+ * the one satisfaction oracle over the state-record table. This module holds
8
+ * no database client, no store handle and no import path to any hosted
9
+ * package, so it cannot reach that oracle — see the import-boundary test,
10
+ * `packages/bundle/test/scan-import-boundary.test.ts`.
11
+ *
12
+ * What it may write, and the whole of what it may write (INV-021):
13
+ * - a **second-class** value, `'asserted'`, set per LAYER (never per step) when
14
+ * the repository already holds that layer's artefact(s) on disk;
15
+ * - never `'green'`; never anything that satisfies a `requires` edge or counts
16
+ * toward a gate — those are hosted-side concepts this file never touches;
17
+ * - carrying its own provenance (`source: 'bootstrap-scan'`, the paths found,
18
+ * and when the scan ran) so a reader can never mistake it for a record —
19
+ * PLUS a file-level `note` saying the same of the file as a whole, because
20
+ * an EMPTY `layers` has no entry to carry provenance, and emptiness is
21
+ * exactly the case that misled a cold session (W3-F-1, walk 4);
22
+ * - superseded by the first REAL record for that layer — this module has no
23
+ * opinion on that; a caller merges/overlays hosted records over this file's
24
+ * `asserted` rows and MUST prefer the real record. See `mergeWithRecords`.
25
+ *
26
+ * Runs ONCE, during onboarding, and never again (INV-021's reworded clause —
27
+ * decision (bb) names no command). `runBootstrapScan` is idempotent-refusing:
28
+ * it does nothing if `.halfcycle/state.json` already exists, so re-running the
29
+ * installer or resolving `/halfcycle-setup` a second time cannot re-scan.
30
+ *
31
+ * INV-001: this file imports only @halfcycle/events and Node builtins.
32
+ */
33
+ import { methodLayerSchema, type MethodLayer } from '@halfcycle/events';
34
+ /** The state file's format tag — bumped only on a breaking shape change. */
35
+ export declare const HALFCYCLE_STATE_FORMAT: "halfcycle-state-file/v1";
36
+ /** Re-exported so a consumer can validate a layer key without a second import. */
37
+ export { methodLayerSchema };
38
+ /**
39
+ * The only non-absent value this module ever writes — there is no
40
+ * `'green'`/`'complete'` member because INV-021 forbids this file from ever
41
+ * claiming one. A layer with no matching artefact on disk simply has no
42
+ * entry (absence, not a `'not-instrumented'` literal — that rendering is the
43
+ * hosted board's, per `state-contract.md` Non-goals, and this file does not
44
+ * reproduce it).
45
+ */
46
+ export type ScannedLayerValue = 'asserted';
47
+ /** One layer's scanned entry, with its own provenance. */
48
+ export interface ScannedLayerEntry {
49
+ value: ScannedLayerValue;
50
+ /** Always 'bootstrap-scan' — the one emitter this file ever has. */
51
+ source: 'bootstrap-scan';
52
+ /** Repo-relative paths whose presence caused this layer to be asserted. */
53
+ foundPaths: string[];
54
+ /** When the (one, onboarding-time) scan ran. */
55
+ scannedAt: string;
56
+ }
57
+ /**
58
+ * What the file says about itself, in the file, for a reader who has only the
59
+ * file (W3-F-1). Every per-layer entry already carries provenance so it "can
60
+ * never be mistaken for a record" — but an EMPTY `layers` has no entry to carry
61
+ * it, and emptiness is the state a greenfield install writes and then keeps for
62
+ * the life of the engagement. Walk 4's cold session read `{"layers":{}}`,
63
+ * concluded "literally nothing recorded", and told the developer to redo an
64
+ * approval gate they had already passed. The provenance rule was sound; it just
65
+ * had nowhere to live in the one case that misleads.
66
+ *
67
+ * This is deliberately prose rather than a flag: the reader that gets this
68
+ * wrong is a language model reading JSON it did not expect, and a `false` on a
69
+ * key it has never seen carries no instruction. Kept to one sentence per claim.
70
+ */
71
+ export declare const HALFCYCLE_STATE_NOTE: string;
72
+ /** The whole of `.halfcycle/state.json`. */
73
+ export interface HalfcycleStateFile {
74
+ format: typeof HALFCYCLE_STATE_FORMAT;
75
+ /**
76
+ * Self-description, always written, always the same string. Present so that
77
+ * an empty `layers` still tells a reader what it is and where to ask instead.
78
+ */
79
+ note: typeof HALFCYCLE_STATE_NOTE;
80
+ /** Per-layer scan results. Only layers with a hit are present. */
81
+ layers: Partial<Record<MethodLayer, ScannedLayerEntry>>;
82
+ }
83
+ /**
84
+ * The repo-relative artefact paths that, if present, assert a layer. Kept
85
+ * narrow and file-existence-only — this is a presence check, not a content
86
+ * judgement, and it never reads a record store (there is none to read from a
87
+ * client machine; see the module docblock).
88
+ *
89
+ * Layers with no local artefact convention (`l3-l4`, `xl`) are deliberately
90
+ * absent — the scan has nothing on disk to look for and asserts nothing.
91
+ *
92
+ * **Exported so there is ONE home for it.** The discover phase
93
+ * (`src/setup/discovery.ts`) needs the same "which document belongs to which
94
+ * layer" table to report which layer documents already exist, and a second copy
95
+ * there would drift the moment either moved. It consumes the CONSTANT and never
96
+ * `scanLayers`, so no second position value is constructed: discovery reports a
97
+ * file's existence with its path as the anchor, and derives no `asserted`
98
+ * (INV-021 — position is computed from records, and the one permitted scan is
99
+ * this module's).
100
+ */
101
+ export declare const LAYER_ARTEFACT_PATHS: Partial<Record<MethodLayer, string[]>>;
102
+ /** Scan `targetRepo` for each layer's artefact convention. Pure filesystem read. */
103
+ export declare function scanLayers(targetRepo: string, scannedAt?: string): HalfcycleStateFile;
104
+ /**
105
+ * Run the bootstrap scan ONCE. If `.halfcycle/state.json` already exists this
106
+ * is a no-op (returns the existing file unchanged) — the "never again" half of
107
+ * INV-021's onboarding clause, enforced here rather than left to caller
108
+ * discipline. Returns the file that is now on disk either way.
109
+ */
110
+ export declare function runBootstrapScan(targetRepo: string): {
111
+ state: HalfcycleStateFile;
112
+ ran: boolean;
113
+ };
114
+ /**
115
+ * What a layer reads as, once real records are known. This is the reader's
116
+ * side of the "superseded by the first real record" rule — it takes the local
117
+ * scan file and a set of layers the HOSTED oracle already holds a satisfying
118
+ * record for (computed elsewhere, over HTTP, never read from disk here), and
119
+ * returns a merged view where any layer with a real record reads `'complete'`
120
+ * regardless of what the scan asserted, and every other layer reads the scan's
121
+ * `'asserted'` or is absent.
122
+ *
123
+ * This function takes the real-record set as a plain `Set<MethodLayer>`
124
+ * parameter — never a store handle, a pool, a URL, or anything else that could
125
+ * let a caller wire this file to a live query. That is the structural half of
126
+ * criterion 3: nothing in `packages/bundle` can reach the hosted oracle, so the
127
+ * only way "real records" enter this function is a value the CALLER already
128
+ * computed and handed in.
129
+ */
130
+ export declare function mergeWithRecords(state: HalfcycleStateFile, layersWithRealRecords: ReadonlySet<MethodLayer>): Partial<Record<MethodLayer, 'asserted' | 'complete'>>;
131
+ //# sourceMappingURL=scan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../src/scan.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAIH,OAAO,EAAE,iBAAiB,EAAE,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAMxE,4EAA4E;AAC5E,eAAO,MAAM,sBAAsB,EAAG,yBAAkC,CAAC;AAEzE,kFAAkF;AAClF,OAAO,EAAE,iBAAiB,EAAE,CAAC;AAE7B;;;;;;;GAOG;AACH,MAAM,MAAM,iBAAiB,GAAG,UAAU,CAAC;AAE3C,0DAA0D;AAC1D,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,iBAAiB,CAAC;IACzB,oEAAoE;IACpE,MAAM,EAAE,gBAAgB,CAAC;IACzB,2EAA2E;IAC3E,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,gDAAgD;IAChD,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,oBAAoB,QAStB,CAAC;AAEZ,4CAA4C;AAC5C,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,OAAO,sBAAsB,CAAC;IACtC;;;OAGG;IACH,IAAI,EAAE,OAAO,oBAAoB,CAAC;IAClC,kEAAkE;IAClE,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,iBAAiB,CAAC,CAAC,CAAC;CACzD;AAMD;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,oBAAoB,EAAE,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,CAAC,CAOvE,CAAC;AAMF,oFAAoF;AACpF,wBAAgB,UAAU,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,GAAE,MAAiC,GAAG,kBAAkB,CAc/G;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,kBAAkB,CAAC;IAAC,GAAG,EAAE,OAAO,CAAA;CAAE,CAUhG;AAMD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,kBAAkB,EACzB,qBAAqB,EAAE,WAAW,CAAC,WAAW,CAAC,GAC9C,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,UAAU,GAAG,UAAU,CAAC,CAAC,CAcvD"}
@@ -0,0 +1,73 @@
1
+ /**
2
+ * The discover phase — everything discoverable is discovered before anything is
3
+ * asked, and every discovered value carries an anchor (feature `halfcycle-setup`,
4
+ * criterion 1; task T-17).
5
+ *
6
+ * **The rule this module exists to make unbreakable.** A discovered value with no
7
+ * `file:line` or command output is NOT discovered — it is an assumption, and it
8
+ * moves into the confirm phase as a decision rather than being presented as a
9
+ * finding. That is what stops the discover phase becoming a plausible guess the
10
+ * developer is invited to rubber-stamp. It is enforced by construction here: a
11
+ * finding cannot be built without an anchor, and the one function that takes a
12
+ * possibly-anchorless value returns a QUESTION when the anchor is missing.
13
+ *
14
+ * **What it reads and what it does not.** Plain filesystem reads of the project's
15
+ * own files. It computes no position, writes nothing, and constructs no `asserted`
16
+ * value — the one permitted position-setting read of the repository is the
17
+ * bootstrap scan's (`src/scan.ts`, INV-021), and this module borrows only its
18
+ * layer→document table so there is one home for it.
19
+ *
20
+ * INV-001: Node builtins and `./scan.js` (same package) only.
21
+ */
22
+ /** Where a discovered value came from. There is no third form: if neither a file
23
+ * line nor a command's output can be pointed at, the value is not a finding. */
24
+ export type FactAnchor = {
25
+ readonly kind: 'file';
26
+ readonly path: string;
27
+ readonly line: number;
28
+ } | {
29
+ readonly kind: 'command';
30
+ readonly command: string;
31
+ readonly output: string;
32
+ };
33
+ /** One thing the repository answered, and where it said so. */
34
+ export interface DiscoveredFact {
35
+ /** A stable key so the confirm phase can tell which questions are already answered. */
36
+ readonly key: string;
37
+ /** What the developer is told, in their language. */
38
+ readonly value: string;
39
+ readonly anchor: FactAnchor;
40
+ }
41
+ /** One thing the repository could not answer, so it is asked. */
42
+ export interface OpenQuestion {
43
+ readonly key: string;
44
+ /** The question, in plain words. */
45
+ readonly question: string;
46
+ /** Why it is being asked rather than reported — always the same shape: nothing
47
+ * in the repository anchors an answer. */
48
+ readonly why: string;
49
+ }
50
+ export interface DiscoveryResult {
51
+ readonly findings: readonly DiscoveredFact[];
52
+ readonly questions: readonly OpenQuestion[];
53
+ }
54
+ /** Render one anchor the way the discover phase prints it: every line carries the
55
+ * file or command it came from. */
56
+ export declare function renderAnchor(anchor: FactAnchor): string;
57
+ /**
58
+ * Read a project repository and report what it answers, each line carrying the file
59
+ * it came from — and what it does not answer, as questions for the confirm phase.
60
+ *
61
+ * Nothing is asked here and nothing is written. The caller puts the questions.
62
+ */
63
+ export declare function discoverRepository(targetRepo: string): DiscoveryResult;
64
+ /**
65
+ * The question set the confirm phase actually puts: every question discovery could
66
+ * not settle. A key that appears as a finding never appears here — that is the
67
+ * "omits every fact those files answer" property, stated as one function so a
68
+ * caller cannot re-derive it differently.
69
+ */
70
+ export declare function questionsToPut(result: DiscoveryResult): readonly OpenQuestion[];
71
+ /** The discover phase's report: one line per finding, each carrying its anchor. */
72
+ export declare function renderDiscovery(result: DiscoveryResult): string;
73
+ //# sourceMappingURL=discovery.d.ts.map