@quickchatai/quickreview 1.1.0 → 1.2.0

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 (225) hide show
  1. package/README.md +35 -19
  2. package/config.example.toml +41 -9
  3. package/dist/extensions/cli/login-route.js +5 -0
  4. package/dist/extensions/cli/login-route.js.map +1 -0
  5. package/dist/extensions/login-route.js +180 -0
  6. package/dist/extensions/login-route.js.map +1 -0
  7. package/dist/extensions/onboarding.js +29 -6
  8. package/dist/extensions/onboarding.js.map +1 -1
  9. package/dist/src/ciretry.js +2 -1
  10. package/dist/src/ciretry.js.map +1 -1
  11. package/dist/src/cli.js +43 -12
  12. package/dist/src/cli.js.map +1 -1
  13. package/dist/src/cliops.js +3 -2
  14. package/dist/src/cliops.js.map +1 -1
  15. package/dist/src/commands.js +25 -2
  16. package/dist/src/commands.js.map +1 -1
  17. package/dist/src/config.js +16 -2
  18. package/dist/src/config.js.map +1 -1
  19. package/dist/src/daemon.js +24 -8
  20. package/dist/src/daemon.js.map +1 -1
  21. package/dist/src/debugroundpi.js +5 -1
  22. package/dist/src/debugroundpi.js.map +1 -1
  23. package/dist/src/dispatch.js +5 -4
  24. package/dist/src/dispatch.js.map +1 -1
  25. package/dist/src/engine/debugpi.js +17 -5
  26. package/dist/src/engine/debugpi.js.map +1 -1
  27. package/dist/src/execboundary.js +10 -3
  28. package/dist/src/execboundary.js.map +1 -1
  29. package/dist/src/github.js +51 -16
  30. package/dist/src/github.js.map +1 -1
  31. package/dist/src/githubtoken.js +346 -0
  32. package/dist/src/githubtoken.js.map +1 -0
  33. package/dist/src/loginhint.js +8 -3
  34. package/dist/src/loginhint.js.map +1 -1
  35. package/dist/src/loginrefresh/pirefresh.js +3 -0
  36. package/dist/src/loginrefresh/pirefresh.js.map +1 -1
  37. package/dist/src/npmverbs.js +4 -2
  38. package/dist/src/npmverbs.js.map +1 -1
  39. package/dist/src/onboard/choices.js +29 -1
  40. package/dist/src/onboard/choices.js.map +1 -1
  41. package/dist/src/onboard/engine.js +8 -23
  42. package/dist/src/onboard/engine.js.map +1 -1
  43. package/dist/src/onboard/launch.js +12 -20
  44. package/dist/src/onboard/launch.js.map +1 -1
  45. package/dist/src/onboard/pipin.js +22 -0
  46. package/dist/src/onboard/pipin.js.map +1 -0
  47. package/dist/src/onboard/stepforward.js +41 -0
  48. package/dist/src/onboard/stepforward.js.map +1 -0
  49. package/dist/src/onboard/steps/appslug.js +4 -1
  50. package/dist/src/onboard/steps/appslug.js.map +1 -1
  51. package/dist/src/onboard/steps/compose.js +29 -0
  52. package/dist/src/onboard/steps/compose.js.map +1 -1
  53. package/dist/src/onboard/steps/configfill.js +102 -32
  54. package/dist/src/onboard/steps/configfill.js.map +1 -1
  55. package/dist/src/onboard/steps/githubaccount.js +595 -0
  56. package/dist/src/onboard/steps/githubaccount.js.map +1 -0
  57. package/dist/src/onboard/steps/githubapp.js +65 -7
  58. package/dist/src/onboard/steps/githubapp.js.map +1 -1
  59. package/dist/src/onboard/steps/golive.js +2 -0
  60. package/dist/src/onboard/steps/golive.js.map +1 -1
  61. package/dist/src/onboard/steps/index.js +11 -0
  62. package/dist/src/onboard/steps/index.js.map +1 -1
  63. package/dist/src/onboard/steps/ingress.js +80 -25
  64. package/dist/src/onboard/steps/ingress.js.map +1 -1
  65. package/dist/src/onboard/steps/llmauth.js +103 -30
  66. package/dist/src/onboard/steps/llmauth.js.map +1 -1
  67. package/dist/src/onboard/steps/memory.js +53 -1
  68. package/dist/src/onboard/steps/memory.js.map +1 -1
  69. package/dist/src/onboard/steps/proof.js +366 -56
  70. package/dist/src/onboard/steps/proof.js.map +1 -1
  71. package/dist/src/onboard/steps/topology.js +6 -6
  72. package/dist/src/onboard/steps/topology.js.map +1 -1
  73. package/dist/src/onboard/steps/transport.js +9 -1
  74. package/dist/src/onboard/steps/transport.js.map +1 -1
  75. package/dist/src/onboard/steps/tuiroute.js +102 -0
  76. package/dist/src/onboard/steps/tuiroute.js.map +1 -0
  77. package/dist/src/onboard/steps/verifyup.js +17 -1
  78. package/dist/src/onboard/steps/verifyup.js.map +1 -1
  79. package/dist/src/onboard/toolnames.js +30 -0
  80. package/dist/src/onboard/toolnames.js.map +1 -0
  81. package/dist/src/operatorwrite.js +23 -0
  82. package/dist/src/operatorwrite.js.map +1 -0
  83. package/dist/src/outcomes.js +4 -1
  84. package/dist/src/outcomes.js.map +1 -1
  85. package/dist/src/posting-journal.js +2 -1
  86. package/dist/src/posting-journal.js.map +1 -1
  87. package/dist/src/posting-outbox.js +6 -2
  88. package/dist/src/posting-outbox.js.map +1 -1
  89. package/dist/src/posting-receipt.js +14 -1
  90. package/dist/src/posting-receipt.js.map +1 -1
  91. package/dist/src/recheck.js +2 -1
  92. package/dist/src/recheck.js.map +1 -1
  93. package/dist/src/reconcile.js +22 -2
  94. package/dist/src/reconcile.js.map +1 -1
  95. package/dist/src/reviewhistory.js +32 -6
  96. package/dist/src/reviewhistory.js.map +1 -1
  97. package/dist/src/reviewnow.js +45 -0
  98. package/dist/src/reviewnow.js.map +1 -0
  99. package/dist/src/state.js.map +1 -1
  100. package/dist/src/statemachine.js +4 -1
  101. package/dist/src/statemachine.js.map +1 -1
  102. package/dist/src/takeoverpi.js +13 -0
  103. package/dist/src/takeoverpi.js.map +1 -1
  104. package/dist/src/tui/app.js +49 -0
  105. package/dist/src/tui/app.js.map +1 -1
  106. package/dist/src/tui/board.js +88 -12
  107. package/dist/src/tui/board.js.map +1 -1
  108. package/dist/src/tui/cmdbar.js +4 -1
  109. package/dist/src/tui/cmdbar.js.map +1 -1
  110. package/dist/src/tui/companion.js +74 -10
  111. package/dist/src/tui/companion.js.map +1 -1
  112. package/dist/src/tui/connect.js +7 -1
  113. package/dist/src/tui/connect.js.map +1 -1
  114. package/dist/src/tui/filters.js +5 -2
  115. package/dist/src/tui/filters.js.map +1 -1
  116. package/dist/src/tui/ghread.js +61 -5
  117. package/dist/src/tui/ghread.js.map +1 -1
  118. package/dist/src/tui/index.js +106 -20
  119. package/dist/src/tui/index.js.map +1 -1
  120. package/dist/src/tui/memforward.js +39 -2
  121. package/dist/src/tui/memforward.js.map +1 -1
  122. package/dist/src/tui/model.js +25 -0
  123. package/dist/src/tui/model.js.map +1 -1
  124. package/dist/src/tui/prpause.js +7 -0
  125. package/dist/src/tui/prpause.js.map +1 -1
  126. package/dist/src/tui/remote.js +7 -1
  127. package/dist/src/tui/remote.js.map +1 -1
  128. package/dist/src/tui/route.js +143 -0
  129. package/dist/src/tui/route.js.map +1 -0
  130. package/dist/src/tui/setupstate.js +29 -0
  131. package/dist/src/tui/setupstate.js.map +1 -0
  132. package/dist/src/tui/sshtriage.js +102 -5
  133. package/dist/src/tui/sshtriage.js.map +1 -1
  134. package/dist/src/tui/theme.js +2 -1
  135. package/dist/src/tui/theme.js.map +1 -1
  136. package/dist/src/tui/unwedge.js +2 -12
  137. package/dist/src/tui/unwedge.js.map +1 -1
  138. package/dist/src/verdicts.js +17 -5
  139. package/dist/src/verdicts.js.map +1 -1
  140. package/dist/src/version.js +1 -1
  141. package/dist/src/version.js.map +1 -1
  142. package/dist/src/webhook.js +12 -1
  143. package/dist/src/webhook.js.map +1 -1
  144. package/dist/src/wire.js +28 -6
  145. package/dist/src/wire.js.map +1 -1
  146. package/dist/src/workerworld.js +7 -1
  147. package/dist/src/workerworld.js.map +1 -1
  148. package/docker/companion/entrypoint.sh +20 -1
  149. package/docker/entrypoint.sh +17 -0
  150. package/extensions/cli/login-route.ts +10 -0
  151. package/extensions/login-route.ts +220 -0
  152. package/extensions/onboarding.ts +35 -6
  153. package/package.json +2 -2
  154. package/prompts/onboarding.md +57 -18
  155. package/scripts/mint-token.sh +25 -4
  156. package/src/ciretry.ts +2 -1
  157. package/src/cli.ts +49 -14
  158. package/src/cliops.ts +3 -2
  159. package/src/commands.ts +28 -2
  160. package/src/config.ts +24 -4
  161. package/src/daemon.ts +32 -7
  162. package/src/debugroundpi.ts +11 -1
  163. package/src/dispatch.ts +5 -4
  164. package/src/engine/debugpi.ts +17 -5
  165. package/src/execboundary.ts +16 -4
  166. package/src/github.ts +57 -15
  167. package/src/githubtoken.ts +435 -0
  168. package/src/loginhint.ts +8 -3
  169. package/src/loginrefresh/pirefresh.ts +3 -0
  170. package/src/npmverbs.ts +4 -2
  171. package/src/onboard/choices.ts +34 -1
  172. package/src/onboard/engine.ts +22 -3
  173. package/src/onboard/launch.ts +17 -29
  174. package/src/onboard/pipin.ts +31 -0
  175. package/src/onboard/stepforward.ts +61 -0
  176. package/src/onboard/steps/appslug.ts +4 -1
  177. package/src/onboard/steps/compose.ts +31 -0
  178. package/src/onboard/steps/configfill.ts +121 -33
  179. package/src/onboard/steps/githubaccount.ts +716 -0
  180. package/src/onboard/steps/githubapp.ts +92 -8
  181. package/src/onboard/steps/golive.ts +2 -0
  182. package/src/onboard/steps/index.ts +11 -0
  183. package/src/onboard/steps/ingress.ts +98 -31
  184. package/src/onboard/steps/llmauth.ts +114 -29
  185. package/src/onboard/steps/memory.ts +58 -1
  186. package/src/onboard/steps/proof.ts +473 -55
  187. package/src/onboard/steps/topology.ts +7 -6
  188. package/src/onboard/steps/transport.ts +15 -0
  189. package/src/onboard/steps/tuiroute.ts +119 -0
  190. package/src/onboard/steps/verifyup.ts +20 -1
  191. package/src/onboard/toolnames.ts +34 -0
  192. package/src/operatorwrite.ts +25 -0
  193. package/src/outcomes.ts +8 -1
  194. package/src/posting-journal.ts +6 -1
  195. package/src/posting-outbox.ts +10 -2
  196. package/src/posting-receipt.ts +17 -0
  197. package/src/recheck.ts +2 -1
  198. package/src/reconcile.ts +29 -3
  199. package/src/reviewhistory.ts +20 -6
  200. package/src/reviewnow.ts +76 -0
  201. package/src/state.ts +18 -0
  202. package/src/statemachine.ts +4 -1
  203. package/src/takeoverpi.ts +13 -0
  204. package/src/tui/app.ts +47 -0
  205. package/src/tui/board.ts +111 -14
  206. package/src/tui/cmdbar.ts +4 -1
  207. package/src/tui/companion.ts +78 -10
  208. package/src/tui/connect.ts +7 -1
  209. package/src/tui/filters.ts +7 -3
  210. package/src/tui/ghread.ts +74 -7
  211. package/src/tui/index.ts +114 -23
  212. package/src/tui/memforward.ts +52 -3
  213. package/src/tui/model.ts +36 -2
  214. package/src/tui/prpause.ts +11 -2
  215. package/src/tui/remote.ts +9 -1
  216. package/src/tui/route.ts +173 -0
  217. package/src/tui/setupstate.ts +52 -0
  218. package/src/tui/sshtriage.ts +127 -6
  219. package/src/tui/theme.ts +2 -1
  220. package/src/tui/unwedge.ts +2 -14
  221. package/src/verdicts.ts +23 -5
  222. package/src/version.ts +1 -1
  223. package/src/webhook.ts +10 -1
  224. package/src/wire.ts +36 -7
  225. package/src/workerworld.ts +7 -1
