ptbk 0.119.0-21 → 0.119.0-26

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 (223) hide show
  1. package/README.md +16 -9
  2. package/dist/browser/browser.d.ts +50 -36
  3. package/dist/browser/browser.js +162 -118
  4. package/dist/browser/browser.js.map +1 -1
  5. package/dist/browser/fleet.d.ts +59 -0
  6. package/dist/browser/fleet.js +148 -0
  7. package/dist/browser/fleet.js.map +1 -0
  8. package/dist/browser/recording.js +2 -2
  9. package/dist/browser/recording.js.map +1 -1
  10. package/dist/chats/conversation.d.ts +1 -1
  11. package/dist/chats/conversation.js +8 -6
  12. package/dist/chats/conversation.js.map +1 -1
  13. package/dist/chats/local.js +5 -5
  14. package/dist/chats/local.js.map +1 -1
  15. package/dist/cli/commands/add.js +1 -1
  16. package/dist/cli/commands/add.js.map +1 -1
  17. package/dist/cli/commands/boilerplates.js +1 -1
  18. package/dist/cli/commands/boilerplates.js.map +1 -1
  19. package/dist/cli/commands/find.js +1 -1
  20. package/dist/cli/commands/find.js.map +1 -1
  21. package/dist/cli/commands/fix.js +9 -4
  22. package/dist/cli/commands/fix.js.map +1 -1
  23. package/dist/cli/commands/init.d.ts +5 -0
  24. package/dist/cli/commands/init.js +279 -118
  25. package/dist/cli/commands/init.js.map +1 -1
  26. package/dist/cli/commands/migrate.js +1 -1
  27. package/dist/cli/commands/migrate.js.map +1 -1
  28. package/dist/cli/commands/ping.js +20 -12
  29. package/dist/cli/commands/ping.js.map +1 -1
  30. package/dist/cli/commands/plan.js +20 -12
  31. package/dist/cli/commands/plan.js.map +1 -1
  32. package/dist/cli/commands/recover.js +1 -1
  33. package/dist/cli/commands/recover.js.map +1 -1
  34. package/dist/cli/commands/restart.js +6 -5
  35. package/dist/cli/commands/restart.js.map +1 -1
  36. package/dist/cli/commands/run.js +26 -15
  37. package/dist/cli/commands/run.js.map +1 -1
  38. package/dist/cli/commands/start.js +11 -7
  39. package/dist/cli/commands/start.js.map +1 -1
  40. package/dist/cli/commands/tool.d.ts +19 -0
  41. package/dist/cli/commands/tool.js +320 -0
  42. package/dist/cli/commands/tool.js.map +1 -0
  43. package/dist/cli/commands/verify.js +1 -1
  44. package/dist/cli/commands/verify.js.map +1 -1
  45. package/dist/cli/common.d.ts +7 -1
  46. package/dist/cli/common.js +37 -20
  47. package/dist/cli/common.js.map +1 -1
  48. package/dist/cli/compatibility.d.ts +8 -0
  49. package/dist/cli/compatibility.js +32 -0
  50. package/dist/cli/compatibility.js.map +1 -0
  51. package/dist/cli/parser.js +7 -3
  52. package/dist/cli/parser.js.map +1 -1
  53. package/dist/cli/registry.js +182 -7
  54. package/dist/cli/registry.js.map +1 -1
  55. package/dist/cli/service-view.js +1 -1
  56. package/dist/cli/service-view.js.map +1 -1
  57. package/dist/cli/sources.js +2 -0
  58. package/dist/cli/sources.js.map +1 -1
  59. package/dist/cli/spec.js +8 -6
  60. package/dist/cli/spec.js.map +1 -1
  61. package/dist/common/naming.d.ts +53 -0
  62. package/dist/common/naming.js +90 -0
  63. package/dist/common/naming.js.map +1 -0
  64. package/dist/engine/agenda.d.ts +70 -9
  65. package/dist/engine/agenda.js +294 -61
  66. package/dist/engine/agenda.js.map +1 -1
  67. package/dist/engine/executor.d.ts +5 -0
  68. package/dist/engine/executor.js +24 -11
  69. package/dist/engine/executor.js.map +1 -1
  70. package/dist/engine/harnesses.d.ts +82 -18
  71. package/dist/engine/harnesses.js +191 -39
  72. package/dist/engine/harnesses.js.map +1 -1
  73. package/dist/engine/preview.js +2 -2
  74. package/dist/engine/preview.js.map +1 -1
  75. package/dist/engine/routing.d.ts +10 -0
  76. package/dist/engine/routing.js +67 -28
  77. package/dist/engine/routing.js.map +1 -1
  78. package/dist/engine/snapshot.d.ts +107 -7
  79. package/dist/engine/state.d.ts +4 -2
  80. package/dist/engine/state.js.map +1 -1
  81. package/dist/engine/sync.d.ts +96 -0
  82. package/dist/engine/sync.js +199 -0
  83. package/dist/engine/sync.js.map +1 -0
  84. package/dist/engine/team.d.ts +1 -0
  85. package/dist/engine/team.js +12 -8
  86. package/dist/engine/team.js.map +1 -1
  87. package/dist/expert/_main.md +96 -67
  88. package/dist/expert/agents/_index.md +10 -7
  89. package/dist/expert/agents/core-agents.md +4 -2
  90. package/dist/expert/agents/fixer.md +15 -12
  91. package/dist/expert/cli/acceptance.md +96 -78
  92. package/dist/expert/cli/api.md +26 -14
  93. package/dist/expert/cli/autonomous-operation.md +17 -19
  94. package/dist/expert/cli/browser.md +24 -24
  95. package/dist/expert/cli/checks.md +18 -10
  96. package/dist/expert/cli/cli.md +48 -41
  97. package/dist/expert/cli/configuration-migrations.md +43 -0
  98. package/dist/expert/cli/configuration.md +24 -14
  99. package/dist/expert/cli/controls.md +23 -11
  100. package/dist/expert/cli/dashboard-details.md +28 -10
  101. package/dist/expert/cli/dashboard-layout.md +15 -15
  102. package/dist/expert/cli/dashboard.md +8 -10
  103. package/dist/expert/cli/environment.md +33 -0
  104. package/dist/expert/cli/execution.md +10 -8
  105. package/dist/expert/cli/git-persistence.md +27 -21
  106. package/dist/expert/cli/git-preflight.md +8 -4
  107. package/dist/expert/cli/git-synchronization.md +32 -6
  108. package/dist/expert/cli/harness-plugins.md +12 -12
  109. package/dist/expert/cli/harnesses.md +22 -16
  110. package/dist/expert/cli/initialization-ui.md +14 -14
  111. package/dist/expert/cli/initialization.md +31 -21
  112. package/dist/expert/cli/installation.md +6 -0
  113. package/dist/expert/cli/isolation.md +5 -3
  114. package/dist/expert/cli/releasing.md +17 -3
  115. package/dist/expert/cli/retries.md +10 -8
  116. package/dist/expert/cli/run.md +7 -5
  117. package/dist/expert/cli/start.md +21 -15
  118. package/dist/expert/cli/status-view.md +15 -17
  119. package/dist/expert/cli/synchronization-setup.md +24 -0
  120. package/dist/expert/cli/terminal.md +14 -16
  121. package/dist/expert/cli/tool-management.md +60 -0
  122. package/dist/expert/cli/tool-runtime.md +35 -0
  123. package/dist/expert/cli/tools-migration.md +50 -0
  124. package/dist/expert/cli/tools.md +73 -0
  125. package/dist/expert/cli/trace-configuration.md +9 -11
  126. package/dist/expert/cli/updates.md +7 -7
  127. package/dist/expert/cli/windows-installer.md +67 -0
  128. package/dist/expert/cli/workspace.md +2 -0
  129. package/dist/expert/coding-standards/_index.md +11 -0
  130. package/dist/expert/coding-standards/naming.md +30 -0
  131. package/dist/expert/coding-standards/shared-logic.md +27 -0
  132. package/dist/expert/concepts/automatic-repair.md +33 -0
  133. package/dist/expert/concepts/engine-and-project.md +7 -5
  134. package/dist/expert/dictionary.md +18 -18
  135. package/dist/harness/plugins/claude-code/index.js +4 -2
  136. package/dist/harness/plugins/claude-code/index.js.map +1 -1
  137. package/dist/harness/plugins/cline/index.js +1 -0
  138. package/dist/harness/plugins/cline/index.js.map +1 -1
  139. package/dist/harness/plugins/gemini/index.js +2 -0
  140. package/dist/harness/plugins/gemini/index.js.map +1 -1
  141. package/dist/harness/plugins/github-copilot/index.js +1 -0
  142. package/dist/harness/plugins/github-copilot/index.js.map +1 -1
  143. package/dist/harness/plugins/openai-codex/index.js +13 -9
  144. package/dist/harness/plugins/openai-codex/index.js.map +1 -1
  145. package/dist/harness/plugins/opencode/index.js +1 -0
  146. package/dist/harness/plugins/opencode/index.js.map +1 -1
  147. package/dist/harness/plugins/qwen-code/index.js +1 -0
  148. package/dist/harness/plugins/qwen-code/index.js.map +1 -1
  149. package/dist/harness/types.d.ts +16 -5
  150. package/dist/project/config.d.ts +135 -48
  151. package/dist/project/config.js +331 -119
  152. package/dist/project/config.js.map +1 -1
  153. package/dist/project/configuration.d.ts +43 -0
  154. package/dist/project/configuration.js +165 -0
  155. package/dist/project/configuration.js.map +1 -0
  156. package/dist/project/defaults.d.ts +4 -2
  157. package/dist/project/defaults.js +10 -5
  158. package/dist/project/defaults.js.map +1 -1
  159. package/dist/project/git.d.ts +28 -0
  160. package/dist/project/git.js +81 -0
  161. package/dist/project/git.js.map +1 -1
  162. package/dist/project/migrations.d.ts +96 -0
  163. package/dist/project/migrations.js +301 -0
  164. package/dist/project/migrations.js.map +1 -0
  165. package/dist/project/publication.d.ts +57 -0
  166. package/dist/project/publication.js +117 -0
  167. package/dist/project/publication.js.map +1 -0
  168. package/dist/project/readiness.d.ts +14 -5
  169. package/dist/project/readiness.js +23 -22
  170. package/dist/project/readiness.js.map +1 -1
  171. package/dist/project/setup.js +2 -0
  172. package/dist/project/setup.js.map +1 -1
  173. package/dist/service/actions.d.ts +12 -4
  174. package/dist/service/actions.js +32 -2
  175. package/dist/service/actions.js.map +1 -1
  176. package/dist/service/control.d.ts +1 -1
  177. package/dist/service/control.js +1 -1
  178. package/dist/service/host.d.ts +5 -2
  179. package/dist/service/host.js +26 -16
  180. package/dist/service/host.js.map +1 -1
  181. package/dist/service/http.js +7 -2
  182. package/dist/service/http.js.map +1 -1
  183. package/dist/service/launcher.d.ts +2 -1
  184. package/dist/service/launcher.js +1 -0
  185. package/dist/service/launcher.js.map +1 -1
  186. package/dist/service/records.d.ts +3 -0
  187. package/dist/service/records.js +1 -1
  188. package/dist/service/records.js.map +1 -1
  189. package/dist/service/spa/app.css +39 -0
  190. package/dist/service/spa/app.js +242 -39
  191. package/dist/service/worker.js +1 -0
  192. package/dist/service/worker.js.map +1 -1
  193. package/dist/tools/environment.d.ts +122 -0
  194. package/dist/tools/environment.js +199 -0
  195. package/dist/tools/environment.js.map +1 -0
  196. package/dist/tools/model.d.ts +180 -0
  197. package/dist/tools/model.js +111 -0
  198. package/dist/tools/model.js.map +1 -0
  199. package/dist/tools/normalize.d.ts +51 -0
  200. package/dist/tools/normalize.js +321 -0
  201. package/dist/tools/normalize.js.map +1 -0
  202. package/dist/tools/operations.d.ts +198 -0
  203. package/dist/tools/operations.js +378 -0
  204. package/dist/tools/operations.js.map +1 -0
  205. package/dist/tools/readiness.d.ts +72 -0
  206. package/dist/tools/readiness.js +160 -0
  207. package/dist/tools/readiness.js.map +1 -0
  208. package/dist/ui/dashboard.d.ts +3 -0
  209. package/dist/ui/dashboard.js +173 -2
  210. package/dist/ui/dashboard.js.map +1 -1
  211. package/dist/ui/details.d.ts +35 -1
  212. package/dist/ui/details.js +103 -2
  213. package/dist/ui/details.js.map +1 -1
  214. package/dist/ui/secret.d.ts +8 -0
  215. package/dist/ui/secret.js +41 -0
  216. package/dist/ui/secret.js.map +1 -0
  217. package/dist/ui/status-view.d.ts +1 -0
  218. package/dist/ui/status-view.js +33 -8
  219. package/dist/ui/status-view.js.map +1 -1
  220. package/dist/ui/tool-forms.d.ts +24 -0
  221. package/dist/ui/tool-forms.js +133 -0
  222. package/dist/ui/tool-forms.js.map +1 -0
  223. package/package.json +1 -1
