@besales/ops-framework 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +328 -0
  3. package/bin/build-check-context.mjs +67 -0
  4. package/bin/build-execution-ledger.mjs +54 -0
  5. package/bin/estimate-llm-input.mjs +160 -0
  6. package/bin/guard-task.mjs +384 -0
  7. package/bin/hash-task-artifacts.mjs +44 -0
  8. package/bin/init-project.mjs +49 -0
  9. package/bin/intake-execution-feedback.mjs +207 -0
  10. package/bin/intake-feedback.test.mjs +73 -0
  11. package/bin/learning-loop.mjs +658 -0
  12. package/bin/learning-loop.test.mjs +175 -0
  13. package/bin/lib/bootstrap-utils.mjs +542 -0
  14. package/bin/lib/bootstrap-utils.test.mjs +156 -0
  15. package/bin/lib/check-context-utils.mjs +1448 -0
  16. package/bin/lib/check-context-utils.test.mjs +497 -0
  17. package/bin/lib/execution-ledger-utils.mjs +162 -0
  18. package/bin/lib/execution-ledger-utils.test.mjs +74 -0
  19. package/bin/lib/llm-input-pack-utils.mjs +663 -0
  20. package/bin/lib/llm-input-pack-utils.test.mjs +262 -0
  21. package/bin/lib/project-config.mjs +229 -0
  22. package/bin/lib/project-config.test.mjs +102 -0
  23. package/bin/lib/task-manifest-utils.mjs +512 -0
  24. package/bin/lib/task-manifest-utils.test.mjs +218 -0
  25. package/bin/lib/task-metrics-utils.mjs +63 -0
  26. package/bin/lib/task-metrics-utils.test.mjs +40 -0
  27. package/bin/lib/test-setup.mjs +37 -0
  28. package/bin/new-task.mjs +42 -0
  29. package/bin/ops-agent.mjs +81 -0
  30. package/bin/preflight.mjs +56 -0
  31. package/bin/providers/external-cli-checker.mjs +190 -0
  32. package/bin/providers/openai-checker.mjs +62 -0
  33. package/bin/quality-gates.mjs +92 -0
  34. package/bin/run-check.mjs +559 -0
  35. package/bin/run-plan-check-loop.mjs +392 -0
  36. package/bin/run-verify.mjs +627 -0
  37. package/bin/self-lint.mjs +88 -0
  38. package/bin/supervisor-turn.mjs +146 -0
  39. package/bin/supervisor-turn.test.mjs +72 -0
  40. package/bin/task-manifest.mjs +57 -0
  41. package/bin/task-metrics.mjs +48 -0
  42. package/bin/transition.mjs +94 -0
  43. package/bin/validate-check-artifacts.mjs +418 -0
  44. package/config/default-agents.json +100 -0
  45. package/package.json +28 -0
  46. package/playbooks/checker-context.md +9 -0
  47. package/playbooks/complexity-performance.md +13 -0
  48. package/playbooks/production-rollout.md +9 -0
  49. package/playbooks/source-sync-provider.md +9 -0
  50. package/playbooks/ui-acceptance.md +9 -0
  51. package/prompts/checker.md +170 -0
  52. package/prompts/executor.md +54 -0
  53. package/prompts/planner.md +128 -0
  54. package/prompts/researcher.md +44 -0
  55. package/prompts/supervisor.md +337 -0
  56. package/prompts/verifier.md +128 -0
  57. package/templates/brief.md +15 -0
  58. package/templates/check-resolution.md +69 -0
  59. package/templates/check-result.json +32 -0
  60. package/templates/check.md +46 -0
  61. package/templates/execution-feedback.md +25 -0
  62. package/templates/execution.md +101 -0
  63. package/templates/human-gate-summary.md +49 -0
  64. package/templates/orchestration-log.md +8 -0
  65. package/templates/plan.md +86 -0
  66. package/templates/research.md +13 -0
  67. package/templates/retrospective.md +48 -0
  68. package/templates/status.md +53 -0
  69. package/templates/verify-result.json +19 -0
  70. package/templates/verify.md +41 -0