@@ -41,6 +41,12 @@ export type IngressChoice = (typeof INGRESS_CHOICES)[number];
41
41
  export const AUTH_ROUTES = ["oauth", "api"] as const;
42
42
  export type AuthRoute = (typeof AUTH_ROUTES)[number];
43
43
 
44
+ /** How the daemon authenticates to GitHub (`config.toml [github] auth`):
45
+ * "user" is the person's own account (polling only, no App), "app" a GitHub
46
+ * App. Answered as the github_account step's input; absent ⇒ unanswered. */
47
+ export const GITHUB_AUTH_CHOICES = ["user", "app"] as const;
48
+ export type GithubAuth = (typeof GITHUB_AUTH_CHOICES)[number];
49
+
44
50
  /** "owner/name" — the reviewed repo as the interview records it and the App
45
51
  * step parses it: no URL, no trailing `.git`. */
46
52
  export const REPO_SLUG_RE = /^([A-Za-z0-9._-]+)\/([A-Za-z0-9._-]+)$/;
@@ -71,6 +77,20 @@ export const CHOICE_KEYS = {
71
77
  /** `AuthRoute`. Written by the LLM step; read by the interview's shape line. */
72
78
  authRoute: "llm_auth_route",
73
79
 
80
+ /** `GithubAuth`. Written by the github_account step (the operator's answer,
81
+ * defaulted to "user" where `gh auth token` works); read by everything that
82
+ * differs between the own-account route and the App route — which of
83
+ * github_account / github_app / app_slug_reconcile is enabled, the
84
+ * interview's App questions, the ingress shapes on offer, verification. */
85
+ githubAuth: "github_auth",
86
+ /** The person's GitHub login, from `GET /user` with their own token (own-
87
+ * account route). The interview's owner-login answer and `[github]
88
+ * bot_login` both come from it. */
89
+ accountLogin: "github_account_login",
90
+ /** "owner/name" the person picked from the repos their account reaches
91
+ * (own-account route). The interview reads it as its repo answer. */
92
+ accountRepo: "github_account_repo",
93
+
74
94
  /** "owner/name" of the reviewed repo. Written by the interview; read by the
75
95
  * GitHub App step and the proof round. */
76
96
  repo: "reviewed_repo",
@@ -144,6 +164,9 @@ export type ChoiceKey = (typeof CHOICE_KEYS)[ChoiceName];
144
164
  */
145
165
  export const CHOICE_WRITERS: Record<ChoiceName, readonly string[]> = {
146
166
  uid: ["preflight"],
167
+ githubAuth: ["github_account"],
168
+ accountLogin: ["github_account"],
169
+ accountRepo: ["github_account"],
147
170
  topology: ["topology"],
148
171
  sshAlias: ["topology"],
149
172
  sshUser: ["ssh_preflight"],
@@ -158,7 +181,7 @@ export const CHOICE_WRITERS: Record<ChoiceName, readonly string[]> = {
158
181
  dryRun: ["config_fill"],
159
182
  labels: ["config_fill"],
160
183
  appSlug: ["config_fill", "github_app", "app_slug_reconcile"],
161
- botLogin: ["github_app", "app_slug_reconcile"],
184
+ botLogin: ["github_account", "github_app", "app_slug_reconcile"],
162
185
  appId: ["github_app"],
163
186
  appTarget: ["github_app"],
164
187
  appName: ["github_app"],
@@ -216,6 +239,16 @@ export function authRouteOf(choices: Readonly<Record<string, unknown>>): AuthRou
216
239
  return memberOf(AUTH_ROUTES, raw(choices, CHOICE_KEYS.authRoute));
217
240
  }
218
241
 
242
+ /** The recorded GitHub auth route, or null while unanswered. */
243
+ export function githubAuthOf(choices: Readonly<Record<string, unknown>>): GithubAuth | null {
244
+ return memberOf(GITHUB_AUTH_CHOICES, raw(choices, CHOICE_KEYS.githubAuth));
245
+ }
246
+
247
+ /** Is the own-account route recorded? (Unanswered is NOT user mode.) */
248
+ export function userAuthRecorded(choices: Readonly<Record<string, unknown>>): boolean {
249
+ return githubAuthOf(choices) === "user";
250
+ }
251
+
219
252
  /** The recorded ingress shape, or null when the ingress step has not run. */
220
253
  export function ingressChoiceOf(choices: Readonly<Record<string, unknown>>): IngressChoice | null {
221
254
  return memberOf(INGRESS_CHOICES, raw(choices, CHOICE_KEYS.ingress));
@@ -19,6 +19,7 @@
19
19
  // The finish path writes the first-run marker state/companion/first-run.json
20
20
  // ({"completed_at": "<iso>"}) exactly once — the contract the companion's
21
21
  // first-run greeting consumes. Keep the path and shape stable.
22
+ import type { StepForward, StepForwardRequest } from "./stepforward.js";
22
23
  import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
23
24
  import { basename, dirname, join, resolve } from "node:path";
24
25
 
@@ -60,6 +61,11 @@ export interface OnboardAdapters {
60
61
  * so no step builds a config path itself. */
61
62
  paths: { stateDir: string; configPath: string };
62
63
  log?: (line: string) => void;
64
+ /** Opens the local memory forward for one step call (src/onboard/stepforward.ts):
65
+ * the compose route's memory engine is reachable from this machine only
66
+ * through it. The real assembly sets it; absent (every test), no forward is
67
+ * opened and the step sees whatever already listens. */
68
+ memoryForward?: (req: StepForwardRequest) => Promise<StepForward | null>;
63
69
  }
64
70
 
65
71
  // ---- step vocabulary --------------------------------------------------------
@@ -116,6 +122,10 @@ export interface OnboardingStep {
116
122
  * input, which is the common case. */
117
123
  parameters?: unknown;
118
124
  enabled?(choices: Readonly<Record<string, unknown>>): boolean;
125
+ /** After setup: the step still runs and verifies like any other, but the
126
+ * checklist completes without it — finish never waits on it. (The first
127
+ * review and the go-live flip: the board offers them once setup is done.) */
128
+ optional?: boolean;
119
129
  run(ctx: StepContext): Promise<StepReport>;
120
130
  verify(ctx: StepContext): Promise<StepReport>;
121
131
  }
@@ -242,6 +252,8 @@ export interface StepStatusLine {
242
252
  explain: string;
243
253
  status: "pending" | "done" | "disabled";
244
254
  note: string | null;
255
+ /** The step's `optional` flag: setup completes without it. */
256
+ optional?: boolean;
245
257
  }
246
258
 
247
259
  export interface ReconcileReport {
@@ -260,7 +272,8 @@ export interface StepRunResult {
260
272
 
261
273
  export interface VerifyAllResult {
262
274
  ok: boolean;
263
- results: Array<{ id: string; ok: boolean; failures: string[] }>;
275
+ /** `optional` results are reported but never hold `ok` false. */
276
+ results: Array<{ id: string; ok: boolean; failures: string[]; optional?: boolean }>;
264
277
  }
265
278
 
266
279
  export type FinishResult =
@@ -364,6 +377,7 @@ export class OnboardingEngine {
364
377
  explain: step.explain,
365
378
  status: this.enabled(step) ? (rec?.status ?? "pending") : "disabled",
366
379
  note: rec?.note ?? null,
380
+ ...(step.optional ? { optional: true } : {}),
367
381
  };
368
382
  });
369
383
  }
@@ -483,10 +497,15 @@ export class OnboardingEngine {
483
497
  rec.note = "verified green without a recorded run";
484
498
  }
485
499
  }
486
- results.push({ id: step.id, ok: outcome.ok, failures: failures(outcome.checks) });
500
+ results.push({
501
+ id: step.id,
502
+ ok: outcome.ok,
503
+ failures: failures(outcome.checks),
504
+ ...(step.optional ? { optional: true } : {}),
505
+ });
487
506
  }