@@ -1,13 +1,23 @@
1
1
  import { readFile, writeFile } from 'node:fs/promises';
2
- import { dirname, join, relative, resolve, sep } from 'node:path';
2
+ import { basename, dirname, join, relative, resolve, sep } from 'node:path';
3
3
  import { EXIT, PtbkError, errorMessage, usageError } from '../../common/errors.js';
4
- import { ensureDirectory, isDirectory, pathExists, toProjectRelative } from '../../common/fs.js';
4
+ import { ensureDirectory, isDirectory, pathExists, toProjectRelative, writeFileAtomic } from '../../common/fs.js';
5
+ import { slugify } from '../../common/ids.js';
5
6
  import { ENGINE_VERSION } from '../../common/version.js';
6
- import { describeBrowserProfile, userChromeProfile } from '../../browser/browser.js';
7
+ import { userChromeProfile } from '../../browser/browser.js';
7
8
  import { BookIndex } from '../../project/books.js';
8
9
  import { generatedName } from '../../tasks/identity.js';
9
- import { checkCandidates, validateCheckCommand } from '../../project/checks.js';
10
- import { enabledHarnesses, getPath, loadSettings, readProjectConfigFile, setting, updateProjectConfig } from '../../project/config.js';
10
+ import { validateCheckCommand } from '../../project/checks.js';
11
+ import { CHECK_PLACEHOLDER, SETTING_SPEC_BY_KEY, canonicalizeConfig, configText, enabledHarnesses, getPath, interpretConfig, isCheckPlaceholder, loadSettings, profileFromSpelling, readProjectConfigFile, serializeSettingValue, setPath, setting } from '../../project/config.js';
12
+ import { maintainProjectConfiguration } from '../../project/configuration.js';
13
+ import { MIGRATED_BROWSER_NAME, planConfigurationMigration } from '../../project/migrations.js';
14
+ import { requireEngineUpdate } from '../compatibility.js';
15
+ import { existingOrigin, githubLogin, publishProject } from '../../project/publication.js';
16
+ import { instanceEnvironment } from '../../engine/harnesses.js';
17
+ import { redactUrl } from '../../engine/sync.js';
18
+ import { applyToolOperationsToConfig } from '../../tools/operations.js';
19
+ import { inspectBrowserTool } from '../../tools/readiness.js';
20
+ import { describeProfileSource, harnessVariantOf } from '../../tools/model.js';
11
21
  import { INITIAL_ASSETS, INITIAL_SPECIFICATION, bootstrapTask, sampleReadme } from '../../project/defaults.js';
