@promptbook/cli 0.112.0-121 β†’ 0.112.0-124

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 (55) hide show
  1. package/README.md +14 -14
  2. package/agents/default/developer.book +23 -0
  3. package/apps/agents-server/src/app/agents/[agentName]/api/user-chats/[chatId]/messages/route.ts +4 -11
  4. package/apps/agents-server/src/app/api/scrape/route.ts +18 -0
  5. package/apps/agents-server/src/tools/createAgentProgressTools.ts +6 -4
  6. package/apps/agents-server/src/utils/assertSafeUrl.ts +136 -0
  7. package/apps/agents-server/src/utils/authenticateUser.ts +2 -1
  8. package/apps/agents-server/src/utils/getCurrentUser.ts +2 -1
  9. package/apps/agents-server/src/utils/isAdminPasswordEqual.ts +28 -0
  10. package/apps/agents-server/src/utils/isUserGlobalAdmin.ts +3 -1
  11. package/apps/agents-server/src/utils/userChat/createRunUserChatJobPersistenceController.ts +21 -0
  12. package/apps/agents-server/src/utils/userChat/retryUserChatJob.ts +4 -0
  13. package/apps/agents-server/src/utils/userChat/userChatMessageLifecycle.ts +2 -0
  14. package/apps/agents-server/src/utils/userChat/userChatProgressCard.ts +288 -0
  15. package/esm/index.es.js +1430 -283
  16. package/esm/index.es.js.map +1 -1
  17. package/esm/scripts/run-codex-prompts/common/resolveAgentSystemMessage.d.ts +6 -0
  18. package/esm/scripts/run-codex-prompts/main/findUnwrittenPrompts.d.ts +19 -0
  19. package/esm/scripts/run-codex-prompts/main/runCodexPromptsServer.d.ts +27 -0
  20. package/esm/scripts/run-codex-prompts/main/runPromptRound.d.ts +2 -1
  21. package/esm/scripts/run-codex-prompts/server/coderServerHtml.d.ts +9 -0
  22. package/esm/scripts/run-codex-prompts/server/runCoderHttpServer.d.ts +23 -0
  23. package/esm/scripts/run-codex-prompts/server/updatePromptSection.d.ts +9 -0
  24. package/esm/src/cli/cli-commands/coder/ensureCoderDeveloperAgentFile.d.ts +25 -0
  25. package/esm/src/cli/cli-commands/coder/find-unwritten.d.ts +10 -0
  26. package/esm/src/cli/cli-commands/coder/initializeCoderProjectConfiguration.d.ts +2 -0
  27. package/esm/src/cli/cli-commands/coder/server.d.ts +13 -0
  28. package/esm/src/version.d.ts +1 -1
  29. package/package.json +1 -1
  30. package/src/cli/cli-commands/coder/ensureCoderDeveloperAgentFile.ts +79 -0
  31. package/src/cli/cli-commands/coder/find-unwritten.ts +58 -0
  32. package/src/cli/cli-commands/coder/getDefaultCoderPackageJsonScripts.ts +1 -1
  33. package/src/cli/cli-commands/coder/init.ts +6 -1
  34. package/src/cli/cli-commands/coder/initializeCoderProjectConfiguration.ts +10 -0
  35. package/src/cli/cli-commands/coder/printInitializationSummary.ts +3 -0
  36. package/src/cli/cli-commands/coder/run.ts +26 -11
  37. package/src/cli/cli-commands/coder/server.ts +239 -0
  38. package/src/cli/cli-commands/coder.ts +6 -0
  39. package/src/other/templates/getTemplatesPipelineCollection.ts +727 -1105
  40. package/src/version.ts +2 -2
  41. package/src/versions.txt +3 -1
  42. package/umd/index.umd.js +1431 -284
  43. package/umd/index.umd.js.map +1 -1
  44. package/umd/scripts/run-codex-prompts/common/resolveAgentSystemMessage.d.ts +6 -0
  45. package/umd/scripts/run-codex-prompts/main/findUnwrittenPrompts.d.ts +19 -0
  46. package/umd/scripts/run-codex-prompts/main/runCodexPromptsServer.d.ts +27 -0
  47. package/umd/scripts/run-codex-prompts/main/runPromptRound.d.ts +2 -1
  48. package/umd/scripts/run-codex-prompts/server/coderServerHtml.d.ts +9 -0
  49. package/umd/scripts/run-codex-prompts/server/runCoderHttpServer.d.ts +23 -0
  50. package/umd/scripts/run-codex-prompts/server/updatePromptSection.d.ts +9 -0
  51. package/umd/src/cli/cli-commands/coder/ensureCoderDeveloperAgentFile.d.ts +25 -0
  52. package/umd/src/cli/cli-commands/coder/find-unwritten.d.ts +10 -0
  53. package/umd/src/cli/cli-commands/coder/initializeCoderProjectConfiguration.d.ts +2 -0
  54. package/umd/src/cli/cli-commands/coder/server.d.ts +13 -0
  55. package/umd/src/version.d.ts +1 -1
package/esm/index.es.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import colors from 'colors';
2
2
  import commander, { Option } from 'commander';
3
3
  import _spaceTrim, { spaceTrim as spaceTrim$1 } from 'spacetrim';
4
- import { writeFile, stat, mkdir, readFile, rm, cp, lstat, symlink, readdir, unlink, appendFile, rename, access, constants, watch, rmdir } from 'fs/promises';
4
+ import { writeFile, stat, mkdir, readFile, rm, cp, lstat, symlink, readdir, unlink, appendFile, rename, copyFile, access, constants, watch, rmdir } from 'fs/promises';
5
5
  import { join, resolve, dirname, relative, basename, delimiter, isAbsolute, extname } from 'path';
6
6
  import { spawn } from 'child_process';
7
7
  import { createHash, randomBytes } from 'crypto';
@@ -20,7 +20,7 @@ import { JSDOM } from 'jsdom';
20
20
  import CryptoJS from 'crypto-js';
21
21
  import showdown from 'showdown';
22
22
  import glob from 'glob-promise';
23
- import http from 'http';
23
+ import http, { createServer } from 'http';
24
24
  import express from 'express';
25
25
  import { Server } from 'socket.io';
26
26
  import * as OpenApiValidator from 'express-openapi-validator';
@@ -31,14 +31,14 @@ import Anthropic from '@anthropic-ai/sdk';
31
31
  import Bottleneck from 'bottleneck';
32
32
  import { OpenAIClient, AzureKeyCredential } from '@azure/openai';
33
33
  import { Subject, BehaviorSubject } from 'rxjs';
34
- import { lookup, extension } from 'mime-types';
35
- import papaparse from 'papaparse';
36
34
  import { fileSearchTool, tool, Agent as Agent$1, webSearchTool, run, setDefaultOpenAIClient, setDefaultOpenAIKey } from '@openai/agents';
37
35
  import OpenAI from 'openai';
38
36
  import * as ts from 'typescript';
39
37
  import ignore from 'ignore';
40
38
  import * as readline from 'readline';
41
39
  import { emitKeypressEvents, clearLine, cursorTo, createInterface } from 'readline';
40
+ import { lookup, extension } from 'mime-types';
41
+ import papaparse from 'papaparse';
42
42
  import { pathToFileURL } from 'url';
43
43
  import { EventEmitter } from 'events';
44
44
  import { Client } from 'pg';
@@ -58,7 +58,7 @@ const BOOK_LANGUAGE_VERSION = '2.0.0';
58
58
  * @generated
59
59
  * @see https://github.com/webgptorg/promptbook
60
60
  */
61
- const PROMPTBOOK_ENGINE_VERSION = '0.112.0-121';
61
+ const PROMPTBOOK_ENGINE_VERSION = '0.112.0-124';
62
62
  /**
63
63
  * TODO: string_promptbook_version should be constrained to the all versions of Promptbook engine
64
64
  * Note: [πŸ’ž] Ignore a discrepancy between file name and entity name
@@ -2176,7 +2176,7 @@ function getDefaultBookLanguageManualContent() {
2176
2176
  */