488
507
  this.save();
489
- return { ok: results.every((r) => r.ok), results };
508
+ return { ok: results.every((r) => r.ok || r.optional === true), results };
490
509
  }
491
510
 
492
511
  /**
@@ -7,35 +7,14 @@
7
7
  // Everything effectful is injected (spawn, completion read, log), so tests
8
8
  // drive the whole flow with fakes; src/cli.ts cmdOnboard is the one real
9
9
  // assembly.
10
- import { readFileSync } from "node:fs";
11
10
  import { join } from "node:path";
11
+ import { defaultSteps } from "./steps/index.js";
12
+ import { onboardingToolAllowlist } from "./toolnames.js";
12
13
 
13
- export interface PiPin {
14
- package: string;
15
- version: string;
16
- }
17
-
18
- /** The pin from package.json's "qcrPi" block — the mirror that exists on a bare
19
- * public clone, where config.toml does not (the wizard writes it later).
20
- * config.toml [pi] stays the preferred source when it loads (cmdOnboard tries
21
- * it first); both mirror the same pinned version by rule 7's discipline. */
22
- export function piPinFromPackageJson(
23
- root: string,
24
- readFile: (p: string) => string = (p) => readFileSync(p, "utf8"),
25
- ): PiPin | null {
26
- try {
27
- const pkg = JSON.parse(readFile(join(root, "package.json"))) as {
28
- qcrPi?: { package?: unknown; version?: unknown };
29
- };
30
- const pi = pkg.qcrPi;
31
- if (pi && typeof pi.package === "string" && typeof pi.version === "string") {
32
- return { package: pi.package, version: pi.version };
33
- }
34
- } catch {
35
- // fall through — the caller reports the missing pin
36
- }
37
- return null;
38
- }
14
+ // The pin reader lives in a leaf module: src/version.ts (on the daemon's import
15
+ // graph) needs it, and this module imports the whole step registry.
16
+ export { piPinFromPackageJson, type PiPin } from "./pipin.js";
17
+ import type { PiPin } from "./pipin.js";
39
18
 