12
22
  import { clearSetupDraft, readSetupDraft, saveSetupDraft } from '../../project/draft.js';
13
23
  import { commitPaths, inspectGit } from '../../project/git.js';
@@ -64,7 +74,7 @@ const STEP_TITLES = {
64
74
  harnesses: 'Harnesses',
65
75
  browser: 'Project browser',
66
76
  traces: 'Traces',
67
- validation: 'Validation',
77
+ sync: 'Automatic synchronization',
68
78
  finish: 'Set up and start',
69
79
  };
70
80
  /**
@@ -130,6 +140,18 @@ async function nearestExisting(path) {
130
140
  function blankIntent(segments) {
131
141
  return segments.every((segment) => segment.type === 'text' && !segment.text.trim());
132
142
  }
143
+ /**
144
+ * The destination's saved configuration as this engine interprets it (a pending migration applied in memory only).
145
+ */
146
+ async function savedConfiguration(root) {
147
+ return interpretConfig(await readProjectConfigFile(projectAt(root))).interpreted;
148
+ }
149
+ /**
150
+ * Whether a configuration already has a browser tool (the browser question is then not asked again).
151
+ */
152
+ function hasBrowserTool(raw) {
153
+ return Array.isArray(raw.tools) && raw.tools.some((tool) => tool !== null && typeof tool === 'object' && String(tool.type).toUpperCase() === 'BROWSER');
154
+ }
133
155
  /**
134
156
  * Whether a destination already records its purpose (initial specification or an earlier initialization).
135
157
  */