2177
2177
  async function ensureCoderMarkdownFile(projectPath, relativeFilePath, fileContent) {
2178
2178
  const absoluteFilePath = join(projectPath, relativeFilePath);
2179
- if (await isExistingFile$2(absoluteFilePath)) {
2179
+ if (await isExistingFile$3(absoluteFilePath)) {
2180
2180
  return 'unchanged';
2181
2181
  }
2182
2182
  await writeFile(absoluteFilePath, `${fileContent}\n`, 'utf-8');
@@ -2185,7 +2185,7 @@ async function ensureCoderMarkdownFile(projectPath, relativeFilePath, fileConten
2185
2185
  /**
2186
2186
  * Checks whether a path exists and is a file.
2187
2187
  */
2188
- async function isExistingFile$2(path) {
2188
+ async function isExistingFile$3(path) {
2189
2189
  try {
2190
2190
  return (await stat(path)).isFile();
2191
2191
  }
@@ -4016,6 +4016,21 @@ function announcePauseTargetLabel(nextPauseTargetLabel) {
4016
4016
  function resetPauseTargetLabel() {
4017
4017
  setPauseTargetLabel(DEFAULT_PAUSE_TARGET_LABEL);
4018
4018
  }
4019
+ /**
4020
+ * Requests a pause from an external controller (e.g. the Ink UI).
4021
+ */
4022
+ function requestPause() {
4023
+ if (pauseState === 'RUNNING') {
4024
+ setPauseState('PAUSING');
4025
+ }
4026
+ }
4027
+ /**
4028
+ * Resumes execution from an external controller after a pause.
4029
+ */
4030
+ function requestResume() {
4031
+ setPauseState('RUNNING');
4032
+ resetPauseTargetLabel();
4033
+ }
4019
4034
 
4020
4035
  /**
4021
4036
  * Just says that the variable is not used but should be kept
@@ -38608,6 +38623,48 @@ function $initializeCoderFindRefactorCandidatesCommand(program) {
38608
38623
  // Note: [🟑] Code for CLI command [find-refactor-candidates](src/cli/cli-commands/coder/find-refactor-candidates.ts) should never be published outside of `@promptbook/cli`
38609
38624
  // Note: [πŸ’ž] Ignore a discrepancy between file name and entity name
38610
38625
 
38626
+ /**
38627
+ * Initializes `coder find-unwritten` command for Promptbook CLI utilities
38628
+ *
38629
+ * Note: `$` is used to indicate that this function is not a pure function - it registers a command in the CLI
38630
+ *
38631
+ * @private internal function of `promptbookCli`
38632
+ */
38633
+ function $initializeCoderFindUnwrittenCommand(program) {
38634
+ const command = program.command('find-unwritten');
38635
+ command.description('List all prompt sections that still need to be authored (contain @@@ placeholder)');
38636
+ command.option('--priority <minimum-priority>', 'Filter prompts by minimum priority level', parseIntOption$2, 0);
38637
+ command.action(handleActionErrors(async (cliOptions) => {
38638
+ const { priority = 0 } = cliOptions;
38639
+ // Note: Import the function dynamically to avoid loading heavy dependencies until needed
38640
+ const { findUnwrittenPrompts } = await Promise.resolve().then(function () { return findUnwrittenPrompts$1; });
38641
+ try {
38642
+ await findUnwrittenPrompts({ priority });
38643
+ }
38644
+ catch (error) {
38645
+ assertsError(error);
38646
+ console.error(colors.bgRed(`${error.name}`));
38647
+ console.error(colors.red(error.stack || error.message));
38648
+ return process.exit(1);
38649
+ }
38650
+ return process.exit(0);
38651
+ }));
38652
+ }
38653
+ /**
38654
+ * Parses an integer option value
38655
+ *
38656
+ * @private internal utility of `coder find-unwritten` command
38657
+ */
38658
+ function parseIntOption$2(value) {
38659
+ const parsed = parseInt(value, 10);
38660
+ if (isNaN(parsed)) {
38661
+ throw new Error(`Invalid number: ${value}`);
38662
+ }
38663
+ return parsed;
38664
+ }
38665
+ // Note: [🟑] Code for CLI command [find-unwritten](src/cli/cli-commands/coder/find-unwritten.ts) should never be published outside of `@promptbook/cli`
38666
+ // Note: [πŸ’ž] Ignore a discrepancy between file name and entity name
38667
+
38611
38668
  /**
38612
38669
  * Relative path to the root prompts directory used by Promptbook coder utilities.
38613
38670
  *
@@ -38700,7 +38757,7 @@ async function ensureDefaultCoderPromptTemplateFiles(projectPath) {
38700
38757
  const ensuredTemplateFiles = [];
38701
38758
  for (const definition of DEFAULT_CODER_PROJECT_PROMPT_TEMPLATE_DEFINITIONS) {
38702
38759
  const absoluteTemplatePath = join(projectPath, definition.relativeFilePath);
38703
- if (await isExistingFile$1(absoluteTemplatePath)) {
38760
+ if (await isExistingFile$2(absoluteTemplatePath)) {
38704
38761
  ensuredTemplateFiles.push({
38705
38762
  id: definition.id,
38706
38763
  relativeFilePath: definition.relativeFilePath,
@@ -38814,7 +38871,7 @@ function isNodeJsErrorWithCode(error, code) {
38814
38871
  /**
38815
38872
  * Checks whether a path exists and is a file.
38816
38873
  */
38817
- async function isExistingFile$1(path) {
38874
+ async function isExistingFile$2(path) {
38818
38875
  try {
38819
38876
  return (await stat(path)).isFile();
38820
38877
  }
@@ -39087,6 +39144,70 @@ function formatInlineCodeList(values) {
39087
39144
  // Note: [🟑] Code for coder AGENT_CODING file boilerplate [agentCodingFile](src/cli/cli-commands/coder/agentCodingFile.ts) should never be published outside of `@promptbook/cli`
39088
39145
  // Note: [πŸ’ž] Ignore a discrepancy between file name and exported helper names
39089
39146
 
39147
+ /**
39148
+ * Relative directory path for agents initialized by `ptbk coder init`.
39149
+ *
39150
+ * @private internal utility of `coder init` command
39151
+ */
39152
+ const CODER_AGENTS_DIRECTORY_PATH = 'agents';
39153
+ /**
39154
+ * Relative file path to the default developer agent initialized by `ptbk coder init`.
39155
+ *
39156
+ * @private internal utility of `coder init` command
39157
+ */
39158
+ const CODER_DEVELOPER_AGENT_FILE_PATH = 'agents/developer.book';
39159
+ /**
39160
+ * Source file path of the bundled developer agent inside the Promptbook repository.
39161
+ *
39162
+ * @private internal utility of `coder init` command
39163
+ */
39164
+ const DEFAULT_CODER_DEVELOPER_AGENT_SOURCE_FILE_PATH = 'agents/default/developer.book';
39165
+ /**
39166
+ * Ensures the default developer agent exists in the initialized project.
39167
+ *
39168
+ * @private function of `initializeCoderProjectConfiguration`
39169
+ */
39170
+ async function ensureCoderDeveloperAgentFile(projectPath) {
39171
+ const absoluteFilePath = join(projectPath, CODER_DEVELOPER_AGENT_FILE_PATH);
39172
+ if (await isExistingFile$1(absoluteFilePath)) {
39173
+ return 'unchanged';
39174
+ }
39175
+ await copyFile(await resolveDefaultCoderDeveloperAgentFilePath(), absoluteFilePath);
39176
+ return 'created';
39177
+ }
39178
+ /**
39179
+ * Resolves the bundled developer agent from a source checkout or generated CLI package.
39180
+ */
39181
+ async function resolveDefaultCoderDeveloperAgentFilePath() {
39182
+ const candidates = [
39183
+ join(__dirname, '..', DEFAULT_CODER_DEVELOPER_AGENT_SOURCE_FILE_PATH),
39184
+ join(__dirname, '..', '..', '..', '..', DEFAULT_CODER_DEVELOPER_AGENT_SOURCE_FILE_PATH),
39185
+ ];
39186
+ for (const candidate of candidates) {
39187
+ if (await isExistingFile$1(candidate)) {
39188
+ return candidate;
39189
+ }
39190
+ }
39191
+ throw new NotAllowed(spaceTrim$1(`
39192
+ Cannot find the bundled Promptbook developer agent.
39193
+
39194
+ Checked:
39195
+ ${candidates.map((candidate) => `- \`${candidate}\``).join('\n')}
39196
+ `));
39197
+ }
39198
+ /**
39199
+ * Checks whether a path exists and is a file.
39200
+ */
39201
+ async function isExistingFile$1(path) {
39202
+ try {
39203
+ return (await stat(path)).isFile();
39204
+ }
39205
+ catch (_a) {
39206
+ return false;
39207
+ }
39208
+ }
39209
+ // Note: [🟑] Code for coder init developer agent bootstrapping [ensureCoderDeveloperAgentFile](src/cli/cli-commands/coder/ensureCoderDeveloperAgentFile.ts) should never be published outside of `@promptbook/cli`
39210
+
39090
39211
  /**
39091
39212
  * Fallback `.env` content used when no required variables need to be appended.
39092
39213
  */
@@ -39163,7 +39284,7 @@ async function ensureCoderGitignoreFile(projectPath) {
39163
39284
  */
39164
39285
  const DEFAULT_CODER_PACKAGE_JSON_SCRIPTS = {
39165
39286
  'coder:generate-boilerplates': 'ptbk coder generate-boilerplates --template ./prompts/templates/common.md',
39166
- 'coder:run': 'ptbk coder run --harness openai-codex --model gpt-5.4 --thinking-level xhigh --context AGENTS.md --no-wait',
39287
+ 'coder:run': 'ptbk coder run --harness openai-codex --model gpt-5.4 --thinking-level xhigh --agent agents/developer.book --context AGENTS.md --no-wait',
39167
39288
  // 'coder:find-refactor-candidates': 'ptbk coder find-refactor-candidates',
39168
39289
  'coder:verify': 'ptbk coder verify',
39169
39290
  };
@@ -39409,6 +39530,8 @@ async function initializeCoderProjectConfiguration(projectPath) {
39409
39530
  const promptsDoneDirectoryStatus = await ensureDirectory(projectPath, PROMPTS_DONE_DIRECTORY_PATH);
39410
39531
  const promptsTemplatesDirectoryStatus = await ensureDirectory(projectPath, PROMPTS_TEMPLATES_DIRECTORY_PATH);
39411
39532
  const promptTemplateFileStatuses = await ensureDefaultCoderPromptTemplateFiles(projectPath);
39533
+ const agentsDirectoryStatus = await ensureDirectory(projectPath, CODER_AGENTS_DIRECTORY_PATH);
39534
+ const developerAgentFileStatus = await ensureCoderDeveloperAgentFile(projectPath);
39412
39535
  const agentsFileStatus = await ensureCoderMarkdownFile(projectPath, AGENTS_FILE_PATH, getDefaultCoderAgentsFileContent());
39413
39536
  const agentCodingFileStatus = await ensureCoderMarkdownFile(projectPath, AGENT_CODING_FILE_PATH, getDefaultCoderAgentCodingFileContent({
39414
39537
  packageJsonScripts: getDefaultCoderPackageJsonScripts(),
@@ -39422,6 +39545,8 @@ async function initializeCoderProjectConfiguration(projectPath) {
39422
39545
  promptsDoneDirectoryStatus,
39423
39546
  promptsTemplatesDirectoryStatus,
39424
39547
  promptTemplateFileStatuses,
39548
+ agentsDirectoryStatus,
39549
+ developerAgentFileStatus,
39425
39550
  agentsFileStatus,
39426
39551
  agentCodingFileStatus,
39427
39552
  envFileStatus,
@@ -39446,6 +39571,8 @@ function printInitializationSummary(summary) {
39446
39571
  for (const templateFileStatus of summary.promptTemplateFileStatuses) {
39447
39572
  printInitializationStatusLine(formatDisplayPath(templateFileStatus.relativeFilePath), templateFileStatus.status);
39448
39573
  }
39574
+ printInitializationStatusLine('agents/', summary.agentsDirectoryStatus);
39575
+ printInitializationStatusLine(CODER_DEVELOPER_AGENT_FILE_PATH, summary.developerAgentFileStatus);
39449
39576
  printInitializationStatusLine(AGENTS_FILE_PATH, summary.agentsFileStatus);
39450
39577
  printInitializationStatusLine(AGENT_CODING_FILE_PATH, summary.agentCodingFileStatus);
39451
39578
  printInitializationStatusLine('.env', summary.envFileStatus);
@@ -39502,6 +39629,7 @@ function $initializeCoderInitCommand(program) {
39502
39629
  - prompts/
39503
39630
  - prompts/done/
39504
39631
  ${listDefaultCoderProjectPromptTemplateDisplayPaths()}
39632
+ - ${CODER_DEVELOPER_AGENT_FILE_PATH}
39505
39633
  - ${AGENTS_FILE_PATH}
39506
39634
  - ${AGENT_CODING_FILE_PATH}
39507
39635
  - .gitignore
@@ -39514,8 +39642,10 @@ function $initializeCoderInitCommand(program) {
39514
39642
  - CODING_AGENT_GIT_SIGNING_KEY
39515
39643
  `));
39516
39644
  command.action(handleActionErrors(async () => {
39517
- const summary = await initializeCoderProjectConfiguration(process.cwd());
39645
+ const projectPath = process.cwd();
39646
+ const summary = await initializeCoderProjectConfiguration(projectPath);
39518
39647
  printInitializationSummary(summary);
39648
+ await generatePromptBoilerplate({ projectPath, filesCount: 5 });
39519
39649
  }));
39520
39650
  }
39521
39651
  /**
@@ -39607,11 +39737,12 @@ function $initializeCoderRunCommand(program) {
39607
39737
  `));
39608
39738
  command.option('--dry-run', 'Print unwritten prompts without executing', false);
39609
39739
  addPromptRunnerSelectionOptions(command);
39740
+ command.option('--agent <agent-book-path>', 'Path to a .book file whose compiled system message is prepended to each coding prompt');
39610
39741
  command.option('--context <context-or-file>', 'Append extra instructions either inline or from a file path relative to the current project');
39611
39742
  command.option('--test <test-command...>', 'Run a verification command after each prompt; quote it when the command itself contains top-level flags');
39612
39743
  command.option('--preserve-logs', 'Keep generated temp prompt/log artifacts after successful rounds for debugging and analytics', false);
39613
39744
  addPromptRunnerExecutionOptions(command);
39614
- command.option('--priority <minimum-priority>', 'Filter prompts by minimum priority level', parseIntOption, 0);
39745
+ command.option('--priority <minimum-priority>', 'Filter prompts by minimum priority level', parseIntOption$1, 0);
39615
39746
  command.option('--wait [duration]', spaceTrim$1(`
39616
39747
  Wait between prompt rounds.
39617
39748
  Without a value (default): waits for user confirmation before each prompt (interactive mode).
@@ -39622,8 +39753,8 @@ function $initializeCoderRunCommand(program) {
39622
39753
  command.option('--auto-migrate', 'Run testing-server database migrations automatically after each successfully processed prompt');
39623
39754
  command.option('--allow-destructive-auto-migrate', 'Allow auto-migrate even when heuristic SQL safety check flags destructive pending migrations');
39624
39755
  command.action(handleActionErrors(async (cliOptions) => {
39625
- const { dryRun, context, test, preserveLogs, priority, wait, autoMigrate, allowDestructiveAutoMigrate } = cliOptions;
39626
- const testCommand = normalizeCommandOptionValue(test);
39756
+ const { dryRun, agent, context, test, preserveLogs, priority, wait, autoMigrate, allowDestructiveAutoMigrate, } = cliOptions;
39757
+ const testCommand = normalizeCommandOptionValue$1(test);
39627
39758
  const runnerOptions = normalizePromptRunnerCliOptions(cliOptions, {
39628
39759
  isAgentRequired: !dryRun,
39629
39760
  });
@@ -39648,6 +39779,7 @@ function $initializeCoderRunCommand(program) {
39648
39779
  ignoreGitChanges: runnerOptions.ignoreGitChanges,
39649
39780
  agentName: runnerOptions.agentName,
39650
39781
  model: runnerOptions.model,
39782
+ agent,
39651
39783
  context,
39652
39784
  testCommand,
39653
39785
  preserveLogs,
@@ -39681,7 +39813,7 @@ function $initializeCoderRunCommand(program) {
39681
39813
  *
39682
39814
  * @private internal utility of `coder run` command
39683
39815
  */
39684
- function parseIntOption(value) {
39816
+ function parseIntOption$1(value) {
39685
39817
  const parsed = parseInt(value, 10);
39686
39818
  if (isNaN(parsed)) {
39687
39819
  throw new Error(`Invalid number: ${value}`);
@@ -39693,7 +39825,7 @@ function parseIntOption(value) {
39693
39825
  *
39694
39826
  * @private internal utility of `coder run` command
39695
39827
  */
39696
- function normalizeCommandOptionValue(value) {
39828
+ function normalizeCommandOptionValue$1(value) {
39697
39829
  if (value === undefined) {
39698
39830
  return undefined;
39699
39831
  }
@@ -39708,6 +39840,157 @@ function normalizeCommandOptionValue(value) {
39708
39840
  // Note: [🟑] Code for CLI command [run](src/cli/cli-commands/coder/run.ts) should never be published outside of `@promptbook/cli`
39709
39841
  // Note: [πŸ’ž] Ignore a discrepancy between file name and entity name
39710
39842
 
39843
+ /**
39844
+ * Default port used by `ptbk coder server`.
39845
+ *
39846
+ * @private internal constant of `ptbk coder server`
39847
+ */
39848
+ const DEFAULT_CODER_SERVER_PORT = '4441';
39849
+ /**
39850
+ * Initializes `coder server` command for Promptbook CLI utilities.
39851
+ *
39852
+ * Runs the same prompt processing logic as `ptbk coder run` but keeps the process alive
39853
+ * and serves a kanban web UI on the configured port.
39854
+ *
39855
+ * Note: `$` is used to indicate that this function is not a pure function - it registers a command in the CLI
39856
+ *
39857
+ * @private internal function of `promptbookCli`
39858
+ */
39859
+ function $initializeCoderServerCommand(program) {
39860
+ const command = program.command('server');
39861
+ command.description(spaceTrim$1(`
39862
+ Start a coder server that watches for prompts and serves a kanban web UI
39863
+
39864
+ ${PROMPT_RUNNER_DESCRIPTION}
39865
+
39866
+ Features:
39867
+ - Runs the same prompt processing as \`ptbk coder run\`
39868
+ - Does not exit when all prompts are done; polls for new prompt files instead
39869
+ - Serves a kanban board at http://localhost:<port> for visual progress tracking
39870
+ - Allows editing prompt files directly from the browser (Trello-style)
39871
+ - Play / pause button in the browser stays in sync with the CLI pause state
39872
+ - Press "p" in the terminal to pause / resume (same as \`ptbk coder run\`)
39873
+ `));
39874
+ command.addOption(new Option('--port <port>', 'Port to start the coder server on')
39875
+ .env('PTBK_CODER_SERVER_PORT')
39876
+ .default(DEFAULT_CODER_SERVER_PORT));
39877
+ command.option('--dry-run', 'Print unwritten prompts without executing', false);
39878
+ addPromptRunnerSelectionOptions(command);
39879
+ command.option('--agent <agent-book-path>', 'Path to a .book file whose compiled system message is prepended to each coding prompt');
39880
+ command.option('--context <context-or-file>', 'Append extra instructions either inline or from a file path relative to the current project');
39881
+ command.option('--test <test-command...>', 'Run a verification command after each prompt; quote it when the command itself contains top-level flags');
39882
+ command.option('--preserve-logs', 'Keep generated temp prompt/log artifacts after successful rounds for debugging and analytics', false);
39883
+ addPromptRunnerExecutionOptions(command);
39884
+ command.option('--priority <minimum-priority>', 'Filter prompts by minimum priority level', parseIntOption, 0);
39885
+ command.option('--wait [duration]', spaceTrim$1(`
39886
+ Wait between prompt rounds.
39887
+ Without a value (default): waits for user confirmation before each prompt (interactive mode).
39888
+ With a duration like 1h, 30m, 5s: waits that long between prompts to avoid hitting rate limits of the harness.
39889
+ `), true);
39890
+ command.option('--no-wait', 'Skip all waiting between prompts and run non-interactively');
39891
+ command.option('--auto-migrate', 'Run testing-server database migrations automatically after each successfully processed prompt');
39892
+ command.option('--allow-destructive-auto-migrate', 'Allow auto-migrate even when heuristic SQL safety check flags destructive pending migrations');
39893
+ command.action(handleActionErrors(async (cliOptions) => {
39894
+ const { port: rawPort, dryRun, agent, context, test, preserveLogs, priority, wait, autoMigrate, allowDestructiveAutoMigrate, } = cliOptions;
39895
+ const port = parseCoderServerPort(rawPort);
39896
+ const testCommand = normalizeCommandOptionValue(test);
39897
+ const runnerOptions = normalizePromptRunnerCliOptions(cliOptions, {
39898
+ isAgentRequired: !dryRun,
39899
+ });
39900
+ // [1] Parse the --wait option (same logic as `coder run`)
39901
+ let waitForUser = false;
39902
+ let waitBetweenPrompts = 0;
39903
+ if (wait === true) {
39904
+ waitForUser = true;
39905
+ }
39906
+ else if (typeof wait === 'string' && wait !== '') {
39907
+ waitBetweenPrompts = parseDuration(wait);
39908
+ }
39909
+ const runOptions = {
39910
+ port,
39911
+ dryRun,
39912
+ waitForUser,
39913
+ waitBetweenPrompts,
39914
+ noCommit: runnerOptions.noCommit,
39915
+ ignoreGitChanges: runnerOptions.ignoreGitChanges,
39916
+ agentName: runnerOptions.agentName,
39917
+ model: runnerOptions.model,
39918
+ agent,
39919
+ context,
39920
+ testCommand,
39921
+ preserveLogs,
39922
+ noUi: runnerOptions.noUi,
39923
+ thinkingLevel: runnerOptions.thinkingLevel,
39924
+ priority,
39925
+ normalizeLineEndings: runnerOptions.normalizeLineEndings,
39926
+ allowCredits: runnerOptions.allowCredits,
39927
+ autoMigrate,
39928
+ allowDestructiveAutoMigrate,
39929
+ autoPush: runnerOptions.autoPush,
39930
+ autoPull: runnerOptions.autoPull,
39931
+ };
39932
+ // Note: Import dynamically to avoid loading heavy dependencies until needed
39933
+ const { runCodexPromptsServer } = await Promise.resolve().then(function () { return runCodexPromptsServer$1; });
39934
+ try {
39935
+ await runCodexPromptsServer(runOptions);
39936
+ }
39937
+ catch (error) {
39938
+ assertsError(error);
39939
+ console.error(colors.bgRed(`${error.name}`));
39940
+ console.error(colors.red(error.stack || error.message));
39941
+ return process.exit(1);
39942
+ }
39943
+ return process.exit(0);
39944
+ }));
39945
+ }
39946
+ /**
39947
+ * Parses and validates the coder server port number.
39948
+ *
39949
+ * @private internal utility of `coder server` command
39950
+ */
39951
+ function parseCoderServerPort(rawPort) {
39952
+ const port = Number.parseInt(rawPort, 10);
39953
+ if (!Number.isInteger(port) || port <= 0 || port > NETWORK_LIMITS.MAX_PORT) {
39954
+ throw new NotAllowed(spaceTrim$1(`
39955
+ Invalid coder server port: \`${rawPort}\`.
39956
+
39957
+ Use \`--port\` or \`PTBK_CODER_SERVER_PORT\` with an integer between \`1\` and \`${NETWORK_LIMITS.MAX_PORT}\`.
39958
+ `));
39959
+ }
39960
+ return port;
39961
+ }
39962
+ /**
39963
+ * Parses an integer option value.
39964
+ *
39965
+ * @private internal utility of `coder server` command
39966
+ */
39967
+ function parseIntOption(value) {
39968
+ const parsed = parseInt(value, 10);
39969
+ if (isNaN(parsed)) {
39970
+ throw new Error(`Invalid number: ${value}`);
39971
+ }
39972
+ return parsed;
39973
+ }
39974
+ /**
39975
+ * Joins one Commander option that may be parsed either as a single string or a variadic token array.
39976
+ *
39977
+ * @private internal utility of `coder server` command
39978
+ */
39979
+ function normalizeCommandOptionValue(value) {
39980
+ if (value === undefined) {
39981
+ return undefined;
39982
+ }
39983
+ const parts = Array.isArray(value) ? value : [value];
39984
+ const normalizedValue = parts
39985
+ .map((part) => part.trim())
39986
+ .filter(Boolean)
39987
+ .join(' ')
39988
+ .trim();
39989
+ return normalizedValue === '' ? undefined : normalizedValue;
39990
+ }
39991
+ // Note: [🟑] Code for CLI command [server](src/cli/cli-commands/coder/server.ts) should never be published outside of `@promptbook/cli`
39992
+ // Note: [πŸ’ž] Ignore a discrepancy between file name and entity name
39993
+
39711
39994
  /**
39712
39995
  * Initializes `coder verify` command for Promptbook CLI utilities
39713
39996
  *
@@ -39779,7 +40062,9 @@ function $initializeCoderCommand(program) {
39779
40062
  - init: Initialize coder configuration in current project
39780
40063
  - generate-boilerplates: Generate prompt boilerplate files
39781
40064
  - find-refactor-candidates: Find files that need refactoring
40065
+ - find-unwritten: List prompt sections that still need to be authored
39782
40066
  - run: Run coding prompts with AI agents
40067
+ - server: Start a long-running coder server with a kanban web UI
39783
40068
  - verify: Verify completed prompts
39784
40069
  - find-fresh-emoji-tags: Find unused emoji tags
39785
40070
  `));
@@ -39787,7 +40072,9 @@ function $initializeCoderCommand(program) {
39787
40072
  $initializeCoderInitCommand(coderCommand);
39788
40073
  $initializeCoderGenerateBoilerplatesCommand(coderCommand);
39789
40074
  $initializeCoderFindRefactorCandidatesCommand(coderCommand);
40075
+ $initializeCoderFindUnwrittenCommand(coderCommand);
39790
40076
  $initializeCoderRunCommand(coderCommand);
40077
+ $initializeCoderServerCommand(coderCommand);
39791
40078
  $initializeCoderVerifyCommand(coderCommand);
39792
40079
  $initializeCoderFindFreshEmojiTagCommand(coderCommand);
39793
40080
  // If no subcommand is provided, show help
@@ -68623,6 +68910,303 @@ var findRefactorCandidates$1 = /*#__PURE__*/Object.freeze({
68623
68910
  findRefactorCandidates: findRefactorCandidates
68624
68911
  });
68625
68912
 
68913
+ /**
68914
+ * Checks whether a prompt section meets the minimum priority threshold.
68915
+ */
68916
+ function hasSufficientPriority(section, minimumPriority) {
68917
+ return section.priority >= minimumPriority;
68918
+ }
68919
+
68920
+ /**
68921
+ * Trims leading and trailing empty lines.
68922
+ */
68923
+ function trimEmptyEdges(lines) {
68924
+ let start = 0;
68925
+ while (start < lines.length) {
68926
+ const startLine = lines[start];
68927
+ if (startLine !== undefined && startLine.trim() !== '') {
68928
+ break;
68929
+ }
68930
+ start += 1;
68931
+ }
68932
+ let end = lines.length - 1;
68933
+ while (end >= start) {
68934
+ const endLine = lines[end];
68935
+ if (endLine !== undefined && endLine.trim() !== '') {
68936
+ break;
68937
+ }
68938
+ end -= 1;
68939
+ }
68940
+ return lines.slice(start, end + 1);
68941
+ }
68942
+
68943
+ /**
68944
+ * Extracts prompt lines without the status marker.
68945
+ */
68946
+ function buildPromptLinesWithoutStatus(file, section) {
68947
+ const lines = file.lines.slice(section.startLine, section.endLine + 1);
68948
+ if (section.statusLineIndex !== undefined) {
68949
+ const relativeIndex = section.statusLineIndex - section.startLine;
68950
+ if (relativeIndex >= 0 && relativeIndex < lines.length) {
68951
+ lines.splice(relativeIndex, 1);
68952
+ }
68953
+ }
68954
+ return trimEmptyEdges(lines);
68955
+ }
68956
+
68957
+ /**
68958
+ * Builds the prompt text sent to the agent runner.
68959
+ */
68960
+ function buildCodexPrompt(file, section) {
68961
+ const lines = buildPromptLinesWithoutStatus(file, section);
68962
+ for (let i = 0; i < lines.length; i++) {
68963
+ const line = lines[i];
68964
+ if (line === undefined || line.trim() === '') {
68965
+ continue;
68966
+ }
68967
+ lines[i] = line.replace(/^\[[^\]]+\]\s*/, '');
68968
+ break;
68969
+ }
68970
+ return lines.join(file.eol);
68971
+ }
68972
+
68973
+ /**
68974
+ * Checks whether a prompt section still needs authoring (contains "@@@").
68975
+ */
68976
+ function isPromptToBeWritten(file, section) {
68977
+ return buildCodexPrompt(file, section).includes('@@@');
68978
+ }
68979
+
68980
+ /**
68981
+ * Lists todo prompts across all files.
68982
+ */
68983
+ function listTodoPrompts(files) {
68984
+ const prompts = [];
68985
+ for (const file of files) {
68986
+ for (const section of file.sections) {
68987
+ if (section.status === 'todo') {
68988
+ prompts.push({ file, section });
68989
+ }
68990
+ }
68991
+ }
68992
+ return prompts;
68993
+ }
68994
+
68995
+ /**
68996
+ * Lists todo prompts that still contain authoring placeholders.
68997
+ */
68998
+ function listPromptsToBeWritten(files, minimumPriority = 0) {
68999
+ return listTodoPrompts(files).filter((prompt) => isPromptToBeWritten(prompt.file, prompt.section) && hasSufficientPriority(prompt.section, minimumPriority));
69000
+ }
69001
+
69002
+ /**
69003
+ * Parses a prompt markdown file into sections and metadata.
69004
+ */
69005
+ function parsePromptFile(filePath, content) {
69006
+ var _a, _b;
69007
+ const eol = content.includes('\r\n') ? '\r\n' : '\n';
69008
+ const hasFinalEol = content.endsWith('\n');
69009
+ const lines = content.split(/\r?\n/);
69010
+ const sections = [];
69011
+ let startLine = 0;
69012
+ let index = 0;
69013
+ for (let i = 0; i <= lines.length; i++) {
69014
+ const line = lines[i];
69015
+ const isSeparator = i < lines.length && line !== undefined && line.trim() === '---';
69016
+ const isEnd = i === lines.length;
69017
+ if (!isSeparator && !isEnd) {
69018
+ continue;
69019
+ }
69020
+ const endLine = i - 1;
69021
+ const firstNonEmptyLine = findFirstNonEmptyLine(lines, startLine, endLine);
69022
+ if (firstNonEmptyLine !== undefined) {
69023
+ const statusLine = (lines[firstNonEmptyLine] || '').trim();
69024
+ const parsedStatus = parseStatusLine(statusLine);
69025
+ const status = (_a = parsedStatus === null || parsedStatus === void 0 ? void 0 : parsedStatus.status) !== null && _a !== void 0 ? _a : 'not-ready';
69026
+ const priority = (_b = parsedStatus === null || parsedStatus === void 0 ? void 0 : parsedStatus.priority) !== null && _b !== void 0 ? _b : 0;
69027
+ sections.push({
69028
+ index,
69029
+ startLine,
69030
+ endLine,
69031
+ status,
69032
+ priority,
69033
+ statusLineIndex: parsedStatus ? firstNonEmptyLine : undefined,
69034
+ });
69035
+ index += 1;
69036
+ }
69037
+ startLine = i + 1;
69038
+ }
69039
+ return {
69040
+ path: filePath,
69041
+ name: basename(filePath),
69042
+ lines,
69043
+ eol,
69044
+ hasFinalEol,
69045
+ sections,
69046
+ };
69047
+ }
69048
+ /**
69049
+ * Parses a status line like "[ ] !!" or "[-]" or "[x] ~$0.65 21 minutes..." into status and priority.
69050
+ * For [x] done and [!] failed prompts, allow metadata after the status marker.
69051
+ */
69052
+ function parseStatusLine(line) {
69053
+ var _a, _b, _c, _d, _e;
69054
+ // For done prompts [x], allow any content after (for cost/time metadata)
69055
+ const doneMatch = line.match(/^\[(?<status>[xX])\]/);
69056
+ if (doneMatch) {
69057
+ return { status: 'done', priority: 0 };
69058
+ }
69059
+ // For failed prompts [!], allow any content after (for failure metadata)
69060
+ const failedMatch = line.match(/^\[(?<status>!)\]/);
69061
+ if (failedMatch) {
69062
+ return { status: 'failed', priority: 0 };
69063
+ }
69064
+ // For todo [ ] and not-ready [-], require clean end with optional priority markers
69065
+ const match = line.match(/^\[(?<status>[ -])\]\s*(?<priority>!*)\s*$/);
69066
+ if (!match) {
69067
+ return undefined;
69068
+ }
69069
+ const statusChar = (_b = (_a = match.groups) === null || _a === void 0 ? void 0 : _a.status) === null || _b === void 0 ? void 0 : _b.toLowerCase();
69070
+ let status;
69071
+ if (statusChar === '-') {
69072
+ status = 'not-ready';
69073
+ }
69074
+ else {
69075
+ status = 'todo';
69076
+ }
69077
+ const priority = status === 'todo' ? (_e = (_d = (_c = match.groups) === null || _c === void 0 ? void 0 : _c.priority) === null || _d === void 0 ? void 0 : _d.length) !== null && _e !== void 0 ? _e : 0 : 0;
69078
+ return { status, priority };
69079
+ }
69080
+ /**
69081
+ * Finds the first non-empty line index between two bounds.
69082
+ */
69083
+ function findFirstNonEmptyLine(lines, startLine, endLine) {
69084
+ for (let i = startLine; i <= endLine; i++) {
69085
+ const line = lines[i];
69086
+ if (line !== undefined && line.trim() !== '') {
69087
+ return i;
69088
+ }
69089
+ }
69090
+ return undefined;
69091
+ }
69092
+
69093
+ /**
69094
+ * Loads and parses prompt files from the prompts directory.
69095
+ */
69096
+ async function loadPromptFiles(promptsDir) {
69097
+ const entries = await readdir(promptsDir, { withFileTypes: true });
69098
+ const files = entries
69099
+ .filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith('.md'))
69100
+ .map((entry) => join(promptsDir, entry.name))
69101
+ .sort((a, b) => a.localeCompare(b));
69102
+ const promptFiles = [];
69103
+ for (const filePath of files) {
69104
+ const content = await readFile(filePath, 'utf-8');
69105
+ promptFiles.push(parsePromptFile(filePath, content));
69106
+ }
69107
+ return promptFiles;
69108
+ }
69109
+
69110
+ /**
69111
+ * Builds a display label using the prompt line number for easier navigation.
69112
+ */
69113
+ function buildPromptLabelForDisplay(file, section) {
69114
+ return `${relative(process.cwd(), file.path).replace(/\\/g, '/')}#${section.startLine + 1}`;
69115
+ }
69116
+
69117
+ /**
69118
+ * Extracts a short summary line from a prompt section.
69119
+ */
69120
+ function buildPromptSummary(file, section) {
69121
+ const lines = buildCodexPrompt(file, section).split(/\r?\n/);
69122
+ const firstLine = lines.find((line) => line.trim() !== '');
69123
+ return (firstLine === null || firstLine === void 0 ? void 0 : firstLine.trim()) || '(empty prompt)';
69124
+ }
69125
+
69126
+ /**
69127
+ * Prints the list of prompts that still need to be written.
69128
+ */
69129
+ function printPromptsToBeWritten(files, minimumPriority = 0) {
69130
+ const promptsToWrite = listPromptsToBeWritten(files, minimumPriority);
69131
+ let i = 0;
69132
+ for (const { file, section } of promptsToWrite) {
69133
+ const label = buildPromptLabelForDisplay(file, section);
69134
+ const summary = buildPromptSummary(file, section);
69135
+ console.info(` ${++i}) ${label}: ${summary}`);
69136
+ }
69137
+ }
69138
+
69139
+ /**
69140
+ * Prints the summary stats line.
69141
+ */
69142
+ function printStats(stats, minimumPriority = 0) {
69143
+ const priorityStats = minimumPriority > 0 ? ` | Priority <${minimumPriority}: ${stats.belowMinimumPriority}` : '';
69144
+ console.info(colors.cyan(`Done: ${stats.done} | For agent: ${stats.forAgent}${priorityStats} | To be written: ${stats.toBeWritten}`));
69145
+ }
69146
+
69147
+ /**
69148
+ * Summarizes prompt stats for the runner output.
69149
+ */
69150
+ function summarizePrompts(files, minimumPriority = 0) {
69151
+ const stats = { done: 0, forAgent: 0, belowMinimumPriority: 0, toBeWritten: 0 };
69152
+ for (const file of files) {
69153
+ for (const section of file.sections) {
69154
+ if (section.status === 'done') {
69155
+ stats.done += 1;
69156
+ }
69157
+ else if (section.status === 'todo') {
69158
+ const isAbovePriority = hasSufficientPriority(section, minimumPriority);
69159
+ if (isPromptToBeWritten(file, section)) {
69160
+ if (!isAbovePriority) {
69161
+ continue;
69162
+ }
69163
+ stats.toBeWritten += 1;
69164
+ }
69165
+ else if (isAbovePriority) {
69166
+ stats.forAgent += 1;
69167
+ }
69168
+ else {
69169
+ stats.belowMinimumPriority += 1;
69170
+ }
69171
+ }
69172
+ }
69173
+ }
69174
+ return stats;
69175
+ }
69176
+
69177
+ /**
69178
+ * Constant for prompts dir β€” mirrors the constant in `runCodexPrompts.ts`.
69179
+ */
69180
+ const PROMPTS_DIR$2 = join(process.cwd(), 'prompts');
69181
+ /**
69182
+ * Lists and prints all prompt sections that still need to be authored (contain `@@@` placeholder).
69183
+ *
69184
+ * Reuses the same filtering logic as `ptbk coder run --dry-run`.
69185
+ *
69186
+ * @returns the number of unwritten prompts found
69187
+ * @public exported from `@promptbook/cli`
69188
+ */
69189
+ async function findUnwrittenPrompts(options = {}) {
69190
+ const { priority = 0 } = options;
69191
+ const promptFiles = await loadPromptFiles(PROMPTS_DIR$2);
69192
+ const stats = summarizePrompts(promptFiles, priority);
69193
+ printStats(stats, priority);
69194
+ const unwrittenPrompts = listPromptsToBeWritten(promptFiles, priority);
69195
+ if (unwrittenPrompts.length === 0) {
69196
+ console.info(colors.green('All prompts are written β€” nothing to do.'));
69197
+ }
69198
+ else {
69199
+ console.info(colors.yellow(`Found ${unwrittenPrompts.length} prompt(s) that need to be written:`));
69200
+ printPromptsToBeWritten(promptFiles, priority);
69201
+ }
69202
+ return unwrittenPrompts.length;
69203
+ }
69204
+
69205
+ var findUnwrittenPrompts$1 = /*#__PURE__*/Object.freeze({
69206
+ __proto__: null,
69207
+ findUnwrittenPrompts: findUnwrittenPrompts
69208
+ });
69209
+
68626
69210
  /**
68627
69211
  * CLI usage text for this script.
68628
69212
  */
@@ -68966,6 +69550,29 @@ function formatPromptCount(count) {
68966
69550
  return `${count} prompt${count === 1 ? '' : 's'}`;
68967
69551
  }
68968
69552
 
69553
+ /**
69554
+ * Reads an optional agent `.book` file and compiles its system message for injection into coder prompts.
69555
+ *
69556
+ * Returns `undefined` when no agent path is provided.
69557
+ */
69558
+ async function resolveAgentSystemMessage(agentPath, currentWorkingDirectory) {
69559
+ if (!agentPath) {
69560
+ return undefined;
69561
+ }
69562
+ const resolvedAgentPath = resolve(currentWorkingDirectory, agentPath);
69563
+ const agentSource = await readFile(resolvedAgentPath, 'utf-8').catch((error) => {
69564
+ if (error.code === 'ENOENT' || error.code === 'EISDIR') {
69565
+ throw new NotFoundError(spaceTrim(`
69566
+ Agent book \`${agentPath}\` was not found or is not a file.
69567
+
69568
+ Pass a path to a \`.book\` file in \`--agent\`.
69569
+ `));
69570
+ }
69571
+ throw error;
69572
+ });
69573
+ return createAgentRunnerSystemMessage(agentSource);
69574
+ }
69575
+
68969
69576
  /**
68970
69577
  * Resolves optional coding context provided inline or via a file path.
68971
69578
  */
@@ -69008,104 +69615,6 @@ async function ensureWorkingTreeClean() {
69008
69615
  }
69009
69616
  }
69010
69617
 
69011
- /**
69012
- * Builds a display label using the prompt line number for easier navigation.
69013
- */
69014
- function buildPromptLabelForDisplay(file, section) {
69015
- return `${relative(process.cwd(), file.path).replace(/\\/g, '/')}#${section.startLine + 1}`;
69016
- }
69017
-
69018
- /**
69019
- * Trims leading and trailing empty lines.
69020
- */
69021
- function trimEmptyEdges(lines) {
69022
- let start = 0;
69023
- while (start < lines.length) {
69024
- const startLine = lines[start];
69025
- if (startLine !== undefined && startLine.trim() !== '') {
69026
- break;
69027
- }
69028
- start += 1;
69029
- }
69030
- let end = lines.length - 1;
69031
- while (end >= start) {
69032
- const endLine = lines[end];
69033
- if (endLine !== undefined && endLine.trim() !== '') {
69034
- break;
69035
- }
69036
- end -= 1;
69037
- }
69038
- return lines.slice(start, end + 1);
69039
- }
69040
-
69041
- /**
69042
- * Extracts prompt lines without the status marker.
69043
- */
69044
- function buildPromptLinesWithoutStatus(file, section) {
69045
- const lines = file.lines.slice(section.startLine, section.endLine + 1);
69046
- if (section.statusLineIndex !== undefined) {
69047
- const relativeIndex = section.statusLineIndex - section.startLine;
69048
- if (relativeIndex >= 0 && relativeIndex < lines.length) {
69049
- lines.splice(relativeIndex, 1);
69050
- }
69051
- }
69052
- return trimEmptyEdges(lines);
69053
- }
69054
-
69055
- /**
69056
- * Builds the prompt text sent to the agent runner.
69057
- */
69058
- function buildCodexPrompt(file, section) {
69059
- const lines = buildPromptLinesWithoutStatus(file, section);
69060
- for (let i = 0; i < lines.length; i++) {
69061
- const line = lines[i];
69062
- if (line === undefined || line.trim() === '') {
69063
- continue;
69064
- }
69065
- lines[i] = line.replace(/^\[[^\]]+\]\s*/, '');
69066
- break;
69067
- }
69068
- return lines.join(file.eol);
69069
- }
69070
-
69071
- /**
69072
- * Extracts a short summary line from a prompt section.
69073
- */
69074
- function buildPromptSummary(file, section) {
69075
- const lines = buildCodexPrompt(file, section).split(/\r?\n/);
69076
- const firstLine = lines.find((line) => line.trim() !== '');
69077
- return (firstLine === null || firstLine === void 0 ? void 0 : firstLine.trim()) || '(empty prompt)';
69078
- }
69079
-
69080
- /**
69081
- * Checks whether a prompt section meets the minimum priority threshold.
69082
- */
69083
- function hasSufficientPriority(section, minimumPriority) {
69084
- return section.priority >= minimumPriority;
69085
- }
69086
-
69087
- /**
69088
- * Checks whether a prompt section still needs authoring (contains "@@@").
69089
- */
69090
- function isPromptToBeWritten(file, section) {
69091
- return buildCodexPrompt(file, section).includes('@@@');
69092
- }
69093
-
69094
- /**
69095
- * Lists todo prompts across all files.
69096
- */
69097
- function listTodoPrompts(files) {
69098
- const prompts = [];
69099
- for (const file of files) {
69100
- for (const section of file.sections) {
69101
- if (section.status === 'todo') {
69102
- prompts.push({ file, section });
69103
- }
69104
- }
69105
- }
69106
- return prompts;
69107
- }
69108
-
69109
69618
  /**
69110
69619
  * Lists todo prompts that are ready to run (no authoring placeholders).
69111
69620
  */
@@ -69138,142 +69647,6 @@ function listUpcomingTasks(files, minimumPriority = 0) {
69138
69647
  }));
69139
69648
  }
69140
69649
 
69141
- /**
69142
- * Parses a prompt markdown file into sections and metadata.
69143
- */
69144
- function parsePromptFile(filePath, content) {
69145
- var _a, _b;
69146
- const eol = content.includes('\r\n') ? '\r\n' : '\n';
69147
- const hasFinalEol = content.endsWith('\n');
69148
- const lines = content.split(/\r?\n/);
69149
- const sections = [];
69150
- let startLine = 0;
69151
- let index = 0;
69152
- for (let i = 0; i <= lines.length; i++) {
69153
- const line = lines[i];
69154
- const isSeparator = i < lines.length && line !== undefined && line.trim() === '---';
69155
- const isEnd = i === lines.length;
69156
- if (!isSeparator && !isEnd) {
69157
- continue;
69158
- }
69159
- const endLine = i - 1;
69160
- const firstNonEmptyLine = findFirstNonEmptyLine(lines, startLine, endLine);
69161
- if (firstNonEmptyLine !== undefined) {
69162
- const statusLine = (lines[firstNonEmptyLine] || '').trim();
69163
- const parsedStatus = parseStatusLine(statusLine);
69164
- const status = (_a = parsedStatus === null || parsedStatus === void 0 ? void 0 : parsedStatus.status) !== null && _a !== void 0 ? _a : 'not-ready';
69165
- const priority = (_b = parsedStatus === null || parsedStatus === void 0 ? void 0 : parsedStatus.priority) !== null && _b !== void 0 ? _b : 0;
69166
- sections.push({
69167
- index,
69168
- startLine,
69169
- endLine,
69170
- status,
69171
- priority,
69172
- statusLineIndex: parsedStatus ? firstNonEmptyLine : undefined,
69173
- });
69174
- index += 1;
69175
- }
69176
- startLine = i + 1;
69177
- }
69178
- return {
69179
- path: filePath,
69180
- name: basename(filePath),
69181
- lines,
69182
- eol,
69183
- hasFinalEol,
69184
- sections,
69185
- };
69186
- }
69187
- /**
69188
- * Parses a status line like "[ ] !!" or "[-]" or "[x] ~$0.65 21 minutes..." into status and priority.
69189
- * For [x] done and [!] failed prompts, allow metadata after the status marker.
69190
- */
69191
- function parseStatusLine(line) {
69192
- var _a, _b, _c, _d, _e;
69193
- // For done prompts [x], allow any content after (for cost/time metadata)
69194
- const doneMatch = line.match(/^\[(?<status>[xX])\]/);
69195
- if (doneMatch) {
69196
- return { status: 'done', priority: 0 };
69197
- }
69198
- // For failed prompts [!], allow any content after (for failure metadata)
69199
- const failedMatch = line.match(/^\[(?<status>!)\]/);
69200
- if (failedMatch) {
69201
- return { status: 'failed', priority: 0 };
69202
- }
69203
- // For todo [ ] and not-ready [-], require clean end with optional priority markers
69204
- const match = line.match(/^\[(?<status>[ -])\]\s*(?<priority>!*)\s*$/);
69205
- if (!match) {
69206
- return undefined;
69207
- }
69208
- const statusChar = (_b = (_a = match.groups) === null || _a === void 0 ? void 0 : _a.status) === null || _b === void 0 ? void 0 : _b.toLowerCase();
69209
- let status;
69210
- if (statusChar === '-') {
69211
- status = 'not-ready';
69212
- }
69213
- else {
69214
- status = 'todo';
69215
- }
69216
- const priority = status === 'todo' ? (_e = (_d = (_c = match.groups) === null || _c === void 0 ? void 0 : _c.priority) === null || _d === void 0 ? void 0 : _d.length) !== null && _e !== void 0 ? _e : 0 : 0;
69217
- return { status, priority };
69218
- }
69219
- /**
69220
- * Finds the first non-empty line index between two bounds.
69221
- */
69222
- function findFirstNonEmptyLine(lines, startLine, endLine) {
69223
- for (let i = startLine; i <= endLine; i++) {
69224
- const line = lines[i];
69225
- if (line !== undefined && line.trim() !== '') {
69226
- return i;
69227
- }
69228
- }
69229
- return undefined;
69230
- }
69231
-
69232
- /**
69233
- * Loads and parses prompt files from the prompts directory.
69234
- */
69235
- async function loadPromptFiles(promptsDir) {
69236
- const entries = await readdir(promptsDir, { withFileTypes: true });
69237
- const files = entries
69238
- .filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith('.md'))
69239
- .map((entry) => join(promptsDir, entry.name))
69240
- .sort((a, b) => a.localeCompare(b));
69241
- const promptFiles = [];
69242
- for (const filePath of files) {
69243
- const content = await readFile(filePath, 'utf-8');
69244
- promptFiles.push(parsePromptFile(filePath, content));
69245
- }
69246
- return promptFiles;
69247
- }
69248
-
69249
- /**
69250
- * Lists todo prompts that still contain authoring placeholders.
69251
- */
69252
- function listPromptsToBeWritten(files, minimumPriority = 0) {
69253
- return listTodoPrompts(files).filter((prompt) => isPromptToBeWritten(prompt.file, prompt.section) && hasSufficientPriority(prompt.section, minimumPriority));
69254
- }
69255
-
69256
- /**
69257
- * Prints the list of prompts that still need to be written.
69258
- */
69259
- function printPromptsToBeWritten(files, minimumPriority = 0) {
69260
- const promptsToWrite = listPromptsToBeWritten(files, minimumPriority);
69261
- let i = 0;
69262
- for (const { file, section } of promptsToWrite) {
69263
- const label = buildPromptLabelForDisplay(file, section);
69264
- const summary = buildPromptSummary(file, section);
69265
- console.info(` ${++i}) ${label}: ${summary}`);
69266
- }
69267
- }
69268
-
69269
- /**
69270
- * Prints the summary stats line.
69271
- */
69272
- function printStats(stats, minimumPriority = 0) {
69273
- const priorityStats = minimumPriority > 0 ? ` | Priority <${minimumPriority}: ${stats.belowMinimumPriority}` : '';
69274
- console.info(colors.cyan(`Done: ${stats.done} | For agent: ${stats.forAgent}${priorityStats} | To be written: ${stats.toBeWritten}`));
69275
- }
69276
-
69277
69650
  /**
69278
69651
  * Groups upcoming tasks by priority, high to low.
69279
69652
  */
@@ -69311,36 +69684,6 @@ function printUpcomingTasks(tasks) {
69311
69684
  }
69312
69685
  }
69313
69686
 
69314
- /**
69315
- * Summarizes prompt stats for the runner output.
69316
- */
69317
- function summarizePrompts(files, minimumPriority = 0) {
69318
- const stats = { done: 0, forAgent: 0, belowMinimumPriority: 0, toBeWritten: 0 };
69319
- for (const file of files) {
69320
- for (const section of file.sections) {
69321
- if (section.status === 'done') {
69322
- stats.done += 1;
69323
- }
69324
- else if (section.status === 'todo') {
69325
- const isAbovePriority = hasSufficientPriority(section, minimumPriority);
69326
- if (isPromptToBeWritten(file, section)) {
69327
- if (!isAbovePriority) {
69328
- continue;
69329
- }
69330
- stats.toBeWritten += 1;
69331
- }
69332
- else if (isAbovePriority) {
69333
- stats.forAgent += 1;
69334
- }
69335
- else {
69336
- stats.belowMinimumPriority += 1;
69337
- }
69338
- }
69339
- }
69340
- }
69341
- return stats;
69342
- }
69343
-
69344
69687
  /**
69345
69688
  * Waits for the user to press Enter before continuing.
69346
69689
  */
@@ -70965,9 +71308,14 @@ async function writePromptFile(file) {
70965
71308
  *
70966
71309
  * @private function of runCodexPrompts
70967
71310
  */
70968
- async function runPromptRound({ options, runner, runnerMetadata, nextPrompt, promptLabel, resolvedCoderContext, isRichUiEnabled, progressDisplay, uiHandle, waitForRequestedPause, }) {
71311
+ async function runPromptRound({ options, runner, runnerMetadata, nextPrompt, promptLabel, resolvedCoderContext, resolvedAgentSystemMessage, isRichUiEnabled, progressDisplay, uiHandle, waitForRequestedPause, }) {
70969
71312
  const commitMessage = buildCommitMessage(nextPrompt.file, nextPrompt.section);
70970
- const codexPrompt = appendCoderContext(buildCodexPrompt(nextPrompt.file, nextPrompt.section), resolvedCoderContext);
71313
+ const taskPrompt = buildCodexPrompt(nextPrompt.file, nextPrompt.section);
71314
+ // Prepend agent system message before the task so the harness sees agent instructions first
71315
+ const promptWithAgent = resolvedAgentSystemMessage
71316
+ ? `${resolvedAgentSystemMessage.trim()}\n\n${taskPrompt}`
71317
+ : taskPrompt;
71318
+ const codexPrompt = appendCoderContext(promptWithAgent, resolvedCoderContext);
70971
71319
  const scriptPath = buildScriptPath(nextPrompt.file, nextPrompt.section);
70972
71320
  setPromptRoundRunningState({ isRichUiEnabled, promptLabel, scriptPath, uiHandle });
70973
71321
  await waitForRequestedPause({
@@ -71200,6 +71548,7 @@ async function runCodexPrompts(providedOptions) {
71200
71548
  startPauseListenerIfNeeded(isRichUiEnabled);
71201
71549
  try {
71202
71550
  const resolvedCoderContext = await resolveCoderContext(options.context, process.cwd());
71551
+ const resolvedAgentSystemMessage = await resolveAgentSystemMessage(options.agent, process.cwd());
71203
71552
  if (await runDryRunIfRequested(options)) {
71204
71553
  return;
71205
71554
  }
@@ -71239,7 +71588,13 @@ async function runCodexPrompts(providedOptions) {
71239
71588
  minimumPriority: options.priority,
71240
71589
  isRichUiEnabled,
71241
71590
  }));
71242
- if (finishWhenNoPromptIsAvailable(promptQueueSnapshot, isRichUiEnabled, uiHandle)) {
71591
+ if (!promptQueueSnapshot.nextPrompt) {
71592
+ if (options.keepAlive) {
71593
+ announceKeepAliveStatus(promptQueueSnapshot, isRichUiEnabled, uiHandle);
71594
+ await new Promise((resolve) => setTimeout(resolve, KEEP_ALIVE_POLL_INTERVAL_MS));
71595
+ continue;
71596
+ }
71597
+ finishWhenNoPromptIsAvailable(promptQueueSnapshot, isRichUiEnabled, uiHandle);
71243
71598
  return;
71244
71599
  }
71245
71600
  const nextPrompt = promptQueueSnapshot.nextPrompt;
@@ -71278,6 +71633,7 @@ async function runCodexPrompts(providedOptions) {
71278
71633
  nextPrompt,
71279
71634
  promptLabel,
71280
71635
  resolvedCoderContext,
71636
+ resolvedAgentSystemMessage,
71281
71637
  isRichUiEnabled,
71282
71638
  progressDisplay,
71283
71639
  uiHandle,
@@ -71453,6 +71809,19 @@ function finishWhenNoPromptIsAvailable(promptQueueSnapshot, isRichUiEnabled, uiH
71453
71809
  }
71454
71810
  return true;
71455
71811
  }
71812
+ /**
71813
+ * Updates the UI status message while waiting for new prompts in keepAlive server mode.
71814
+ */
71815
+ function announceKeepAliveStatus(promptQueueSnapshot, isRichUiEnabled, uiHandle) {
71816
+ const message = promptQueueSnapshot.stats.toBeWritten > 0
71817
+ ? 'No prompts ready for agent. Watching for changes...'
71818
+ : 'All prompts are done. Watching for changes...';
71819
+ uiHandle === null || uiHandle === void 0 ? void 0 : uiHandle.state.setStatusMessage(message);
71820
+ uiHandle === null || uiHandle === void 0 ? void 0 : uiHandle.state.setPhase('waiting');
71821
+ if (!isRichUiEnabled) {
71822
+ console.info(colors.gray(message));
71823
+ }
71824
+ }
71456
71825
  /**
71457
71826
  * Updates UI state and plain-console output for the terminal completion message.
71458
71827
  */
@@ -71493,6 +71862,10 @@ async function waitForPromptConfirmationIfNeeded(options) {
71493
71862
  * Countdown update interval for the between-rounds wait display.
71494
71863
  */
71495
71864
  const WAIT_COUNTDOWN_UPDATE_INTERVAL_MS = 30000;
71865
+ /**
71866
+ * Polling interval when in keepAlive server mode and no runnable prompts are available.
71867
+ */
71868
+ const KEEP_ALIVE_POLL_INTERVAL_MS = 5000;
71496
71869
  /**
71497
71870
  * Waits the configured time between prompt rounds to avoid hitting harness rate limits.
71498
71871
  * Does nothing when `waitBetweenPrompts` is zero.
@@ -71538,6 +71911,780 @@ var runCodexPrompts$1 = /*#__PURE__*/Object.freeze({
71538
71911
  runCodexPrompts: runCodexPrompts
71539
71912
  });
71540
71913
 
71914
+ /**
71915
+ * Overwrites the body of one prompt section with new content, preserving the status line.
71916
+ *
71917
+ * The `newContent` string is the prompt text without the status marker.
71918
+ * The status line (`[ ]`, `[x]`, `[!]`, `[-]`) is kept intact.
71919
+ *
71920
+ * @private internal utility of `ptbk coder server`
71921
+ */
71922
+ async function updatePromptSection(filePath, sectionIndex, newContent) {
71923
+ var _a;
71924
+ const rawContent = await readFile(filePath, 'utf-8');
71925
+ const promptFile = parsePromptFile(filePath, rawContent);
71926
+ const section = promptFile.sections.find((s) => s.index === sectionIndex);
71927
+ if (!section) {
71928
+ throw new Error(`Section ${sectionIndex} not found in file: ${filePath}`);
71929
+ }
71930
+ // Lines before the status line (usually empty lines at section start)
71931
+ const linesBeforeStatus = section.statusLineIndex !== undefined
71932
+ ? promptFile.lines.slice(section.startLine, section.statusLineIndex)
71933
+ : [];
71934
+ // The status line itself (preserved exactly)
71935
+ const statusLineContent = section.statusLineIndex !== undefined ? [promptFile.lines[section.statusLineIndex]] : [];
71936
+ // Split the new content and trim trailing blank lines
71937
+ const newContentLines = newContent.split(/\r?\n/);
71938
+ while (newContentLines.length > 0 && ((_a = newContentLines[newContentLines.length - 1]) === null || _a === void 0 ? void 0 : _a.trim()) === '') {
71939
+ newContentLines.pop();
71940
+ }
71941
+ // Build the reconstructed section lines
71942
+ const newSectionLines = [
71943
+ ...linesBeforeStatus,
71944
+ ...statusLineContent,
71945
+ ...(statusLineContent.length > 0 ? [''] : []),
71946
+ ...newContentLines,
71947
+ ];
71948
+ // Splice the new section lines into the full file line array
71949
+ const updatedLines = [
71950
+ ...promptFile.lines.slice(0, section.startLine),
71951
+ ...newSectionLines,
71952
+ ...promptFile.lines.slice(section.endLine + 1),
71953
+ ];
71954
+ const updatedContent = updatedLines.join(promptFile.eol) + (promptFile.hasFinalEol ? promptFile.eol : '');
71955
+ await writeFile(filePath, updatedContent, 'utf-8');
71956
+ }
71957
+ // Note: [🟑] Code for CLI command [coder server](scripts/run-codex-prompts/server/updatePromptSection.ts) should never be published outside of `@promptbook/cli`
71958
+
71959
+ /**
71960
+ * HTML source for the coder server kanban UI.
71961
+ *
71962
+ * The canonical source lives in `apps/coder-server/index.html`.
71963
+ * Keep both files in sync when updating the frontend.
71964
+ *
71965
+ * @private internal constant of `ptbk coder server`
71966
+ */
71967
+ const CODER_SERVER_HTML = `<!DOCTYPE html>
71968
+ <html lang="en">
71969
+ <head>
71970
+ <meta charset="UTF-8">
71971
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
71972
+ <title>Ptbk Coder Server</title>
71973
+ <style>
71974
+ * { box-sizing: border-box; margin: 0; padding: 0; }
71975
+ body {
71976
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
71977
+ background: #f4f5f7;
71978
+ color: #172b4d;
71979
+ min-height: 100vh;
71980
+ }
71981
+
71982
+ header {
71983
+ background: #0052cc;
71984
+ color: white;
71985
+ padding: 12px 20px;
71986
+ display: flex;
71987
+ align-items: center;
71988
+ gap: 12px;
71989
+ position: sticky;
71990
+ top: 0;
71991
+ z-index: 10;
71992
+ box-shadow: 0 2px 8px rgba(0,0,0,0.2);
71993
+ }
71994
+ header h1 { font-size: 17px; font-weight: 700; flex-shrink: 0; }
71995
+
71996
+ .status-badge {
71997
+ padding: 3px 10px;
71998
+ border-radius: 12px;
71999
+ font-size: 11px;
72000
+ font-weight: 700;
72001
+ letter-spacing: 0.5px;
72002
+ text-transform: uppercase;
72003
+ flex-shrink: 0;
72004
+ }
72005
+ .status-RUNNING { background: #00875a; color: white; }
72006
+ .status-PAUSING { background: #ff8b00; color: white; }
72007
+ .status-PAUSED { background: #bf2600; color: white; }
72008
+
72009
+ #pause-label {
72010
+ font-size: 12px;
72011
+ color: rgba(255,255,255,0.75);
72012
+ flex: 1;
72013
+ min-width: 0;
72014
+ overflow: hidden;
72015
+ text-overflow: ellipsis;
72016
+ white-space: nowrap;
72017
+ }
72018
+
72019
+ .btn {
72020
+ padding: 6px 14px;
72021
+ border: none;
72022
+ border-radius: 4px;
72023
+ cursor: pointer;
72024
+ font-size: 13px;
72025
+ font-weight: 600;
72026
+ transition: opacity 0.15s;
72027
+ flex-shrink: 0;
72028
+ }
72029
+ .btn:hover { opacity: 0.85; }
72030
+ .btn-pause { background: #ffc400; color: #172b4d; }
72031
+ .btn-resume { background: #00875a; color: white; }
72032
+
72033
+ .board {
72034
+ display: flex;
72035
+ gap: 14px;
72036
+ padding: 16px;
72037
+ overflow-x: auto;
72038
+ min-height: calc(100vh - 52px);
72039
+ align-items: flex-start;
72040
+ }
72041
+
72042
+ .column {
72043
+ background: #ebecf0;
72044
+ border-radius: 8px;
72045
+ padding: 10px;
72046
+ min-width: 250px;
72047
+ width: 270px;
72048
+ flex-shrink: 0;
72049
+ }
72050
+
72051
+ .column-header {
72052
+ font-size: 12px;
72053
+ font-weight: 700;
72054
+ text-transform: uppercase;
72055
+ letter-spacing: 0.6px;
72056
+ color: #5e6c84;
72057
+ margin-bottom: 10px;
72058
+ display: flex;
72059
+ align-items: center;
72060
+ justify-content: space-between;
72061
+ }
72062
+
72063
+ .column-count {
72064
+ background: #dfe1e6;
72065
+ border-radius: 10px;
72066
+ padding: 1px 7px;
72067
+ font-size: 11px;
72068
+ color: #5e6c84;
72069
+ }
72070
+
72071
+ .column-cards { min-height: 40px; }
72072
+
72073
+ .card {
72074
+ background: white;
72075
+ border-radius: 6px;
72076
+ padding: 10px 12px;
72077
+ margin-bottom: 8px;
72078
+ box-shadow: 0 1px 3px rgba(0,0,0,0.08);
72079
+ cursor: pointer;
72080
+ transition: box-shadow 0.15s, transform 0.1s;
72081
+ border-left: 3px solid transparent;
72082
+ }
72083
+ .card:hover {
72084
+ box-shadow: 0 4px 10px rgba(0,0,0,0.14);
72085
+ transform: translateY(-1px);
72086
+ }
72087
+ .card-todo { border-left-color: #0052cc; }
72088
+ .card-not-ready { border-left-color: #8993a4; }
72089
+ .card-done { border-left-color: #00875a; }
72090
+ .card-failed { border-left-color: #bf2600; }
72091
+
72092
+ .card-file {
72093
+ font-size: 10px;
72094
+ color: #8993a4;
72095
+ margin-bottom: 5px;
72096
+ font-weight: 500;
72097
+ }
72098
+
72099
+ .card-summary {
72100
+ font-size: 13px;
72101
+ line-height: 1.5;
72102
+ color: #172b4d;
72103
+ word-break: break-word;
72104
+ white-space: pre-wrap;
72105
+ max-height: 78px;
72106
+ overflow: hidden;
72107
+ display: -webkit-box;
72108
+ -webkit-line-clamp: 4;
72109
+ -webkit-box-orient: vertical;
72110
+ }
72111
+
72112
+ .card-priority {
72113
+ margin-top: 6px;
72114
+ color: #ff8b00;
72115
+ font-size: 11px;
72116
+ font-weight: 700;
72117
+ }
72118
+
72119
+ .card-edit-hint {
72120
+ font-size: 10px;
72121
+ color: #c1c7d0;
72122
+ margin-top: 6px;
72123
+ display: none;
72124
+ }
72125
+ .card:hover .card-edit-hint { display: block; }
72126
+
72127
+ .empty-column {
72128
+ color: #b3bac5;
72129
+ font-size: 12px;
72130
+ padding: 10px 4px;
72131
+ text-align: center;
72132
+ }
72133
+
72134
+ .modal-overlay {
72135
+ position: fixed;
72136
+ inset: 0;
72137
+ background: rgba(9,30,66,0.54);
72138
+ display: flex;
72139
+ align-items: flex-start;
72140
+ justify-content: center;
72141
+ padding: 60px 16px 16px;
72142
+ z-index: 100;
72143
+ animation: fadeIn 0.1s ease;
72144
+ }
72145
+ .modal-overlay.hidden { display: none; }
72146
+
72147
+ @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
72148
+
72149
+ .modal {
72150
+ background: white;
72151
+ border-radius: 8px;
72152
+ width: 100%;
72153
+ max-width: 620px;
72154
+ padding: 20px;
72155
+ box-shadow: 0 8px 32px rgba(0,0,0,0.25);
72156
+ animation: slideIn 0.15s ease;
72157
+ }
72158
+ @keyframes slideIn { from { transform: translateY(-8px); opacity: 0; } to { transform: none; opacity: 1; } }
72159
+
72160
+ .modal-header {
72161
+ display: flex;
72162
+ align-items: flex-start;
72163
+ justify-content: space-between;
72164
+ margin-bottom: 14px;
72165
+ gap: 12px;
72166
+ }
72167
+
72168
+ .modal-meta { flex: 1; min-width: 0; }
72169
+ .modal-file { font-size: 11px; color: #8993a4; margin-bottom: 4px; }
72170
+ .modal-section-label { font-size: 13px; font-weight: 600; color: #172b4d; }
72171
+
72172
+ .modal-status {
72173
+ font-size: 11px;
72174
+ font-weight: 700;
72175
+ padding: 2px 8px;
72176
+ border-radius: 10px;
72177
+ text-transform: uppercase;
72178
+ letter-spacing: 0.4px;
72179
+ flex-shrink: 0;
72180
+ }
72181
+ .status-todo { background: #deebff; color: #0052cc; }
72182
+ .status-not-ready { background: #f4f5f7; color: #5e6c84; }
72183
+ .status-done { background: #e3fcef; color: #006644; }
72184
+ .status-failed { background: #ffebe6; color: #bf2600; }
72185
+
72186
+ .modal-close {
72187
+ background: none;
72188
+ border: none;
72189
+ color: #8993a4;
72190
+ cursor: pointer;
72191
+ font-size: 18px;
72192
+ padding: 0 4px;
72193
+ line-height: 1;
72194
+ flex-shrink: 0;
72195
+ }
72196
+ .modal-close:hover { color: #172b4d; }
72197
+
72198
+ textarea {
72199
+ width: 100%;
72200
+ min-height: 220px;
72201
+ font-family: 'SFMono-Regular', 'Consolas', 'Courier New', monospace;
72202
+ font-size: 13px;
72203
+ border: 2px solid #dfe1e6;
72204
+ border-radius: 4px;
72205
+ padding: 10px 12px;
72206
+ resize: vertical;
72207
+ line-height: 1.65;
72208
+ color: #172b4d;
72209
+ transition: border-color 0.15s;
72210
+ }
72211
+ textarea:focus {
72212
+ outline: none;
72213
+ border-color: #0052cc;
72214
+ box-shadow: 0 0 0 3px rgba(0,82,204,0.12);
72215
+ }
72216
+
72217
+ .modal-hint {
72218
+ font-size: 11px;
72219
+ color: #8993a4;
72220
+ margin-top: 6px;
72221
+ }
72222
+
72223
+ .modal-actions {
72224
+ display: flex;
72225
+ gap: 8px;
72226
+ margin-top: 14px;
72227
+ justify-content: flex-end;
72228
+ }
72229
+
72230
+ .btn-save { background: #0052cc; color: white; }
72231
+ .btn-cancel { background: #f4f5f7; color: #172b4d; }
72232
+ .btn-cancel:hover { background: #ebecf0; opacity: 1; }
72233
+
72234
+ .error-banner {
72235
+ background: #ffebe6;
72236
+ border-bottom: 2px solid #bf2600;
72237
+ color: #bf2600;
72238
+ padding: 8px 20px;
72239
+ font-size: 13px;
72240
+ font-weight: 500;
72241
+ }
72242
+ .error-banner.hidden { display: none; }
72243
+ </style>
72244
+ </head>
72245
+ <body>
72246
+
72247
+ <div id="error-banner" class="error-banner hidden"></div>
72248
+
72249
+ <header>
72250
+ <h1>&#128295; Ptbk Coder Server</h1>
72251
+ <span id="status-badge" class="status-badge status-RUNNING">RUNNING</span>
72252
+ <span id="pause-label"></span>
72253
+ <button id="toggle-btn" class="btn btn-pause">&#9646;&#9646; Pause</button>
72254
+ </header>
72255
+
72256
+ <div class="board">
72257
+ <div class="column">
72258
+ <div class="column-header">
72259
+ To Do
72260
+ <span class="column-count" id="count-todo">0</span>
72261
+ </div>
72262
+ <div class="column-cards" id="cards-todo">
72263
+ <div class="empty-column">Loading&hellip;</div>
72264
+ </div>
72265
+ </div>
72266
+
72267
+ <div class="column">
72268
+ <div class="column-header">
72269
+ Not Ready
72270
+ <span class="column-count" id="count-not-ready">0</span>
72271
+ </div>
72272
+ <div class="column-cards" id="cards-not-ready"></div>
72273
+ </div>
72274
+
72275
+ <div class="column">
72276
+ <div class="column-header">
72277
+ Done
72278
+ <span class="column-count" id="count-done">0</span>
72279
+ </div>
72280
+ <div class="column-cards" id="cards-done"></div>
72281
+ </div>
72282
+
72283
+ <div class="column">
72284
+ <div class="column-header">
72285
+ Failed
72286
+ <span class="column-count" id="count-failed">0</span>
72287
+ </div>
72288
+ <div class="column-cards" id="cards-failed"></div>
72289
+ </div>
72290
+ </div>
72291
+
72292
+ <div class="modal-overlay hidden" id="modal-overlay">
72293
+ <div class="modal">
72294
+ <div class="modal-header">
72295
+ <div class="modal-meta">
72296
+ <div class="modal-file" id="modal-file"></div>
72297
+ <div class="modal-section-label" id="modal-section-label"></div>
72298
+ </div>
72299
+ <span class="modal-status" id="modal-status-badge"></span>
72300
+ <button class="modal-close" onclick="closeModal()" title="Close (Esc)">&#x2715;</button>
72301
+ </div>
72302
+
72303
+ <textarea id="modal-content" placeholder="Prompt content&hellip;"></textarea>
72304
+ <div class="modal-hint">Tip: press Ctrl+Enter to save, Esc to cancel.</div>
72305
+
72306
+ <div class="modal-actions">
72307
+ <button class="btn btn-cancel" onclick="closeModal()">Cancel</button>
72308
+ <button class="btn btn-save" onclick="saveModal()">Save</button>
72309
+ </div>
72310
+ </div>
72311
+ </div>
72312
+
72313
+ <script>
72314
+ 'use strict';
72315
+
72316
+ let modalState = null;
72317
+ let lastPauseState = 'RUNNING';
72318
+
72319
+ function showError(msg) {
72320
+ const banner = document.getElementById('error-banner');
72321
+ banner.textContent = '\\u26a0 ' + msg;
72322
+ banner.classList.remove('hidden');
72323
+ clearTimeout(banner._timer);
72324
+ banner._timer = setTimeout(() => banner.classList.add('hidden'), 6000);
72325
+ }
72326
+
72327
+ function escapeHtml(str) {
72328
+ const d = document.createElement('div');
72329
+ d.textContent = String(str);
72330
+ return d.innerHTML;
72331
+ }
72332
+
72333
+ async function fetchStatus() {
72334
+ try {
72335
+ const res = await fetch('/api/status');
72336
+ if (!res.ok) { showError('Status API error: ' + res.status); return; }
72337
+ const data = await res.json();
72338
+
72339
+ lastPauseState = data.pauseState;
72340
+
72341
+ const badge = document.getElementById('status-badge');
72342
+ badge.textContent = data.pauseState;
72343
+ badge.className = 'status-badge status-' + data.pauseState;
72344
+
72345
+ const btn = document.getElementById('toggle-btn');
72346
+ const label = document.getElementById('pause-label');
72347
+
72348
+ if (data.pauseState === 'RUNNING') {
72349
+ btn.textContent = '\\u23f8 Pause';
72350
+ btn.className = 'btn btn-pause';
72351
+ label.textContent = '';
72352
+ } else if (data.pauseState === 'PAUSING') {
72353
+ btn.textContent = '\\u23f5 Resume';
72354
+ btn.className = 'btn btn-resume';
72355
+ label.textContent = 'Pausing before: ' + (data.pauseTargetLabel || '\\u2026');
72356
+ } else {
72357
+ btn.textContent = '\\u23f5 Resume';
72358
+ btn.className = 'btn btn-resume';
72359
+ label.textContent = 'Paused before: ' + (data.pauseTargetLabel || '\\u2026');
72360
+ }
72361
+ } catch (e) {
72362
+ showError('Could not reach coder server: ' + e.message);
72363
+ }
72364
+ }
72365
+
72366
+ async function fetchPrompts() {
72367
+ try {
72368
+ const res = await fetch('/api/prompts');
72369
+ if (!res.ok) { showError('Prompts API error: ' + res.status); return; }
72370
+ renderBoard(await res.json());
72371
+ } catch (e) {
72372
+ showError('Could not load prompts: ' + e.message);
72373
+ }
72374
+ }
72375
+
72376
+ function renderBoard(promptFiles) {
72377
+ const columns = { 'todo': [], 'not-ready': [], 'done': [], 'failed': [] };
72378
+
72379
+ for (const file of promptFiles) {
72380
+ for (const section of file.sections) {
72381
+ const col = columns[section.status];
72382
+ if (col) col.push({ file, section });
72383
+ }
72384
+ }
72385
+
72386
+ for (const [status, cards] of Object.entries(columns)) {
72387
+ const container = document.getElementById('cards-' + status);
72388
+ const countEl = document.getElementById('count-' + status);
72389
+ if (!container) continue;
72390
+
72391
+ countEl.textContent = cards.length;
72392
+ container.innerHTML = '';
72393
+
72394
+ if (cards.length === 0) {
72395
+ container.innerHTML = '<div class="empty-column">Empty</div>';
72396
+ continue;
72397
+ }
72398
+
72399
+ for (const { file, section } of cards) {
72400
+ const card = document.createElement('div');
72401
+ card.className = 'card card-' + section.status;
72402
+ card.innerHTML =
72403
+ '<div class="card-file">' + escapeHtml(file.fileName) + ' &bull; #' + (section.index + 1) + '</div>' +
72404
+ '<div class="card-summary">' + escapeHtml(section.summary) + '</div>' +
72405
+ (section.priority > 0
72406
+ ? '<div class="card-priority">' + '!'.repeat(section.priority) + ' priority ' + section.priority + '</div>'
72407
+ : '') +
72408
+ '<div class="card-edit-hint">Click to edit</div>';
72409
+ card.onclick = () => openModal(file, section);
72410
+ container.appendChild(card);
72411
+ }
72412
+ }
72413
+ }
72414
+
72415
+ function openModal(file, section) {
72416
+ modalState = { filePath: file.filePath, sectionIndex: section.index };
72417
+
72418
+ document.getElementById('modal-file').textContent = file.fileName;
72419
+ document.getElementById('modal-section-label').textContent = 'Section ' + (section.index + 1);
72420
+
72421
+ const badge = document.getElementById('modal-status-badge');
72422
+ badge.textContent = section.status.replace('-', '\\u2011');
72423
+ badge.className = 'modal-status status-' + section.status;
72424
+
72425
+ document.getElementById('modal-content').value = section.content;
72426
+ document.getElementById('modal-overlay').classList.remove('hidden');
72427
+ setTimeout(() => document.getElementById('modal-content').focus(), 50);
72428
+ }
72429
+
72430
+ function closeModal() {
72431
+ document.getElementById('modal-overlay').classList.add('hidden');
72432
+ modalState = null;
72433
+ }
72434
+
72435
+ async function saveModal() {
72436
+ if (!modalState) return;
72437
+
72438
+ const content = document.getElementById('modal-content').value;
72439
+ const saveBtn = document.querySelector('.btn-save');
72440
+ saveBtn.disabled = true;
72441
+ saveBtn.textContent = 'Saving\\u2026';
72442
+
72443
+ try {
72444
+ const res = await fetch('/api/prompts/update', {
72445
+ method: 'PUT',
72446
+ headers: { 'Content-Type': 'application/json' },
72447
+ body: JSON.stringify({
72448
+ filePath: modalState.filePath,
72449
+ sectionIndex: modalState.sectionIndex,
72450
+ content,
72451
+ }),
72452
+ });
72453
+ if (!res.ok) throw new Error('HTTP ' + res.status);
72454
+ closeModal();
72455
+ fetchPrompts();
72456
+ } catch (e) {
72457
+ showError('Save failed: ' + e.message);
72458
+ } finally {
72459
+ saveBtn.disabled = false;
72460
+ saveBtn.textContent = 'Save';
72461
+ }
72462
+ }
72463
+
72464
+ document.getElementById('toggle-btn').onclick = async () => {
72465
+ try {
72466
+ const endpoint = lastPauseState === 'RUNNING' ? '/api/pause' : '/api/resume';
72467
+ await fetch(endpoint, { method: 'POST' });
72468
+ await fetchStatus();
72469
+ } catch (e) {
72470
+ showError('Toggle failed: ' + e.message);
72471
+ }
72472
+ };
72473
+
72474
+ document.addEventListener('keydown', (e) => {
72475
+ if (e.key === 'Escape') { closeModal(); return; }
72476
+ if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { saveModal(); return; }
72477
+ });
72478
+
72479
+ document.getElementById('modal-overlay').addEventListener('click', (e) => {
72480
+ if (e.target === document.getElementById('modal-overlay')) closeModal();
72481
+ });
72482
+
72483
+ fetchStatus();
72484
+ fetchPrompts();
72485
+ setInterval(fetchStatus, 2000);
72486
+ setInterval(fetchPrompts, 5000);
72487
+ </script>
72488
+ </body>
72489
+ </html>`;
72490
+ // Note: [🟑] Code for CLI command [coder server](scripts/run-codex-prompts/server/coderServerHtml.ts) should never be published outside of `@promptbook/cli`
72491
+ // Note: Keep in sync with apps/coder-server/index.html
72492
+
72493
+ /**
72494
+ * Directory from which prompt files are loaded, relative to the current working directory.
72495
+ *
72496
+ * @private internal constant of `ptbk coder server`
72497
+ */
72498
+ const PROMPTS_DIRECTORY_NAME = 'prompts';
72499
+ /**
72500
+ * Starts the lightweight HTTP server that serves the coder kanban UI and REST API.
72501
+ *
72502
+ * API endpoints:
72503
+ * - `GET /` β†’ HTML kanban page
72504
+ * - `GET /api/prompts` β†’ JSON list of all prompt files and sections
72505
+ * - `GET /api/status` β†’ JSON current pause state
72506
+ * - `POST /api/pause` β†’ request pause
72507
+ * - `POST /api/resume` β†’ request resume
72508
+ * - `PUT /api/prompts/update` β†’ update prompt section content
72509
+ *
72510
+ * @private internal utility of `ptbk coder server`
72511
+ */
72512
+ function startCoderHttpServer(port) {
72513
+ const promptsDir = join(process.cwd(), PROMPTS_DIRECTORY_NAME);
72514
+ const server = createServer(async (req, res) => {
72515
+ try {
72516
+ await handleRequest(req, res, promptsDir);
72517
+ }
72518
+ catch (error) {
72519
+ const message = error instanceof Error ? error.message : String(error);
72520
+ console.error(colors.red(`Coder server request error: ${message}`));
72521
+ if (!res.headersSent) {
72522
+ res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
72523
+ }
72524
+ res.end('Internal server error');
72525
+ }
72526
+ });
72527
+ server.listen(port, () => {
72528
+ console.info(spaceTrim$1(`
72529
+ Coder server running at ${colors.cyan(`http://localhost:${port}`)}
72530
+ Open the URL above in your browser to see the kanban board.
72531
+ Press ${colors.bold('p')} to pause / resume the agent runner.
72532
+ `));
72533
+ });
72534
+ return {
72535
+ close: () => {
72536
+ server.close();
72537
+ },
72538
+ };
72539
+ }
72540
+ /**
72541
+ * Routes one HTTP request to the appropriate handler.
72542
+ */
72543
+ async function handleRequest(req, res, promptsDir) {
72544
+ const urlPath = new URL(req.url || '/', `http://localhost`).pathname;
72545
+ const method = (req.method || 'GET').toUpperCase();
72546
+ // Serve the kanban UI
72547
+ if (urlPath === '/' && method === 'GET') {
72548
+ res.writeHead(200, {
72549
+ 'Content-Type': 'text/html; charset=utf-8',
72550
+ 'Cache-Control': 'no-cache',
72551
+ });
72552
+ res.end(CODER_SERVER_HTML);
72553
+ return;
72554
+ }
72555
+ // GET /api/status
72556
+ if (urlPath === '/api/status' && method === 'GET') {
72557
+ res.writeHead(200, jsonHeaders());
72558
+ res.end(JSON.stringify({
72559
+ pauseState: getPauseState(),
72560
+ pauseTargetLabel: getPauseTargetLabel(),
72561
+ }));
72562
+ return;
72563
+ }
72564
+ // GET /api/prompts
72565
+ if (urlPath === '/api/prompts' && method === 'GET') {
72566
+ const promptData = await loadPromptsForApi(promptsDir);
72567
+ res.writeHead(200, jsonHeaders());
72568
+ res.end(JSON.stringify(promptData));
72569
+ return;
72570
+ }
72571
+ // POST /api/pause
72572
+ if (urlPath === '/api/pause' && method === 'POST') {
72573
+ requestPause();
72574
+ res.writeHead(200, jsonHeaders());
72575
+ res.end(JSON.stringify({ pauseState: getPauseState() }));
72576
+ return;
72577
+ }
72578
+ // POST /api/resume
72579
+ if (urlPath === '/api/resume' && method === 'POST') {
72580
+ requestResume();
72581
+ res.writeHead(200, jsonHeaders());
72582
+ res.end(JSON.stringify({ pauseState: getPauseState() }));
72583
+ return;
72584
+ }
72585
+ // PUT /api/prompts/update
72586
+ if (urlPath === '/api/prompts/update' && method === 'PUT') {
72587
+ const body = await readRequestBody(req);
72588
+ const parsed = JSON.parse(body);
72589
+ if (typeof parsed.filePath !== 'string' ||
72590
+ typeof parsed.sectionIndex !== 'number' ||
72591
+ typeof parsed.content !== 'string') {
72592
+ throw new NotAllowed(spaceTrim$1(`
72593
+ Invalid request body for \`PUT /api/prompts/update\`.
72594
+
72595
+ Expected: \`{ filePath: string, sectionIndex: number, content: string }\`
72596
+ `));
72597
+ }
72598
+ await updatePromptSection(parsed.filePath, parsed.sectionIndex, parsed.content);
72599
+ res.writeHead(200, jsonHeaders());
72600
+ res.end(JSON.stringify({ success: true }));
72601
+ return;
72602
+ }
72603
+ res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
72604
+ res.end('Not found');
72605
+ }
72606
+ /**
72607
+ * Loads all prompt files and converts them to the API response shape.
72608
+ */
72609
+ async function loadPromptsForApi(promptsDir) {
72610
+ try {
72611
+ const promptFiles = await loadPromptFiles(promptsDir);
72612
+ return promptFiles.map((file) => ({
72613
+ filePath: file.path,
72614
+ fileName: file.name,
72615
+ sections: file.sections.map((section) => ({
72616
+ index: section.index,
72617
+ status: section.status,
72618
+ priority: section.priority,
72619
+ summary: buildPromptSummary(file, section),
72620
+ content: buildCodexPrompt(file, section),
72621
+ })),
72622
+ }));
72623
+ }
72624
+ catch (error) {
72625
+ // Prompts directory may not exist yet; return empty list
72626
+ if (error.code === 'ENOENT') {
72627
+ return [];
72628
+ }
72629
+ throw error;
72630
+ }
72631
+ }
72632
+ /**
72633
+ * Reads the full request body as a UTF-8 string.
72634
+ */
72635
+ function readRequestBody(req) {
72636
+ return new Promise((resolve, reject) => {
72637
+ const chunks = [];
72638
+ req.on('data', (chunk) => chunks.push(chunk));
72639
+ req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
72640
+ req.on('error', reject);
72641
+ });
72642
+ }
72643
+ /**
72644
+ * Returns standard JSON response headers with no-cache directives.
72645
+ */
72646
+ function jsonHeaders() {
72647
+ return {
72648
+ 'Content-Type': 'application/json; charset=utf-8',
72649
+ 'Cache-Control': 'no-cache',
72650
+ };
72651
+ }
72652
+ // Note: [🟑] Code for CLI command [coder server](scripts/run-codex-prompts/server/runCoderHttpServer.ts) should never be published outside of `@promptbook/cli`
72653
+
72654
+ /**
72655
+ * Starts the coder server: runs prompt processing in keepAlive mode while serving a
72656
+ * kanban web UI and REST API on the given port.
72657
+ *
72658
+ * Differences from `runCodexPrompts`:
72659
+ * - Does not exit when no runnable prompts are available; instead it polls every few seconds
72660
+ * and resumes processing as soon as a new prompt becomes ready.
72661
+ * - Starts a lightweight HTTP server that serves a kanban board at `http://localhost:<port>`
72662
+ * and exposes REST endpoints for reading prompts, controlling pause state, and editing
72663
+ * prompt files directly from the browser.
72664
+ *
72665
+ * @private internal function of `ptbk coder server`
72666
+ */
72667
+ async function runCodexPromptsServer(options) {
72668
+ const { port, ...runOptions } = options;
72669
+ const serverHandle = startCoderHttpServer(port);
72670
+ console.info(colors.gray('Starting prompt runner in server (keep-alive) mode…'));
72671
+ try {
72672
+ await runCodexPrompts({
72673
+ ...runOptions,
72674
+ keepAlive: true,
72675
+ });
72676
+ }
72677
+ finally {
72678
+ serverHandle.close();
72679
+ }
72680
+ }
72681
+ // Note: [🟑] Code for CLI command [coder server](scripts/run-codex-prompts/main/runCodexPromptsServer.ts) should never be published outside of `@promptbook/cli`
72682
+
72683
+ var runCodexPromptsServer$1 = /*#__PURE__*/Object.freeze({
72684
+ __proto__: null,
72685
+ runCodexPromptsServer: runCodexPromptsServer
72686
+ });
72687
+
71541
72688
  /**
71542
72689
  * Path to the directory that holds the prompt markdown files.
71543
72690
  */