40
19
  /** Dedicated onboarding session store — never the reviewer/companion dirs, so
41
20
  * onboarding cannot pollute (or be polluted by) any other pi session store. */
@@ -55,8 +34,17 @@ export function onboardingSessionDir(root: string): string {
55
34
  * the intent (no ambient extensions, exactly one onboarding extension, a
56
35
  * read-only built-in belt) is the design decision; the exact flag spellings
57
36
  * are unverified statically and belong to the named onboarding smoke step.
37
+ *
38
+ * `--tools` is an allowlist over extension tools too (pi 1.4.0; pinned by
39
+ * tests/onboard-tools-sdk.test.ts on the real SDK), so it names every wizard
40
+ * tool, derived from the step registry: a step added there is active here
41
+ * with no second edit. bash, write and edit stay off it.
58
42
  */
59
- export function onboardingPiArgv(pin: PiPin, root: string): string[] {
43
+ export function onboardingPiArgv(
44
+ pin: PiPin,
45
+ root: string,
46
+ stepIds: readonly string[] = defaultSteps().map((s) => s.id),
47
+ ): string[] {
60
48
  return [
61
49
  "npx",
62
50
  "-y",
@@ -70,7 +58,7 @@ export function onboardingPiArgv(pin: PiPin, root: string): string[] {
70
58
  "--system-prompt",
71
59
  join(root, "prompts", "onboarding.md"),
72
60
  "--tools",
73
- "read,ls,grep",
61
+ onboardingToolAllowlist(stepIds).join(","),
74
62
  "--session-dir",
75
63
  onboardingSessionDir(root),
76
64
  ];
@@ -0,0 +1,31 @@
1
+ // The pinned pi version, read from package.json. A leaf: the daemon reaches it
2
+ // through src/version.ts, so it imports nothing from the onboarding wizard.
3
+ import { readFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+
6
+ export interface PiPin {
7
+ package: string;
8
+ version: string;
9
+ }
10
+
11
+ /** The pin from package.json's "qcrPi" block — the mirror that exists on a bare
12
+ * public clone, where config.toml does not (the wizard writes it later).
13
+ * config.toml [pi] stays the preferred source when it loads (cmdOnboard tries
14
+ * it first); both mirror the same pinned version by rule 7's discipline. */
15
+ export function piPinFromPackageJson(
16
+ root: string,
17
+ readFile: (p: string) => string = (p) => readFileSync(p, "utf8"),
18
+ ): PiPin | null {
19
+ try {
20
+ const pkg = JSON.parse(readFile(join(root, "package.json"))) as {
21
+ qcrPi?: { package?: unknown; version?: unknown };
22
+ };
23
+ const pi = pkg.qcrPi;
24
+ if (pi && typeof pi.package === "string" && typeof pi.version === "string") {
25
+ return { package: pi.package, version: pi.version };
26
+ }
27
+ } catch {
28
+ // fall through — the caller reports the missing pin
29
+ }
30
+ return null;
31
+ }
@@ -0,0 +1,61 @@
1
+ // The memory forward, held open for ONE onboarding step call.
2
+ //
3
+ // On the compose route the memory engine listens only on its own container's
4
+ // loopback; this machine reaches it through the memory forward, the same relay
5
+ // the TUI runs in-process (src/tui/memforward.ts over
6
+ // scripts/companion-memory-forward.mjs). The wizard runs before any TUI, so
7
+ // without this the memory steps' phase B (minting the user keys, the
8
+ // confinement matrix) found nothing listening and sent the operator to a
9
+ // second terminal — and a wizard driven in order never got memory working.
10
+ //
11
+ // A forward that is already listening (`qcr tui`, the systemd unit) is
12
+ // adopted and left alone; one this opens is closed when the step ends, so the
13
+ // wizard leaves nothing running behind it. The real adapter assembly
14
+ // (extensions/onboarding.ts) hands this to the steps; a test's adapters never
15
+ // do, so no test binds a port.
16
+ import { MemoryForward } from "../tui/memforward.js";
17
+
18
+ export interface StepForwardRequest {
19
+ /** Where the step reaches memory (the forward's loopback listener). */
20
+ baseUrl: string;
21
+ /** The checkout holding scripts/. */
22
+ repoRoot: string;
23
+ /** The memory sidecar's container. */
24
+ container: string;
25
+ sudoDocker: boolean;
26
+ log?: (line: string) => void;
27
+ }
28
+
29
+ export interface StepForward {
30
+ /** true when this call started the forward (and stop() closes it). */
31
+ started: boolean;
32
+ detail: string;
33
+ stop(): void;
34
+ }
35
+
36
+ /** Open (or adopt) the local memory forward; null when none could be made to
37
+ * answer, with nothing left bound. */
38
+ export async function openStepMemoryForward(
39
+ req: StepForwardRequest,
40
+ env: NodeJS.ProcessEnv = process.env,
41
+ ): Promise<StepForward | null> {
42
+ const forward = new MemoryForward({
43
+ baseUrl: req.baseUrl,
44
+ repoRoot: req.repoRoot,
45
+ env,
46
+ local: true,
47
+ sudoDocker: req.sudoDocker,
48
+ container: req.container,
49
+ ...(req.log ? { log: req.log } : {}),
50
+ });
51
+ const status = await forward.ensure();
52
+ if (status.state === "owned")
53
+ return {
54
+ started: true,
55
+ detail: `opened a memory forward to ${req.container} for this step; it closes when the step ends, and \`qcr tui\` runs its own`,
56
+ stop: () => forward.stop(),
57
+ };
58
+ if (status.state === "adopted") return { started: false, detail: status.detail, stop: () => {} };
59
+ forward.stop();
60
+ return null;
61
+ }
@@ -50,7 +50,7 @@ import {
50
50
  tomlString,
51
51
  type TomlEdit,
52
52
  } from "./configfill.js";
53
- import { CHOICE_KEYS, botLoginFor } from "../choices.js";
53
+ import { CHOICE_KEYS, botLoginFor, userAuthRecorded } from "../choices.js";
54
54
  import { api, readCredentials } from "./githubapp.js";
55
55
 
56
56
  export const APP_SLUG_STEP_ID = "app_slug_reconcile";
@@ -254,6 +254,9 @@ export function appSlugStep(opts: AppSlugStepOptions = {}): OnboardingStep {
254
254
  "slug, so this step asks the App itself who it is and corrects [github] app_slug and bot_login to match. It matters more " +
255
255
  "than it looks: bot_login is how the reviewer recognises its own reviews and comments, and a wrong one fails silently.",
256
256
  order: 65,
257
+ // There is no App slug on the own-account route: `[github] app_slug` is ""
258
+ // and bot_login is the person's login (the github_account step writes both).
259
+ enabled: (choices) => !userAuthRecorded(choices),
257
260
  async run(ctx): Promise<StepReport> {
258
261
  return reconcile(ctx, true);
259
262
  },
@@ -74,6 +74,9 @@ export const MEMORY_IMAGE_REPO = "ghcr.io/quickchatai/quickreview-memory";
74
74
 
75
75
  export { CONTAINER_STATE_DIR };
76
76
  export const CONTAINER_DRYRUN_DIR = `${CONTAINER_STATE_DIR}/dryrun`;
77
+ /** The daemon's command root in the release image (`<root>/commands`, with the
78
+ * root-sealed `operator/` under it and the result files in `results/`). */
79
+ export const CONTAINER_COMMANDS_DIR = "/opt/qcr/commands";
77
80
 
78
81
  /** `<sandbox root>` — the engine's contract is stateDir = `<root>/state`. */
79
82
  export function rootOf(adapters: OnboardAdapters): string {
@@ -157,6 +160,12 @@ export function daemonExecArgs(root: string, argv: readonly string[]): string[]
157
160
  return composeArgs(root, ["exec", "-T", DAEMON_SERVICE, ...argv]);
158
161
  }
159
162
 
163
+ /** The same exec as uid 0 — the operator seal on `commands/operator/`, which
164
+ * the daemon's own uid (every session's) can read but not write. */
165
+ export function daemonRootExecArgs(root: string, argv: readonly string[]): string[] {
166
+ return composeArgs(root, ["exec", "-T", "-u", "0", DAEMON_SERVICE, ...argv]);
167
+ }
168
+
160
169
  export async function daemonExec(
161
170
  runner: ProcessRunner,
162
171
  root: string,
@@ -172,9 +181,31 @@ export interface PsReport {
172
181
  detail: string;
173
182
  }
174
183
 
184
+ /** docker-compose.yml's `name:` — the project label on its containers. */
185
+ export const COMPOSE_PROJECT = "quickreview";
186
+
187
+ /** `docker ps` over the compose project label, one compose-ps-shaped JSON row
188
+ * per running container: asks Docker without reading the compose file. */
189
+ export function projectPsArgs(): string[] {
190
+ return [
191
+ "ps",
192
+ "--filter",
193
+ `label=com.docker.compose.project=${COMPOSE_PROJECT}`,
194
+ "--format",
195
+ '{"Service":"{{.Label "com.docker.compose.service"}}","State":"{{.State}}"}',
196
+ ];
197
+ }
198
+
175
199
  export async function composePs(runner: ProcessRunner, root: string): Promise<PsReport> {
176
200
  const ps = await runner.run("docker", composeArgs(root, ["ps", "--format", "json"]));
177
201
  if (ps.code !== 0) {
202
+ // `docker compose ps` interpolates the compose file first, and before the
203
+ // bring-up has written the project .env (QCR_SECRETS_DIR and the rest) it
204
+ // refuses to run at all. That is a question about the file, not the
205
+ // stack: Docker itself still answers which of the project's containers
206
+ // run. Only when Docker cannot answer either is the state unknown.
207
+ const plain = await runner.run("docker", projectPsArgs());
208
+ if (plain.code === 0) return { ok: true, rows: parseComposePs(plain.stdout), detail: "" };
178
209
  return {
179
210
  ok: false,
180
211
  rows: [],