@@ -160,8 +182,8 @@ export async function initializeProject(context, options = {}) {
160
182
  const notes = [];
161
183
  const missingDecisions = [];
162
184
  const title = options.fromStart ? 'ptbk start · setup' : 'ptbk init';
163
- const steps = new StepTracker(['intent', 'update', 'destination', 'installation', 'harnesses', 'browser', 'traces', 'validation', 'finish']);
164
- const stepOrder = ['intent', 'update', 'destination', 'installation', 'harnesses', 'browser', 'traces', 'validation', 'finish'];
185
+ const steps = new StepTracker(['intent', 'update', 'destination', 'installation', 'harnesses', 'browser', 'traces', 'sync', 'finish']);
186
+ const stepOrder = ['intent', 'update', 'destination', 'installation', 'harnesses', 'browser', 'traces', 'sync', 'finish'];
165
187
  const frame = (id, note, activity = 'working') => ({
166
188
  title,
167
189
  step: steps.label(id),
@@ -330,6 +352,9 @@ export async function initializeProject(context, options = {}) {
330
352
  break;
331
353
  }
332
354
  answers.set('destination', `Destination: ${state.root}${state.nested ? ` (${state.nested.strategy} child)` : ''}`);
355
+ const compatibility = planConfigurationMigration((await readProjectConfigFile(projectAt(state.root))).raw);
356
+ if (compatibility.kind === 'older-engine')
357
+ await requireEngineUpdate(context, compatibility.saved);
333
358
  await predictPath();
334
359
  // A changed destination may already record its purpose.
335
360
  if (!state.purposeRecorded && (await recordedPurpose(state.root))) {
@@ -423,10 +448,10 @@ export async function initializeProject(context, options = {}) {
423
448
  },
424
449
  // 6. Browser (independent of harness logins).
425
450
  browser: async () => {
426
- const raw = (await readProjectConfigFile(projectAt(state.root))).raw;
451
+ const saved = await savedConfiguration(state.root);
427
452
  state.browserProfile = option(context, 'browser-profile') ?? state.browserProfile;
428
453
  state.browserMode = option(context, 'browser-mode') ?? state.browserMode;
429
- if (raw.browser || !interactive || option(context, 'browser-profile')) {
454
+ if (hasBrowserTool(saved) || !interactive || option(context, 'browser-profile')) {
430
455
  steps.set('browser', false);
431
456
  return;
432
457
  }
@@ -464,19 +489,20 @@ export async function initializeProject(context, options = {}) {
464
489
  ],
465
490
  defaultValue: state.browserMode ?? 'headless',
466
491
  }));
467
- answers.set('browser', `Browser: ${describeBrowserProfile(state.browserProfile ?? 'global')}, ${state.browserMode}`);
492
+ const profile = profileFromSpelling(state.browserProfile ?? 'global');
493
+ answers.set('browser', `Browser: ${describeProfileSource(profile.source, profile.path)}, ${state.browserMode}`);
468
494
  },
469
495
  // 7. Trace recording and Git inclusion (three explicit project choices).
470
496
  traces: async () => {
471
- const raw = (await readProjectConfigFile(projectAt(state.root))).raw;
497
+ const raw = await savedConfiguration(state.root);
472
498
  const saved = (key) => (typeof getPath(raw, key) === 'boolean' ? getPath(raw, key) : undefined);
473
499
  const flagged = { commit: flag(context, 'commit-traces'), ignoreVideos: flag(context, 'ignore-trace-videos'), recordVideo: flag(context, 'record-video') };
474
500
  const current = {
475
- commit: saved('traces.commit') ?? flagged.commit ?? state.traces?.commit ?? false,
476
- ignoreVideos: saved('traces.ignoreVideos') ?? flagged.ignoreVideos ?? state.traces?.ignoreVideos ?? true,
477
- recordVideo: saved('traces.recordVideo') ?? flagged.recordVideo ?? state.traces?.recordVideo ?? true,
501
+ commit: saved('traces.isCommitted') ?? flagged.commit ?? state.traces?.commit ?? false,
502
+ ignoreVideos: saved('traces.isVideoIgnored') ?? flagged.ignoreVideos ?? state.traces?.ignoreVideos ?? true,
503
+ recordVideo: saved('traces.isVideoRecordingEnabled') ?? flagged.recordVideo ?? state.traces?.recordVideo ?? true,
478
504
  };
479
- const complete = saved('traces.commit') !== undefined && saved('traces.ignoreVideos') !== undefined && saved('traces.recordVideo') !== undefined;
505
+ const complete = saved('traces.isCommitted') !== undefined && saved('traces.isVideoIgnored') !== undefined && saved('traces.isVideoRecordingEnabled') !== undefined;
480
506
  const decided = Object.values(flagged).every((value) => value !== undefined);
481
507
  if (complete || decided || !interactive) {
482
508
  state.traces = current;
@@ -502,48 +528,118 @@ export async function initializeProject(context, options = {}) {
502
528
  }));
503
529
  answers.set('traces', `Traces: ${state.traces.commit ? 'committed' : 'local only'}, videos ${state.traces.recordVideo ? `recorded${state.traces.commit && !state.traces.ignoreVideos ? ' and committed' : ' locally'}` : 'off'}`);
504
530
  },
505
- // 8. Real project validation (never an unconditional pass).
506
- validation: async () => {
507
- const configBefore = (await pathExists(projectAt(state.root).configPath)) ? await loadSettings(projectAt(state.root), { env: context.env }) : undefined;
508
- const check = configBefore ? (setting(configBefore, 'execution.check') ?? undefined) : undefined;
509
- const supplied = option(context, 'check');
510
- const existing = await validateCheckCommand(state.root, check === undefined ? undefined : check, context.env);
511
- const fresh = !(await isDirectory(state.root)) || (await isFreshDirectory(state.root));
512
- state.saveCheck = undefined;
513
- if (existing.ok || supplied || fresh || !interactive) {
514
- if (supplied && !existing.ok)
515
- state.saveCheck = supplied;
516
- else if (!existing.ok && fresh)
517
- state.saveCheck = state.installation === 'global' ? 'ptbk lint' : 'npm run check';
518
- steps.set('validation', false);
531
+ // 8. Automatic synchronization: one opt-in decision, default off; "no" asks nothing further.
532
+ sync: async () => {
533
+ const saved = await savedConfiguration(state.root);
534
+ const explicit = flag(context, 'auto-sync') ?? (flag(context, 'auto-pull') || flag(context, 'auto-push') ? true : undefined);
535
+ const savedChoice = typeof saved.isAutoSync === 'boolean' ? saved.isAutoSync : undefined;
536
+ const repositoryGit = await inspectGit(await nearestExisting(state.root));
537
+ const ownRepository = repositoryGit.state === 'repository' && canonicalPath(repositoryGit.root) === canonicalPath(state.root);
538
+ const origin = ownRepository ? await existingOrigin(repositoryGit.root) : undefined;
539
+ if (explicit === undefined && savedChoice !== undefined) {
540
+ state.sync = { enabled: savedChoice, reused: true };
541
+ steps.set('sync', false);
519
542
  return;
520
543
  }
521
- shown.add('validation');
522
- const candidates = await checkCandidates(state.root, context.env);
523
- for (;;) {
524
- const choice = answered(await selectOne(terminal, {
525
- frame: frame('validation', 'every accepted task must pass this check'),
526
- question: check !== undefined ? `The configured check is not usable: ${existing.ok ? '' : existing.reason} Choose the project check:` : 'Which command validates this project? (it must pass before each task is accepted)',
527
- choices: [
528
- ...candidates.map((candidate) => ({ value: candidate.command, label: candidate.command, description: candidate.description })),
529
- { value: '__custom', label: 'Enter a custom command' },
530
- { value: '__defer', label: 'Defer (setup stays incomplete)' },
531
- ],
532
- defaultValue: candidates[0]?.command ?? '__custom',
533
- }));
534
- if (choice === '__defer')
544
+ let enabled = explicit;
545
+ if (enabled === undefined) {
546
+ if (!interactive) {
547
+ state.sync = { enabled: false, reused: false };
548
+ steps.set('sync', false);
535
549
  return;
536
- const command = choice === '__custom' ? answered(await inputLine(terminal, { frame: frame('validation'), question: 'Check command (runs in the project directory):' })) : choice;
537
- const validation = await validateCheckCommand(state.root, command, context.env);
538
- if (!validation.ok) {
539
- say(style.warning(`${marks.warn} ${validation.reason}`));
540
- continue;
541
550
  }
542
- if (answered(await confirm(terminal, { frame: frame('validation'), question: `Save \`${command}\` as ptbk.json execution.check?`, defaultYes: true }))) {
543
- state.saveCheck = command;
544
- return;
551
+ shown.add('sync');
552
+ enabled = answered(await confirm(terminal, {
553
+ frame: frame('sync', 'optional: keep a Git remote up to date'),
554
+ question: 'Synchronize this project automatically with a Git remote? (pull every minute and before work, then push completed work)',
555
+ defaultYes: false,
556
+ yesLabel: 'Yes, set up automatic synchronization',
557
+ noLabel: 'No, keep the project local (default)',
558
+ }));
559
+ }
560
+ if (!enabled) {
561
+ state.sync = { enabled: false, reused: false };
562
+ answers.set('sync', 'Synchronization: off');
563
+ return;
564
+ }
565
+ if (!commit)
566
+ throw usageError('Automatic synchronization publishes commits, so it conflicts with --no-commit.', 'init');
567
+ const provider = option(context, 'sync-provider');
568
+ const repositoryFlag = option(context, 'github-repository');
569
+ const originUrl = option(context, 'origin-url');
570
+ const visibility = option(context, 'github-visibility') ?? 'private';
571
+ const github = (value) => {
572
+ const [owner, name] = value.includes('/') ? value.split('/', 2) : [undefined, value];
573
+ return { kind: 'github', owner: owner || undefined, name: name, visibility };
574
+ };
575
+ let choice;
576
+ if (originUrl)
577
+ choice = { kind: 'origin', url: originUrl };
578
+ else if (repositoryFlag || provider === 'github')
579
+ choice = repositoryFlag ? github(repositoryFlag) : undefined;
580
+ else if (provider === 'existing' || provider === 'origin' || (savedChoice === true && origin))
581
+ choice = origin ? { kind: 'existing' } : undefined;
582
+ if (!choice && interactive) {
583
+ shown.add('sync');
584
+ if (origin) {
585
+ // An existing remote is reused explicitly, never silently replaced or duplicated on GitHub.
586
+ const reuse = answered(await confirm(terminal, { frame: frame('sync'), question: `This repository already has origin ${redactUrl(origin)}. Synchronize with it?`, defaultYes: true, yesLabel: 'Yes, use the existing origin', noLabel: 'No, keep synchronization off' }));
587
+ if (!reuse) {
588
+ state.sync = { enabled: false, reused: false };
589
+ answers.set('sync', 'Synchronization: off');
590
+ return;
591
+ }
592
+ choice = { kind: 'existing' };
593
+ }
594
+ else {
595
+ const kind = answered(await selectOne(terminal, {
596
+ frame: frame('sync', 'where should the project be published?'),
597
+ question: 'Where should the project be synchronized?',
598
+ choices: [
599
+ { value: 'github', label: 'GitHub', description: 'create a new repository (private by default) with your GitHub CLI login and publish this project' },
600
+ { value: 'origin', label: 'Another Git origin', description: 'enter a remote URL (any Git host or server)' },
601
+ ],
602
+ defaultValue: 'github',
603
+ }));
604
+ if (kind === 'origin') {
605
+ const url = answered(await inputLine(terminal, { frame: frame('sync'), question: 'Remote URL of the Git origin:', validate: (value) => (!value.trim() ? 'Enter a Git remote URL.' : undefined) })).trim();
606
+ choice = { kind: 'origin', url };
607
+ }
608
+ else {
609
+ const login = await githubLogin(context.env);
610
+ if (login.problem)
611
+ say(style.warning(`${marks.warn} ${login.problem} Publication is attempted after the local setup and can be retried later.`));
612
+ const name = slugify(basename(state.root)) || 'project';
613
+ const repository = answered(await inputLine(terminal, {
614
+ frame: frame('sync'),
615
+ question: 'New GitHub repository (owner/name):',
616
+ initial: `${login.login ?? ''}${login.login ? '/' : ''}${name}`,
617
+ validate: (value) => (/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(value.trim()) || (/^[A-Za-z0-9_.-]+$/.test(value.trim()) && login.login) ? undefined : 'Enter owner/name, e.g. my-company/newsletter.'),
618
+ })).trim();
619
+ const chosenVisibility = answered(await selectOne(terminal, {
620
+ frame: frame('sync'),
621
+ question: 'Visibility of the new repository:',
622
+ choices: [
623
+ { value: 'private', label: 'Private (recommended)', description: 'only people you invite can see it' },
624
+ { value: 'public', label: 'Public', description: 'anyone can see the code and its whole history' },
625
+ ],
626
+ defaultValue: 'private',
627
+ }));
628
+ const target = github(repository);
629
+ choice = { ...target, visibility: chosenVisibility };
630
+ const shownName = `${target.owner ?? login.login ?? '<you>'}/${target.name}`;
631
+ const ok = answered(await confirm(terminal, { frame: frame('sync'), question: `Create github.com/${shownName} (${chosenVisibility}) and publish this project after the local setup is committed?`, defaultYes: true }));
632
+ if (!ok)
633
+ throw new WizardBack('back');
634
+ }
545
635
  }
546
636
  }
637
+ if (!choice) {
638
+ missingDecisions.push(origin ? '--sync-provider existing (reuse the existing origin)' : '--github-repository <owner>/<name> or --origin-url <url> (automatic synchronization was enabled)');
639
+ return;
640
+ }
641
+ state.sync = { enabled: true, choice, reused: false };
642
+ answers.set('sync', `Synchronization: ${choice.kind === 'existing' ? `existing origin ${redactUrl(origin ?? '')}` : choice.kind === 'origin' ? `origin ${redactUrl(choice.url)}` : `GitHub ${choice.owner ? `${choice.owner}/` : ''}${choice.name} (${choice.visibility})`}`);
547
643
  },
548
644
  };
549
645
  /**
@@ -554,27 +650,28 @@ export async function initializeProject(context, options = {}) {
554
650
  if (!interactive)
555
651
  return;
556
652
  const destination = projectAt(state.root);
557
- const raw = (await readProjectConfigFile(destination)).raw;
653
+ const raw = await savedConfiguration(state.root);
558
654
  const local = await inspectLocalInstallation(state.root);
559
655
  const saved = await loadSettings(destination, { env: context.env }).catch(() => undefined);
560
656
  const remembered = (raw.initialized ?? {}).installation === 'global';
561
- const fresh = !(await isDirectory(state.root)) || (await isFreshDirectory(state.root));
562
657
  steps.set('installation', !(local.usable || local.declaredIn || option(context, 'installation') || remembered));
563
658
  steps.set('harnesses', !(option(context, 'harness') || flag(context, 'no-harness') || (saved && enabledHarnesses(saved).length)));
564
- steps.set('browser', !(raw.browser || option(context, 'browser-profile')));
565
- const tracesSaved = ['traces.commit', 'traces.ignoreVideos', 'traces.recordVideo'].every((key) => typeof getPath(raw, key) === 'boolean');
659
+ steps.set('browser', !(hasBrowserTool(raw) || option(context, 'browser-profile')));
660
+ const tracesSaved = ['traces.isCommitted', 'traces.isVideoIgnored', 'traces.isVideoRecordingEnabled'].every((key) => typeof getPath(raw, key) === 'boolean');
566
661
  const tracesFlagged = flag(context, 'commit-traces') !== undefined && flag(context, 'ignore-trace-videos') !== undefined && flag(context, 'record-video') !== undefined;
567
662
  steps.set('traces', !(tracesSaved || tracesFlagged));
568
- const check = saved ? (setting(saved, 'execution.check') ?? undefined) : undefined;
569
- steps.set('validation', !(fresh || option(context, 'check') || (check !== undefined && (await validateCheckCommand(state.root, check, context.env)).ok)));
663
+ steps.set('sync', !(typeof raw.isAutoSync === 'boolean' || flag(context, 'auto-sync') !== undefined || flag(context, 'auto-pull') || flag(context, 'auto-push')));
570
664
  };
571
665
  /**
572
666
  * Installs, logs in and updates selected harnesses (interactive sub-steps of the Harnesses step).
573
667
  */
574
668
  const setUpHarnesses = async (selected) => {
575
- const context2 = { projectRoot: await nearestExisting(state.root), env: context.env };
669
+ const projectRoot = await nearestExisting(state.root);
576
670
  for (const id of [...selected]) {
577
671
  const plugin = harnessPlugin(id);
672
+ // The simple init instance: the native account context of the global installation (never an ambient API key).
673
+ const nativeTool = { type: 'HARNESS', variant: plugin.variant, name: plugin.id, isEnabled: true, authentication: 'NATIVE' };
674
+ const context2 = { projectRoot, env: instanceEnvironment(plugin, nativeTool, undefined, context.env), tool: nativeTool };
578
675
  let inspection = await plugin.inspect(context2);
579
676
  if (!inspection.installed) {
580
677
  let install = flag(context, 'install-harnesses') === true;
@@ -620,6 +717,7 @@ export async function initializeProject(context, options = {}) {
620
717
  continue;
621
718
  state.inspections.set(id, inspection);
622
719
  }
720
+ const context2 = { projectRoot, env: context.env };
623
721
  const outdated = [];
624
722
  const canDecideUpdates = interactive || option(context, 'update-harnesses') === 'update';
625
723
  for (const id of canDecideUpdates ? selected : []) {
@@ -674,7 +772,7 @@ export async function initializeProject(context, options = {}) {
674
772
  alreadyReady.push(`${item.plugin.displayName} kept at ${item.current}`);
675
773
  };
676
774
  // Questions, with Esc going back to the previous screen.
677
- const order = ['intent', 'update', 'destination', 'installation', 'harnesses', 'browser', 'traces', 'validation'];
775
+ const order = ['intent', 'update', 'destination', 'installation', 'harnesses', 'browser', 'traces', 'sync'];
678
776
  for (let index = 0; index < order.length;) {
679
777
  const id = order[index];
680
778
  try {
@@ -723,8 +821,16 @@ export async function initializeProject(context, options = {}) {
723
821
  changed.push('Git repository initialized');
724
822
  else
725
823
  alreadyReady.push(`Git repository ${repository.repositoryRoot}`);
824
+ // Required configuration evolution of an existing project is its own committed pass, before any setup write.
825
+ if (await pathExists(project.configPath)) {
826
+ const maintenance = await maintainProjectConfiguration(project, { commit, env: context.env });
827
+ if (maintenance.migrated)
828
+ changed.push(`Migrated ptbk.json from ${maintenance.migrated.from} to ${maintenance.migrated.to}${maintenance.migrated.commit ? ` (${maintenance.migrated.commit.slice(0, 8)})` : ''}`);
829
+ if (maintenance.normalized)
830
+ changed.push(`Normalized ptbk.json spelling${maintenance.normalized.commit ? ` (${maintenance.normalized.commit.slice(0, 8)})` : ''}`);
831
+ }
726
832
  const configBefore = (await pathExists(project.configPath)) ? await loadSettings(project, { env: context.env }) : undefined;
727
- let check = configBefore ? (setting(configBefore, 'execution.check') ?? undefined) : undefined;
833
+ const savedCheck = configBefore ? (setting(configBefore, 'execution.check') ?? undefined) : undefined;
728
834
  const installation = state.installation ?? 'existing';
729
835
  if (installation === 'existing') {
730
836
  const local = await inspectLocalInstallation(root);
@@ -740,17 +846,18 @@ export async function initializeProject(context, options = {}) {
740
846
  else
741
847
  unresolved.push(`${plugin.displayName}: ${inspection?.summary ?? 'not inspected'}${inspection?.nextAction ? ` → ${inspection.nextAction}` : ''}`);
742
848
  }
743
- if (!selected.length)
744
- unresolved.push('No harness is selected → run `ptbk init --harness <id>`');
849
+ if (!selected.length && !(configBefore && enabledHarnesses(configBefore).length))
850
+ notes.push('No harness is selected: the agenda runs but waits for tools until one is added (`ptbk tool add HARNESS …`).');
745
851
  const tasksDirectory = configBefore ? setting(configBefore, 'paths.tasks') : 'tasks';
746
852
  const agentsDirectory = configBefore ? setting(configBefore, 'paths.agents') : 'agents';
747
853
  const tracePolicy = state.traces ?? { commit: false, ignoreVideos: true, recordVideo: true };
748
854
  const createdManifest = installation === 'local' && !(await pathExists(join(root, 'package.json')));
749
- const ignore = await ensureRuntimeIgnore(root, createdManifest || (await pathExists(join(root, 'node_modules'))) ? ['node_modules/'] : []);
855
+ // Runtime state and local secrets stay ignored independently of every trace choice.
856
+ const ignore = await ensureRuntimeIgnore(root, [...(createdManifest || (await pathExists(join(root, 'node_modules'))) ? ['node_modules/'] : []), '.env']);
750
857
  if (ignore)
751
858
  createdPaths.push(ignore);
752
- const savedPolicy = configBefore ? { commit: setting(configBefore, 'traces.commit'), ignoreVideos: setting(configBefore, 'traces.ignoreVideos') } : undefined;
753
- const traceIgnore = await ensureTraceIgnoreRules(root, savedPolicy && configBefore?.raw.traces ? savedPolicy : tracePolicy);
859
+ const savedPolicy = configBefore ? { commit: setting(configBefore, 'traces.isCommitted'), ignoreVideos: setting(configBefore, 'traces.isVideoIgnored') } : undefined;
860
+ const traceIgnore = await ensureTraceIgnoreRules(root, savedPolicy && configBefore?.interpreted.traces ? savedPolicy : tracePolicy);
754
861
  if (traceIgnore)
755
862
  createdPaths.push(traceIgnore);
756
863
  // Default roles first: a missing Chatter's initial TEAM covers the completed role set.
@@ -777,7 +884,7 @@ export async function initializeProject(context, options = {}) {
777
884
  createdPaths.push(specification, ...files);
778
885
  }
779
886
  if (installation === 'local' || createdManifest) {
780
- if (await ensureManifest(root, project.name, state.saveCheck === 'npm run check' && fresh ? { check: 'ptbk lint' } : {}))
887
+ if (await ensureManifest(root, project.name))
781
888
  createdPaths.push(join(root, 'package.json'));
782
889
  }
783
890
  if (installation === 'local') {
@@ -803,59 +910,47 @@ export async function initializeProject(context, options = {}) {
803
910
  else
804
911
  notes.push(global.note);
805
912
  }
806
- const existingValidation = await validateCheckCommand(root, check === undefined ? undefined : check, context.env);
807
- if (existingValidation.ok) {
808
- alreadyReady.push(`Check: ${check}`);
809
- const supplied = option(context, 'check');
810
- if (supplied && supplied !== check)
811
- notes.push(`Check override for this run only: ${supplied}`);
913
+ // No check question: a real existing check is kept (even a failing one: that is ordinary repair work); a supplied
914
+ // --check fills a missing one; otherwise the explicit informational placeholder records that validation is absent.
915
+ const suppliedCheck = option(context, 'check');
916
+ let check = savedCheck;
917
+ let saveCheck;
918
+ if (savedCheck !== undefined) {
919
+ if (isCheckPlaceholder(savedCheck))
920
+ notes.push('Real automated validation is not configured yet (execution.check holds the informational placeholder).');
921
+ else {
922
+ const validation = await validateCheckCommand(root, savedCheck, context.env);
923
+ if (validation.ok)
924
+ alreadyReady.push(`Check: ${savedCheck}`);
925
+ else
926
+ notes.push(`The configured check \`${savedCheck}\` is kept but does not look runnable (${validation.reason}); it is repaired through ordinary work, never replaced by the placeholder.`);
927
+ }
928
+ if (suppliedCheck && suppliedCheck !== savedCheck)
929
+ notes.push(`Check override for this run only: ${suppliedCheck}`);
812
930
  }
813
- if (state.saveCheck !== undefined) {
814
- const validation = await validateCheckCommand(root, state.saveCheck, context.env);
931
+ else if (suppliedCheck) {
932
+ saveCheck = suppliedCheck;
933
+ const validation = await validateCheckCommand(root, suppliedCheck, context.env);
815
934
  if (!validation.ok)
816
- unresolved.push(`Check \`${state.saveCheck}\`: ${validation.reason}`);
817
- changed.push(`Check: ${state.saveCheck}`);
818
- check = state.saveCheck;
935
+ notes.push(`The supplied check \`${suppliedCheck}\` does not look runnable yet (${validation.reason}).`);
936
+ changed.push(`Check: ${suppliedCheck}`);
819
937
  }
820
- else if (!existingValidation.ok) {
821
- unresolved.push(`Project check not configured${flag(context, 'defer-check') ? ' (deferred)' : ''} → \`ptbk init --check "<command>"\``);
938
+ else {
939
+ saveCheck = CHECK_PLACEHOLDER;
940
+ changed.push('Check: informational placeholder (real automated validation is not configured)');
822
941
  }
942
+ if (flag(context, 'defer-check'))
943
+ notes.push('--defer-check is no longer needed: init does not ask for a check.');
944
+ if (saveCheck !== undefined)
945
+ check = saveCheck;
823
946
  const instructions = await ensureProjectInstructions(root, project.name, check);
824
947
  if (instructions)
825
948
  createdPaths.push(instructions);
826
- // ptbk.json: selected harnesses, bootstrap routing, browser, check, traces, initialization marker.
827
- const rawConfig = (await readProjectConfigFile(project)).raw;
828
- const routingHarness = rawConfig.routing && typeof rawConfig.routing === 'object' ? rawConfig.routing.harness : undefined;
829
- const usableSelected = selected.filter((id) => state.inspections.get(id)?.usable);
830
- const settingsChanges = {};
831
- if (!routingHarness && (usableSelected[0] ?? selected[0]))
832
- settingsChanges['routing.harness'] = usableSelected[0] ?? selected[0];
833
- if (state.saveCheck !== undefined)
834
- settingsChanges['execution.check'] = state.saveCheck;
835
- if (!rawConfig.browser || option(context, 'browser-profile') !== undefined)
836
- settingsChanges['browser.profile'] = state.browserProfile ?? 'global';
837
- if (!rawConfig.browser || option(context, 'browser-mode') !== undefined)
838
- settingsChanges['browser.mode'] = state.browserMode ?? 'headless';
839
- for (const [key, value] of [
840
- ['traces.commit', tracePolicy.commit],
841
- ['traces.ignoreVideos', tracePolicy.ignoreVideos],
842
- ['traces.recordVideo', tracePolicy.recordVideo],
843
- ]) {
844
- if (getPath(rawConfig, key) === undefined)
845
- settingsChanges[key] = value;
846
- }
847
- const harnessChanges = {};
848
- const configured = configBefore ? enabledHarnesses(configBefore) : [];
849
- for (const id of selected)
850
- harnessChanges[id] = { enabled: true };
851
- for (const id of configured)
852
- if (!selected.includes(id))
853
- harnessChanges[id] = { enabled: false };
854
- if (await updateProjectConfig(project, { settings: settingsChanges, harnesses: harnessChanges }))
855
- createdPaths.push(project.configPath);
856
- // The first task: one ordinary Expert task referring to the preserved specification, never duplicated.
949
+ // ptbk.json in one write: settings, the simple initial tools (through the shared tool operations), synchronization
950
+ // choice and the initialization marker. A new configuration records the running engine version.
951
+ const file = await readProjectConfigFile(project);
857
952
  const timeZone = setting(await loadSettings(project, { env: context.env }), 'timezone');
858
- const marker = ((await readProjectConfigFile(project)).raw.initialized ?? {});
953
+ const marker = (file.raw.initialized ?? {});
859
954
  let firstTask;
860
955
  const tasksRoot = join(root, tasksDirectory);
861
956
  if (newPurpose && !marker.starterTask && !(await hasFirstTask({ root, tasksDirectory, timeZone }))) {
@@ -870,19 +965,65 @@ export async function initializeProject(context, options = {}) {
870
965
  createdPaths.push(draftPath);
871
966
  firstTask = { title, path: draftPath };
872
967
  }
968
+ const raw = structuredClone(file.raw);
969
+ if (!file.exists)
970
+ raw.promptbookVersion = ENGINE_VERSION;
971
+ const set = (key, value) => setPath(raw, key, serializeSettingValue(SETTING_SPEC_BY_KEY.get(key), value));
972
+ const routingHarness = getPath(raw, 'routing.harness');
973
+ const usableSelected = selected.filter((id) => state.inspections.get(id)?.usable);
974
+ if (routingHarness === undefined && (usableSelected[0] ?? selected[0]))
975
+ set('routing.harness', usableSelected[0] ?? selected[0]);
976
+ if (saveCheck !== undefined)
977
+ set('execution.check', saveCheck);
978
+ for (const [key, value] of [
979
+ ['traces.isCommitted', tracePolicy.commit],
980
+ ['traces.isVideoIgnored', tracePolicy.ignoreVideos],
981
+ ['traces.isVideoRecordingEnabled', tracePolicy.recordVideo],
982
+ ]) {
983
+ if (getPath(raw, key) === undefined)
984
+ set(key, value);
985
+ }
986
+ if (state.sync && (!state.sync.reused || raw.isAutoSync === undefined))
987
+ raw.isAutoSync = state.sync.enabled;
988
+ const existingTools = Array.isArray(raw.tools) ? raw.tools : [];
989
+ const typedTools = interpretConfig({ raw, exists: true, diagnostics: [] }).tools;
990
+ const operations = [];
991
+ // One simple browser from the familiar picker; advanced profiles/accounts are managed later.
992
+ const browserProfile = profileFromSpelling(state.browserProfile ?? 'global');
993
+ const firstBrowser = typedTools.find((tool) => tool.type === 'BROWSER');
994
+ if (!firstBrowser) {
995
+ operations.push({ kind: 'add', tool: { type: 'BROWSER', variant: 'CHROME', name: MIGRATED_BROWSER_NAME, profileSource: browserProfile.source, ...(browserProfile.path ? { profilePath: browserProfile.path } : {}), isHeadless: (state.browserMode ?? 'headless') !== 'headful' } });
996
+ }
997
+ else if (option(context, 'browser-profile') !== undefined || option(context, 'browser-mode') !== undefined) {
998
+ operations.push({ kind: 'modify', name: firstBrowser.name, changes: { ...(option(context, 'browser-profile') !== undefined ? { profileSource: browserProfile.source, profilePath: browserProfile.path ?? null } : {}), ...(option(context, 'browser-mode') !== undefined ? { isHeadless: state.browserMode !== 'headful' } : {}) } });
999
+ }
1000
+ // Each selected integration gets one native instance named after it (existing instances are kept as they are).
1001
+ for (const id of selected) {
1002
+ const variant = harnessVariantOf(id);
1003
+ const instances = typedTools.filter((tool) => tool.type === 'HARNESS' && tool.variant === variant);
1004
+ if (!instances.length)
1005
+ operations.push({ kind: 'add', tool: { type: 'HARNESS', variant, name: typedTools.some((tool) => tool.name === id) ? `${id}-native` : id, authentication: 'NATIVE' } });
1006
+ else if (!instances.some((tool) => tool.isEnabled))
1007
+ operations.push({ kind: 'enable', name: instances[0].name });
1008
+ }
1009
+ const configured = operations.length ? applyToolOperationsToConfig({ ...raw, tools: existingTools }, operations) : canonicalizeConfig(raw).raw;
873
1010
  const effectiveInstallation = installation === 'existing' ? (marker.installation ?? 'local') : installation;
874
1011
  if (firstTask || !marker.engineVersion || marker.installation !== effectiveInstallation) {
875
- const raw = (await readProjectConfigFile(project)).raw;
876
- raw.initialized = { ...raw.initialized, engineVersion: marker.engineVersion ?? ENGINE_VERSION, ...(firstTask ? { starterTask: firstTask.title } : {}), installation: effectiveInstallation };
877
- await writeFile(project.configPath, `${JSON.stringify(raw, null, 4)}\n`);
1012
+ configured.initialized = { ...configured.initialized, engineVersion: marker.engineVersion ?? ENGINE_VERSION, ...(firstTask ? { starterTask: firstTask.title } : {}), installation: effectiveInstallation };
1013
+ }
1014
+ const text = configText(configured);
1015
+ if (text !== file.text) {
1016
+ await writeFileAtomic(project.configPath, text);
878
1017
  createdPaths.push(project.configPath);
879
1018
  }
1019
+ for (const operation of operations)
1020
+ changed.push(operation.kind === 'add' ? `Tool ${operation.tool.name} (${operation.tool.type} ${operation.tool.variant})` : `Tool ${operation.name}: ${operation.kind}`);
880
1021
  for (const path of createdPaths) {
881
1022
  const label = toProjectRelative(root, path);
882
1023
  if (!changed.includes(label))
883
1024
  changed.push(label);
884
1025
  }
885
- // Commit authorized setup artifacts only (never unrelated changes), with the first task's ID.
1026
+ // Commit authorized setup artifacts only (never unrelated changes), with the first task's name.
886
1027
  let commitSha;
887
1028
  const uniquePaths = [...new Set(createdPaths)];
888
1029
  if (uniquePaths.length) {
@@ -922,6 +1063,26 @@ export async function initializeProject(context, options = {}) {
922
1063
  notes.push(`Parent repository changes left uncommitted: ${parentChanges.map((path) => toProjectRelative(state.nested.parentRoot, path) || '.').join(', ')}`);
923
1064
  if (!traceIgnore && !createdPaths.length)
924
1065
  alreadyReady.push(`Trace policy: ${traceIgnoreLines(tracePolicy).join(' ')}`);
1066
+ // The initial browser must actually be usable (inspected without launching it).
1067
+ const browserTool = interpretConfig({ raw: configured, exists: true, diagnostics: [] }).tools.find((tool) => tool.type === 'BROWSER' && tool.isEnabled);
1068
+ if (browserTool) {
1069
+ const browserReadiness = await inspectBrowserTool(project, browserTool, context.env);
1070
+ if (browserReadiness.usable)
1071
+ alreadyReady.push(`Browser ${browserTool.name}: ${browserReadiness.summary}`);
1072
+ else
1073
+ unresolved.push(`Browser ${browserTool.name}: ${browserReadiness.summary}${browserReadiness.nextAction ? ` → ${browserReadiness.nextAction}` : ''}`);
1074
+ }
1075
+ // Remote publication is a separate, honestly reported outcome after the committed local setup.
1076
+ let publication;
1077
+ if (state.sync?.enabled && state.sync.choice && commitSha !== undefined && !unresolved.length) {
1078
+ say('Setting up automatic synchronization…');
1079
+ publication = await publishProject(project, repository.repositoryRoot, state.sync.choice, context.env);
1080
+ (publication.ok ? changed : notes).push(publication.ok ? publication.message : `Remote publication did not succeed: ${publication.message} Local setup is complete; synchronization stays enabled and retries in the background (or rerun \`ptbk init\`).`);
1081
+ }
1082
+ else if (state.sync?.enabled && state.sync.choice && !commitSha && !unresolved.length && !uniquePaths.length) {
1083
+ publication = await publishProject(project, repository.repositoryRoot, state.sync.choice, context.env);
1084
+ (publication.ok ? alreadyReady : notes).push(publication.ok ? publication.message : `Remote publication did not succeed: ${publication.message}`);
1085
+ }
925
1086
  // Readiness from the saved settings the service will use.
926
1087
  project = projectAt(root);
927
1088
  const readiness = await assessReadiness(project, await loadSettings(project, { env: context.env }), context.env);
@@ -929,7 +1090,7 @@ export async function initializeProject(context, options = {}) {
929
1090
  if (!unresolved.some((item) => item.startsWith(problem.message.slice(0, 30))))
930
1091
  unresolved.push(`${problem.message} → ${problem.action}`);
931
1092
  const ready = readiness.ready && !unresolved.length;
932
- return { outcome: ready ? 'ready' : 'incomplete', project, changed, alreadyReady, unresolved, notes, commit: commitSha, firstTask };
1093
+ return { outcome: ready ? 'ready' : 'incomplete', project, changed, alreadyReady, unresolved, notes, commit: commitSha, firstTask, publication, synchronization: state.sync?.enabled ?? false };
933
1094
  }
934
1095
  /**
935
1096
  * Performs the ordinary background start with the init invocation's applicable overrides (one service, never a second
@@ -943,7 +1104,7 @@ async function startService(context, project) {
943
1104
  return { kind: 'reused', snapshot: await liveSnapshot(project, existing), note: `The ${existing.record.mode} service (PID ${existing.record.pid}) was already running and keeps its configuration.` };
944
1105
  }
945
1106
  const settings = await loadSettings(project, { env: context.env, cli: overrides });
946
- const persistent = setting(settings, 'service.persist');
1107
+ const persistent = setting(settings, 'service.isPersistent');
947
1108
  const launched = await launchDaemon(project, { cli: overrides, tasksDirectory: tasksOverride(context), persistent, env: context.env });
948
1109
  let snapshot = await liveSnapshot(project, launched.lookup);
949
1110
  const deadline = Date.now() + 10_000;