@@ -0,0 +1,542 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { resolveProjectContext } from './project-config.mjs';
5
+
6
+ export const DEFAULT_OPS_ROOT = 'ops';
7
+ export const DEFAULT_PIPELINE_ROOT = 'ops/agent-pipeline';
8
+ export const DEFAULT_TASKS_DIR = 'ops/agent-pipeline/tasks';
9
+ export const DEFAULT_MEMORY_DIR = 'ops/agent-pipeline/memory';
10
+ export const DEFAULT_CACHE_DIR = 'ops/agent-pipeline/cache';
11
+ export const DEFAULT_CONFIG_DIR = 'ops/agent-pipeline/config';
12
+ export const DEFAULT_PLAYBOOKS_DIR = 'ops/agent-pipeline/playbooks';
13
+ export const DEFAULT_AGENTS_CONFIG = 'ops/agent-pipeline/config/agents.json';
14
+ export const TASK_ID_PATTERN = /^TASK-\d{3}-[a-z0-9]+(?:-[a-z0-9]+)*$/;
15
+
16
+ export const REQUIRED_MEMORY_FILES = [
17
+ 'project-context-digest.md',
18
+ 'module-boundaries.md',
19
+ 'standards-digest.md',
20
+ 'recurring-failures.md',
21
+ 'domain-glossary.md',
22
+ 'checker-rubric.md',
23
+ ];
24
+
25
+ const __filename = fileURLToPath(import.meta.url);
26
+ const __dirname = path.dirname(__filename);
27
+ const packageRoot = path.resolve(__dirname, '..', '..');
28
+
29
+ export function initProject({
30
+ projectRoot = process.cwd(),
31
+ projectName = path.basename(path.resolve(projectRoot)),
32
+ opsRoot = DEFAULT_OPS_ROOT,
33
+ installScripts = false,
34
+ frameworkPackage = null,
35
+ frameworkVersion = null,
36
+ force = false,
37
+ } = {}) {
38
+ const normalizedProjectRoot = path.resolve(projectRoot);
39
+ const normalizedOpsRoot = normalizeRelativePath(opsRoot, 'ops root');
40
+ const paths = buildProjectPaths({ projectRoot: normalizedProjectRoot, opsRoot: normalizedOpsRoot });
41
+ const changes = [];
42
+
43
+ ensureDirectory(paths.opsRoot, changes);
44
+ ensureDirectory(paths.pipelineRoot, changes);
45
+ ensureDirectory(paths.tasksRoot, changes);
46
+ ensureDirectory(paths.memoryRoot, changes);
47
+ ensureDirectory(paths.cacheRoot, changes);
48
+ ensureDirectory(paths.configRoot, changes);
49
+ ensureDirectory(paths.playbooksRoot, changes);
50
+
51
+ writeFileIfAllowed(paths.projectConfig, buildProjectConfig({ projectName, opsRoot: normalizedOpsRoot }), {
52
+ force,
53
+ changes,
54
+ });
55
+ writeFileIfAllowed(paths.agentsConfig, readDefaultAgentsConfig(), {
56
+ force,
57
+ changes,
58
+ });
59
+ writeFileIfAllowed(path.join(paths.playbooksRoot, 'README.md'), buildProjectPlaybooksReadme(), {
60
+ force,
61
+ changes,
62
+ });
63
+
64
+ for (const fileName of REQUIRED_MEMORY_FILES) {
65
+ writeFileIfAllowed(path.join(paths.memoryRoot, fileName), buildMemoryFile(fileName, projectName), {
66
+ force,
67
+ changes,
68
+ });
69
+ }
70
+
71
+ if (installScripts) {
72
+ upsertPackageScripts({
73
+ packageJsonPath: path.join(normalizedProjectRoot, 'package.json'),
74
+ packageSpec: buildFrameworkPackageSpec({ frameworkPackage, frameworkVersion }),
75
+ changes,
76
+ });
77
+ }
78
+
79
+ return {
80
+ projectRoot: normalizedProjectRoot,
81
+ projectName,
82
+ opsRoot: normalizedOpsRoot,
83
+ paths,
84
+ changes,
85
+ };
86
+ }
87
+
88
+ export function buildFrameworkPackageSpec({ frameworkPackage = null, frameworkVersion = null } = {}) {
89
+ const ownPackageJson = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
90
+ const packageName = frameworkPackage || ownPackageJson.name || '@besales/ops-framework';
91
+ const version = frameworkVersion || ownPackageJson.version;
92
+ return version ? `${packageName}@${version}` : packageName;
93
+ }
94
+
95
+ export function buildOpsScripts(packageSpec) {
96
+ const run = (command) => `corepack yarn dlx ${packageSpec} ${command}`;
97
+ return {
98
+ ops: `corepack yarn dlx ${packageSpec}`,
99
+ 'agent:build-check-context': run('build-check-context'),
100
+ 'agent:validate-check-artifacts': run('validate-check-artifacts'),
101
+ 'agent:manifest': run('manifest'),
102
+ 'agent:preflight': run('preflight'),
103
+ 'agent:transition': run('transition'),
104
+ 'agent:estimate-llm-input': run('estimate-llm-input'),
105
+ 'agent:quality-gates': run('quality-gates'),
106
+ 'agent:run-check': run('run-check'),
107
+ 'agent:run-verify': run('run-verify'),
108
+ 'agent:hash-task-artifacts': run('hash-task-artifacts'),
109
+ 'agent:build-execution-ledger': run('build-execution-ledger'),
110
+ 'agent:task-metrics': run('task-metrics'),
111
+ 'agent:run-plan-check-loop': run('run-plan-check-loop'),
112
+ 'agent:supervisor-turn': run('supervisor-turn'),
113
+ 'agent:intake-feedback': run('intake-feedback'),
114
+ 'agent:intake-execution-feedback': run('intake-execution-feedback'),
115
+ 'agent:guard-task': run('guard-task'),
116
+ 'agent:memory-candidates': run('memory-candidates'),
117
+ 'agent:learning-index': run('learning-index'),
118
+ 'agent:learning-review': run('learning-review'),
119
+ 'agent:update-memory': run('update-memory'),
120
+ 'agent:learning-audit': run('learning-audit'),
121
+ 'agent:learning-report': run('learning-report'),
122
+ 'agent:test': run('test/self-test'),
123
+ };
124
+ }
125
+
126
+ function upsertPackageScripts({ packageJsonPath, packageSpec, changes }) {
127
+ if (!fs.existsSync(packageJsonPath)) {
128
+ changes.push({
129
+ path: packageJsonPath,
130
+ kind: 'file',
131
+ status: 'existing',
132
+ note: 'package.json not found; ops scripts not installed',
133
+ });
134
+ return;
135
+ }
136
+
137
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
138
+ packageJson.scripts = {
139
+ ...(packageJson.scripts || {}),
140
+ ...buildOpsScripts(packageSpec),
141
+ };
142
+ fs.writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`);
143
+ changes.push({ path: packageJsonPath, kind: 'file', status: 'overwritten' });
144
+ }
145
+
146
+ export function createTask({
147
+ projectRoot = process.cwd(),
148
+ taskId,
149
+ title,
150
+ owner = 'Supervisor',
151
+ force = false,
152
+ } = {}) {
153
+ if (!TASK_ID_PATTERN.test(taskId || '')) {
154
+ throw new Error(`Task id must match ${TASK_ID_PATTERN}: ${taskId || '<missing>'}`);
155
+ }
156
+ const normalizedProjectRoot = path.resolve(projectRoot);
157
+ const projectContext = resolveProjectContext({ cwd: normalizedProjectRoot });
158
+ const tasksRoot = projectContext.configPath
159
+ ? projectContext.tasksRoot
160
+ : buildProjectPaths({ projectRoot: normalizedProjectRoot, opsRoot: DEFAULT_OPS_ROOT }).tasksRoot;
161
+ const paths = { tasksRoot };
162
+ const taskDir = path.join(paths.tasksRoot, taskId);
163
+ const taskTitle = title || humanizeTaskId(taskId);
164
+ const changes = [];
165
+
166
+ ensureDirectory(paths.tasksRoot, changes);
167
+ ensureDirectory(taskDir, changes);
168
+
169
+ const files = new Map([
170
+ ['brief.md', buildBrief({ taskTitle })],
171
+ ['research.md', buildResearch()],
172
+ ['plan.md', buildPlan()],
173
+ ['status.md', buildStatus({ taskId, owner })],
174
+ ['orchestration-log.md', buildOrchestrationLog({ taskId })],
175
+ ]);
176
+
177
+ for (const [fileName, content] of files.entries()) {
178
+ writeFileIfAllowed(path.join(taskDir, fileName), content, { force, changes });
179
+ }
180
+
181
+ return {
182
+ projectRoot: normalizedProjectRoot,
183
+ taskId,
184
+ taskDir,
185
+ changes,
186
+ };
187
+ }
188
+
189
+ export function summarizeChanges(changes) {
190
+ const summary = {
191
+ created: 0,
192
+ overwritten: 0,
193
+ existing: 0,
194
+ };
195
+ for (const change of changes) {
196
+ summary[change.status] += 1;
197
+ }
198
+ return summary;
199
+ }
200
+
201
+ export function assertPortableGeneratedConfig({ projectConfigContent, agentsConfigContent }) {
202
+ const combined = `${projectConfigContent}\n${agentsConfigContent}`;
203
+ const forbiddenPatterns = [
204
+ /\/Users\//,
205
+ /\/private\/tmp\//,
206
+ new RegExp(['Leak', 'Engine'].join('')),
207
+ new RegExp(['Delivery', 'Os'].join('')),
208
+ new RegExp(['Personal', 'Blog'].join('')),
209
+ /\.env/,
210
+ /access[_-]?token/i,
211
+ /api[_-]?key/i,
212
+ /secret[_-]?(key|value)?/i,
213
+ ];
214
+
215
+ for (const pattern of forbiddenPatterns) {
216
+ if (pattern.test(combined)) {
217
+ throw new Error(`Generated config is not portable; matched ${pattern}`);
218
+ }
219
+ }
220
+ }
221
+
222
+ function buildProjectPaths({ projectRoot, opsRoot }) {
223
+ const pipelineRoot = `${opsRoot}/agent-pipeline`;
224
+ return {
225
+ opsRoot: path.join(projectRoot, opsRoot),
226
+ pipelineRoot: path.join(projectRoot, pipelineRoot),
227
+ tasksRoot: path.join(projectRoot, pipelineRoot, 'tasks'),
228
+ memoryRoot: path.join(projectRoot, pipelineRoot, 'memory'),
229
+ cacheRoot: path.join(projectRoot, pipelineRoot, 'cache'),
230
+ configRoot: path.join(projectRoot, pipelineRoot, 'config'),
231
+ playbooksRoot: path.join(projectRoot, pipelineRoot, 'playbooks'),
232
+ projectConfig: path.join(projectRoot, opsRoot, 'project.ops.yaml'),
233
+ agentsConfig: path.join(projectRoot, pipelineRoot, 'config', 'agents.json'),
234
+ };
235
+ }
236
+
237
+ function normalizeRelativePath(value, label) {
238
+ if (!value || path.isAbsolute(value) || value.includes('..')) {
239
+ throw new Error(`${label} must be a project-relative path without '..': ${value || '<missing>'}`);
240
+ }
241
+ return value.replace(/^\.\/+/, '').replace(/\/+$/, '') || DEFAULT_OPS_ROOT;
242
+ }
243
+
244
+ function ensureDirectory(dirPath, changes) {
245
+ if (fs.existsSync(dirPath)) {
246
+ changes.push({ path: dirPath, kind: 'dir', status: 'existing' });
247
+ return;
248
+ }
249
+ fs.mkdirSync(dirPath, { recursive: true });
250
+ changes.push({ path: dirPath, kind: 'dir', status: 'created' });
251
+ }
252
+
253
+ function writeFileIfAllowed(filePath, content, { force, changes }) {
254
+ const normalizedContent = content.endsWith('\n') ? content : `${content}\n`;
255
+ if (fs.existsSync(filePath) && !force) {
256
+ changes.push({ path: filePath, kind: 'file', status: 'existing' });
257
+ return;
258
+ }
259
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
260
+ const status = fs.existsSync(filePath) ? 'overwritten' : 'created';
261
+ fs.writeFileSync(filePath, normalizedContent);
262
+ changes.push({ path: filePath, kind: 'file', status });
263
+ }
264
+
265
+ function buildProjectConfig({ projectName, opsRoot }) {
266
+ return [
267
+ `name: ${sanitizeYamlScalar(projectName)}`,
268
+ 'ops:',
269
+ ` legacyPipelineDir: ${opsRoot}/agent-pipeline`,
270
+ ` tasksDir: ${opsRoot}/agent-pipeline/tasks`,
271
+ ` memoryDir: ${opsRoot}/agent-pipeline/memory`,
272
+ ` cacheDir: ${opsRoot}/agent-pipeline/cache`,
273
+ ` playbooksDir: ${opsRoot}/agent-pipeline/playbooks`,
274
+ 'agents:',
275
+ ` configFile: ${opsRoot}/agent-pipeline/config/agents.json`,
276
+ 'risk:',
277
+ ' uiRoots:',
278
+ ' # - apps/web',
279
+ ' backendRoots:',
280
+ ' # - apps/api',
281
+ ' workerRoots:',
282
+ ' # - apps/workers',
283
+ '',
284
+ ].join('\n');
285
+ }
286
+
287
+ function readDefaultAgentsConfig() {
288
+ const content = fs.readFileSync(path.join(packageRoot, 'config', 'default-agents.json'), 'utf8');
289
+ const parsed = JSON.parse(content);
290
+ return `${JSON.stringify(parsed, null, 2)}\n`;
291
+ }
292
+
293
+ function buildMemoryFile(fileName, projectName) {
294
+ const title = fileName.replace(/\.md$/, '').split('-').map(capitalize).join(' ');
295
+ return [
296
+ `# ${title}`,
297
+ '',
298
+ `Project: ${projectName}`,
299
+ '',
300
+ 'Bootstrap placeholder. Update this file with project-specific memory as the project evolves.',
301
+ '',
302
+ ].join('\n');
303
+ }
304
+
305
+ function buildProjectPlaybooksReadme() {
306
+ return [
307
+ '# Project Playbooks',
308
+ '',
309
+ 'Shared playbooks live in the Ops Framework package.',
310
+ '',
311
+ 'Project playbooks live here and should contain project-specific routes, commands, services, environments and provider quirks.',
312
+ '',
313
+ 'When a project playbook has the same file name as a shared playbook, it extends the shared playbook as a project overlay.',
314
+ '',
315
+ 'Do not copy secrets or local machine paths into project playbooks.',
316
+ '',
317
+ ].join('\n');
318
+ }
319
+
320
+ function buildBrief({ taskTitle }) {
321
+ return [
322
+ '# Brief',
323
+ '',
324
+ '## Задача',
325
+ '',
326
+ taskTitle,
327
+ '',
328
+ '## Зачем это нужно',
329
+ '',
330
+ 'Проверить, что Ops Framework bootstrap работает в этом проекте.',
331
+ '',
332
+ '## Критерии успеха',
333
+ '',
334
+ '- Task artifacts созданы в project-owned `ops/**`.',
335
+ '- Framework lifecycle commands запускаются из project root.',
336
+ '',
337
+ '## Ограничения',
338
+ '',
339
+ '- Не трогать runtime-код проекта без отдельного approval.',
340
+ '',
341
+ '## Что точно вне scope',
342
+ '',
343
+ '- Deploy, DB migrations, provider integrations.',
344
+ '',
345
+ '## Что дал человек-владелец',
346
+ '',
347
+ '- Bootstrap smoke task.',
348
+ '',
349
+ '## Открытые вопросы',
350
+ '',
351
+ '- Нет.',
352
+ '',
353
+ ].join('\n');
354
+ }
355
+
356
+ function buildResearch() {
357
+ return [
358
+ '# Research',
359
+ '',
360
+ '## Цель исследования',
361
+ '',
362
+ 'Подтвердить bootstrap-ready состояние проекта.',
363
+ '',
364
+ '## Подтвержденные repo findings',
365
+ '',
366
+ '- Project-owned ops structure создана локально.',
367
+ '',
368
+ '## Затронутые модули и файлы',
369
+ '',
370
+ '- `ops/**`',
371
+ '',
372
+ '## Риски и ограничения',
373
+ '',
374
+ '- Runtime-код не меняется.',
375
+ '',
376
+ '## Открытые вопросы',
377
+ '',
378
+ '- Нет.',
379
+ '',
380
+ '## Рекомендации для planning',
381
+ '',
382
+ '- Использовать docs-only/local-only проверку.',
383
+ '',
384
+ ].join('\n');
385
+ }
386
+
387
+ function buildPlan() {
388
+ return [
389
+ '# Plan',
390
+ '',
391
+ '## Цель',
392
+ '',
393
+ 'Проверить минимальный lifecycle Ops Framework в новом проекте.',
394
+ '',
395
+ '## Опора на research',
396
+ '',
397
+ '- Project-owned ops structure создана.',
398
+ '',
399
+ '## Допущения',
400
+ '',
401
+ '- Проверка docs-only/local-only.',
402
+ '',
403
+ '## Затронутые модули и файлы',
404
+ '',
405
+ '- `ops/**`',
406
+ '',
407
+ '## Risk tier and execution budget',
408
+ '',
409
+ '- Risk tier: `R1`',
410
+ '- Speed mode: `Fast`',
411
+ '- Approved execution target: project-owned `ops/**`.',
412
+ '- Fast-loop allowed inside this slice: task artifact updates only.',
413
+ '- Requires return to Plan/Check if product code, secrets, data storage or external integrations become involved.',
414
+ '',
415
+ '## Checker Context Hints',
416
+ '',
417
+ '- Required repo files for Checker to inspect: `ops/**`.',
418
+ '- Prior relevant failures: none.',
419
+ '- Exact questions Checker must answer: bootstrap artifacts stay project-owned.',
420
+ '- Relevant playbooks: checker-context, complexity-performance.',
421
+ '',
422
+ '## Шаги реализации',
423
+ '',
424
+ '1. Собрать check context.',
425
+ '2. Проверить manifest/preflight.',
426
+ '',
427
+ '## Риски и открытые вопросы',
428
+ '',
429
+ '- Нет.',
430
+ '',
431
+ '## План проверки',
432
+ '',
433
+ '### Verification ladder',
434
+ '',
435
+ '- Micro-verify during Execute: manifest/preflight/build-check-context.',
436
+ '- Slice-verify before completion: generated artifacts remain under project-owned `ops/**`.',
437
+ '- External Verify required before closeout: `no`, docs-only smoke.',
438
+ '',
439
+ '## Что требует human approval',
440
+ '',
441
+ '- Любые изменения вне project-owned `ops/**`.',
442
+ '',
443
+ ].join('\n');
444
+ }
445
+
446
+ function buildStatus({ taskId, owner }) {
447
+ return [
448
+ '# Status',
449
+ '',
450
+ '## Task ID',
451
+ '',
452
+ `\`${taskId}\``,
453
+ '',
454
+ '## Текущий этап',
455
+ '',
456
+ 'Brief',
457
+ '',
458
+ '## Текущий owner',
459
+ '',
460
+ `\`${owner}\``,
461
+ '',
462
+ '## Latest routing decision',
463
+ '',
464
+ '`task_created_by_ops_agent_new_task`',
465
+ '',
466
+ '## Plan SHA',
467
+ '',
468
+ '`not_started`',
469
+ '',
470
+ '## Memory SHA',
471
+ '',
472
+ '`not_started`',
473
+ '',
474
+ '## Latest check verdict',
475
+ '',
476
+ '`not_started`',
477
+ '',
478
+ '## Latest check result',
479
+ '',
480
+ '- `check.result.json`: missing',
481
+ '',
482
+ '## Latest check resolution',
483
+ '',
484
+ '- `check-resolution.md`: not_required',
485
+ '',
486
+ '## Human Gate summary',
487
+ '',
488
+ '- `human-gate-summary.md`: not_required',
489
+ '',
490
+ '## Feedback intake',
491
+ '',
492
+ '- `feedback.md`: no_events',
493
+ '',
494
+ '## Latest verify verdict',
495
+ '',
496
+ '`not_started`',
497
+ '',
498
+ '## Latest verify result',
499
+ '',
500
+ '- `verify.result.json`: missing',
501
+ '',
502
+ '## Последнее действие supervisor',
503
+ '',
504
+ 'Task artifacts created by `ops-agent new-task`.',
505
+ '',
506
+ '## Что ожидается на выходе текущего этапа',
507
+ '',
508
+ 'Fill task brief or run bootstrap smoke validation.',
509
+ '',
510
+ '## Нужен ли сейчас human approval',
511
+ '',
512
+ 'no',
513
+ '',
514
+ '## Следующий шаг',
515
+ '',
516
+ 'Continue framework lifecycle.',
517
+ '',
518
+ ].join('\n');
519
+ }
520
+
521
+ function buildOrchestrationLog({ taskId }) {
522
+ return [
523
+ '# Orchestration Log',
524
+ '',
525
+ '## Entries',
526
+ '',
527
+ `- \`${new Date().toISOString()}\` — task \`${taskId}\` created by \`ops-agent new-task\`.`,
528
+ '',
529
+ ].join('\n');
530
+ }
531
+
532
+ function humanizeTaskId(taskId) {
533
+ return taskId.replace(/^TASK-\d{3}-/, '').split('-').map(capitalize).join(' ');
534
+ }
535
+
536
+ function capitalize(value) {
537
+ return value ? `${value[0].toUpperCase()}${value.slice(1)}` : value;
538
+ }
539
+
540
+ function sanitizeYamlScalar(value) {
541
+ return String(value || 'Project').replace(/[^A-Za-z0-9_-]/g, '-');
542
+ }
@@ -0,0 +1,156 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { afterEach, describe, expect, it } from 'vitest';
7
+ import {
8
+ assertPortableGeneratedConfig,
9
+ buildOpsScripts,
10
+ createTask,
11
+ initProject,
12
+ REQUIRED_MEMORY_FILES,
13
+ } from './bootstrap-utils.mjs';
14
+
15
+ const __filename = fileURLToPath(import.meta.url);
16
+ const __dirname = path.dirname(__filename);
17
+ const opsAgentBin = path.resolve(__dirname, '..', 'ops-agent.mjs');
18
+ const tempDirs = [];
19
+
20
+ afterEach(() => {
21
+ for (const dir of tempDirs.splice(0)) {
22
+ fs.rmSync(dir, { recursive: true, force: true });
23
+ }
24
+ });
25
+
26
+ describe('bootstrap utilities', () => {
27
+ it('initializes a project-owned ops structure without overwriting existing files', () => {
28
+ const root = makeTempProject();
29
+ const first = initProject({ projectRoot: root, projectName: 'FixtureProject' });
30
+ const projectConfigPath = path.join(root, 'ops', 'project.ops.yaml');
31
+ const originalConfig = fs.readFileSync(projectConfigPath, 'utf8');
32
+
33
+ fs.writeFileSync(projectConfigPath, `${originalConfig}# custom note\n`);
34
+ const second = initProject({ projectRoot: root, projectName: 'FixtureProject' });
35
+
36
+ expect(first.changes.some((change) => change.status === 'created')).toBe(true);
37
+ expect(second.changes.some((change) => change.status === 'overwritten')).toBe(false);
38
+ expect(fs.readFileSync(projectConfigPath, 'utf8')).toContain('# custom note');
39
+ for (const fileName of REQUIRED_MEMORY_FILES) {
40
+ expect(fs.existsSync(path.join(root, 'ops', 'agent-pipeline', 'memory', fileName))).toBe(true);
41
+ }
42
+ expect(fs.existsSync(path.join(root, 'ops', 'agent-pipeline', 'playbooks', 'README.md'))).toBe(true);
43
+ });
44
+
45
+ it('creates portable project config and agents config', () => {
46
+ const root = makeTempProject();
47
+ initProject({ projectRoot: root, projectName: 'PortableProject' });
48
+
49
+ const projectConfigContent = fs.readFileSync(path.join(root, 'ops', 'project.ops.yaml'), 'utf8');
50
+ const agentsConfigContent = fs.readFileSync(path.join(root, 'ops', 'agent-pipeline', 'config', 'agents.json'), 'utf8');
51
+
52
+ expect(projectConfigContent).toContain('tasksDir: ops/agent-pipeline/tasks');
53
+ expect(projectConfigContent).toContain('memoryDir: ops/agent-pipeline/memory');
54
+ expect(projectConfigContent).toContain('# - apps/web');
55
+ expect(agentsConfigContent).toContain('${CODEX_CLI_COMMAND}');
56
+ expect(() => assertPortableGeneratedConfig({ projectConfigContent, agentsConfigContent })).not.toThrow();
57
+ });
58
+
59
+ it('can install dlx-based package scripts without adding package dependencies', () => {
60
+ const root = makeTempProject();
61
+ fs.writeFileSync(path.join(root, 'package.json'), `${JSON.stringify({
62
+ name: 'fixture-project',
63
+ private: true,
64
+ scripts: {
65
+ test: 'echo ok',
66
+ },
67
+ dependencies: {
68
+ leftpad: '1.0.0',
69
+ },
70
+ }, null, 2)}\n`);
71
+
72
+ initProject({
73
+ projectRoot: root,
74
+ projectName: 'ScriptProject',
75
+ installScripts: true,
76
+ frameworkPackage: '@besales/ops-framework',
77
+ frameworkVersion: '0.1.0',
78
+ });
79
+
80
+ const packageJson = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
81
+ expect(packageJson.scripts.test).toBe('echo ok');
82
+ expect(packageJson.scripts.ops).toBe('corepack yarn dlx @besales/ops-framework@0.1.0');
83
+ expect(packageJson.scripts['agent:run-verify']).toBe('corepack yarn dlx @besales/ops-framework@0.1.0 run-verify');
84
+ expect(packageJson.dependencies.leftpad).toBe('1.0.0');
85
+ expect(packageJson.devDependencies?.['@besales/ops-framework']).toBeUndefined();
86
+ });
87
+
88
+ it('creates a minimal task artifact set', () => {
89
+ const root = makeTempProject();
90
+ initProject({ projectRoot: root, projectName: 'TaskProject' });
91
+
92
+ const result = createTask({
93
+ projectRoot: root,
94
+ taskId: 'TASK-000-bootstrap-smoke',
95
+ title: 'Bootstrap smoke',
96
+ });
97
+
98
+ expect(result.taskDir).toBe(path.join(root, 'ops', 'agent-pipeline', 'tasks', 'TASK-000-bootstrap-smoke'));
99
+ for (const fileName of ['brief.md', 'research.md', 'plan.md', 'status.md', 'orchestration-log.md']) {
100
+ expect(fs.existsSync(path.join(result.taskDir, fileName))).toBe(true);
101
+ }
102
+ });
103
+
104
+ it('respects a custom ops root from project config when creating tasks', () => {
105
+ const root = makeTempProject();
106
+ initProject({ projectRoot: root, projectName: 'CustomProject', opsRoot: '.ops' });
107
+
108
+ const result = createTask({
109
+ projectRoot: root,
110
+ taskId: 'TASK-001-custom-root',
111
+ title: 'Custom root',
112
+ });
113
+
114
+ expect(result.taskDir).toBe(path.join(root, '.ops', 'agent-pipeline', 'tasks', 'TASK-001-custom-root'));
115
+ expect(fs.existsSync(path.join(root, '.ops', 'agent-pipeline', 'tasks', 'TASK-001-custom-root', 'brief.md'))).toBe(true);
116
+ expect(fs.existsSync(path.join(root, 'ops', 'agent-pipeline', 'tasks', 'TASK-001-custom-root'))).toBe(false);
117
+ });
118
+
119
+ it('supports fixture lifecycle through the ops-agent bin', () => {
120
+ const root = makeTempProject();
121
+
122
+ expect(runOpsAgent(root, ['init', '--project-name', 'FixtureProject']).status).toBe(0);
123
+ expect(runOpsAgent(root, ['init', '--project-name', 'FixtureProject']).status).toBe(0);
124
+ expect(runOpsAgent(root, ['new-task', 'TASK-000-bootstrap-smoke', '--title', 'Bootstrap smoke']).status).toBe(0);
125
+ expect(runOpsAgent(root, ['manifest', 'TASK-000-bootstrap-smoke', '--json']).status).toBe(0);
126
+ expect(runOpsAgent(root, ['preflight', 'TASK-000-bootstrap-smoke', '--target', 'check']).status).toBe(0);
127
+ expect(runOpsAgent(root, ['build-check-context', 'TASK-000-bootstrap-smoke']).status).toBe(0);
128
+
129
+ const taskDir = path.join(root, 'ops', 'agent-pipeline', 'tasks', 'TASK-000-bootstrap-smoke');
130
+ expect(fs.existsSync(path.join(taskDir, 'check-context.json'))).toBe(true);
131
+ expect(fs.existsSync(path.join(taskDir, 'checker-context-pack.md'))).toBe(true);
132
+ });
133
+ });
134
+
135
+ describe('buildOpsScripts', () => {
136
+ it('generates the standard project script surface', () => {
137
+ const scripts = buildOpsScripts('@besales/ops-framework@0.1.0');
138
+
139
+ expect(scripts.ops).toBe('corepack yarn dlx @besales/ops-framework@0.1.0');
140
+ expect(scripts['agent:quality-gates']).toBe('corepack yarn dlx @besales/ops-framework@0.1.0 quality-gates');
141
+ expect(scripts['agent:test']).toBe('corepack yarn dlx @besales/ops-framework@0.1.0 test/self-test');
142
+ });
143
+ });
144
+
145
+ function makeTempProject() {
146
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ops-bootstrap-'));
147
+ tempDirs.push(root);
148
+ return root;
149
+ }
150
+
151
+ function runOpsAgent(cwd, args) {
152
+ return spawnSync(process.execPath, [opsAgentBin, ...args], {
153
+ cwd,
154
+ encoding: 'utf8',
155
+ });
156
+ }