@besales/ops-framework 0.1.6 → 0.1.8

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.8
4
+
5
+ - Added `initiative-intake` for source-doc indexing, source SHA tracking and deterministic requirement candidates.
6
+ - Added `initiative-requirements` to create `requirements-map.md` and `coverage.md` from intake candidates.
7
+ - Added generated project scripts for initiative intake and requirements mapping.
8
+
9
+ ## 0.1.7
10
+
11
+ - Added the first Initiative Framework layer above tasks for MVPs, large feature trains and multi-phase work.
12
+ - Added `initiative-create`, `initiative-add-work-package`, `initiative-status` and `initiative-next`.
13
+ - Added work-package task materialization so a work package remains a normal task while slices stay inside `plan.md` and `execution.md`.
14
+ - Added `ops.initiativesDir` project config support and generated project scripts for initiative commands.
15
+
3
16
  ## 0.1.6
4
17
 
5
18
  - Added `ops-agent closeout <TASK>` as the Supervisor entrypoint before final task closure.
package/README.md CHANGED
@@ -180,6 +180,12 @@ Do not commit that `file:` dependency to production projects. It is only for pac
180
180
  - `learning-report`
181
181
  - `learning-closeout`
182
182
  - `closeout`
183
+ - `initiative-create`
184
+ - `initiative-add-work-package`
185
+ - `initiative-status`
186
+ - `initiative-next`
187
+ - `initiative-intake`
188
+ - `initiative-requirements`
183
189
  - `test/self-test`
184
190
 
185
191
  ## Learning Loop
@@ -219,6 +225,41 @@ ops-agent closeout TASK-001-example
219
225
 
220
226
  Shared playbook candidates are intentionally manual-review only. Promote them through a separate reviewed framework task, not by auto-writing project-specific observations into the shared package.
221
227
 
228
+ ## Initiative Framework
229
+
230
+ Initiatives are the program-level layer above tasks. Use them for MVPs, large feature trains and multi-phase functionality where a one-task-at-a-time flow would be too slow.
231
+
232
+ ```bash
233
+ ops-agent initiative-create delivery-os-mvp --title "Delivery OS MVP" --mode fast_mvp
234
+ ops-agent initiative-add-work-package delivery-os-mvp WP-001-foundation --title "Foundation"
235
+ ops-agent initiative-intake delivery-os-mvp docs/delivery-os
236
+ ops-agent initiative-requirements delivery-os-mvp
237
+ ops-agent initiative-status delivery-os-mvp
238
+ ops-agent initiative-next delivery-os-mvp --materialize-task
239
+ ```
240
+
241
+ The hierarchy is:
242
+
243
+ ```text
244
+ Initiative
245
+ -> Work-package task
246
+ -> Execution slices inside plan.md/execution.md
247
+ ```
248
+
249
+ Work packages are materialized as normal tasks, so `brief/research/plan/check/execute/verify/retrospective/learning` still works at the audit boundary. Slices are not separate tasks; they use fast execution, micro-verify and slice evidence inside the work-package task. Create a separate task only when a slice triggers scope, risk, architecture or human-approval escalation.
250
+
251
+ For source docs, use intake before work-package planning:
252
+
253
+ ```text
254
+ Raw docs
255
+ -> initiative intake/source-index
256
+ -> requirements-map.md
257
+ -> work packages
258
+ -> tasks
259
+ ```
260
+
261
+ `initiative-intake <initiative> <docs-dir>` indexes supported source documents, writes source SHA hashes and deterministic requirement candidates into `intake/`. `initiative-requirements <initiative>` turns those candidates into `REQ-*` rows in `requirements-map.md` and writes `coverage.md`. Human review is still required before treating candidates as approved or planned.
262
+
222
263
  ## Feedback Intake
223
264
 
224
265
  Feedback is stage-agnostic. Any user question, correction, review note or learning observation during an active task should be captured before it is acted on:
@@ -0,0 +1,823 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import crypto from 'node:crypto';
4
+ import { fileURLToPath } from 'node:url';
5
+ import {
6
+ getFlag,
7
+ parseCliArgs,
8
+ updateMarkdownSection,
9
+ } from './lib/check-context-utils.mjs';
10
+ import {
11
+ createTask,
12
+ summarizeChanges,
13
+ } from './lib/bootstrap-utils.mjs';
14
+ import { resolveProjectContext } from './lib/project-config.mjs';
15
+
16
+ const INITIATIVE_ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
17
+ const WORK_PACKAGE_ID_PATTERN = /^WP-\d{3}-[a-z0-9][a-z0-9-]*$/;
18
+ const SUPPORTED_SOURCE_EXTENSIONS = new Set(['.md', '.markdown', '.txt', '.json', '.yaml', '.yml']);
19
+
20
+ export function main() {
21
+ const command = process.argv[2];
22
+ const args = parseCliArgs(process.argv.slice(3));
23
+ try {
24
+ if (command === 'initiative-create') {
25
+ const result = createInitiative({
26
+ projectRoot: process.cwd(),
27
+ initiativeId: args.positional[0],
28
+ title: getFlag(args, 'title', null),
29
+ goal: getFlag(args, 'goal', null),
30
+ mode: getFlag(args, 'mode', 'fast_mvp'),
31
+ force: args.flags.has('force'),
32
+ });
33
+ printChangeSummary(`Initiative created: ${result.initiativeId}`, result.changes);
34
+ return;
35
+ }
36
+ if (command === 'initiative-add-work-package') {
37
+ const result = addWorkPackage({
38
+ projectRoot: process.cwd(),
39
+ initiativeId: args.positional[0],
40
+ workPackageId: args.positional[1],
41
+ title: getFlag(args, 'title', null),
42
+ goal: getFlag(args, 'goal', null),
43
+ mode: getFlag(args, 'mode', 'standard_work_package'),
44
+ force: args.flags.has('force'),
45
+ });
46
+ printChangeSummary(`Work package added: ${result.workPackageId}`, result.changes);
47
+ return;
48
+ }
49
+ if (command === 'initiative-status') {
50
+ const result = initiativeStatus({
51
+ projectRoot: process.cwd(),
52
+ initiativeId: args.positional[0],
53
+ });
54
+ printInitiativeStatus(result);
55
+ return;
56
+ }
57
+ if (command === 'initiative-next') {
58
+ const result = initiativeNext({
59
+ projectRoot: process.cwd(),
60
+ initiativeId: args.positional[0],
61
+ materializeTask: args.flags.has('materialize-task'),
62
+ taskId: getFlag(args, 'task-id', null),
63
+ force: args.flags.has('force'),
64
+ });
65
+ printInitiativeNext(result);
66
+ return;
67
+ }
68
+ if (command === 'initiative-intake') {
69
+ const result = initiativeIntake({
70
+ projectRoot: process.cwd(),
71
+ initiativeId: args.positional[0],
72
+ docsDir: args.positional[1],
73
+ force: args.flags.has('force'),
74
+ });
75
+ printChangeSummary(`Initiative intake written: ${result.initiativeId}`, result.changes);
76
+ console.log(`- sources: ${result.sources.length}`);
77
+ console.log(`- requirement candidates: ${result.candidates.length}`);
78
+ return;
79
+ }
80
+ if (command === 'initiative-requirements') {
81
+ const result = initiativeRequirements({
82
+ projectRoot: process.cwd(),
83
+ initiativeId: args.positional[0],
84
+ force: args.flags.has('force'),
85
+ });
86
+ printChangeSummary(`Initiative requirements map written: ${result.initiativeId}`, result.changes);
87
+ console.log(`- requirements: ${result.requirements.length}`);
88
+ return;
89
+ }
90
+ fail('Usage: ops-agent initiative-create|initiative-add-work-package|initiative-status|initiative-next|initiative-intake|initiative-requirements ...');
91
+ } catch (error) {
92
+ fail(error.message);
93
+ }
94
+ }
95
+
96
+ export function createInitiative({
97
+ projectRoot = process.cwd(),
98
+ initiativeId,
99
+ title,
100
+ goal,
101
+ mode = 'fast_mvp',
102
+ force = false,
103
+ } = {}) {
104
+ assertInitiativeId(initiativeId);
105
+ const context = resolveProjectContext({ cwd: projectRoot });
106
+ const initiativeDir = path.join(context.initiativesRoot, initiativeId);
107
+ const workPackagesDir = path.join(initiativeDir, 'work-packages');
108
+ const changes = [];
109
+ ensureDirectory(context.initiativesRoot, changes);
110
+ ensureDirectory(initiativeDir, changes);
111
+ ensureDirectory(workPackagesDir, changes);
112
+ const displayTitle = title || humanizeSlug(initiativeId);
113
+ const displayGoal = goal || 'Define MVP/program outcome before planning work packages.';
114
+ writeFileIfAllowed(path.join(initiativeDir, 'initiative.yaml'), buildInitiativeYaml({
115
+ initiativeId,
116
+ title: displayTitle,
117
+ goal: displayGoal,
118
+ mode,
119
+ }), { force, changes });
120
+ writeFileIfAllowed(path.join(initiativeDir, 'initiative.md'), buildInitiativeMarkdown({
121
+ initiativeId,
122
+ title: displayTitle,
123
+ goal: displayGoal,
124
+ mode,
125
+ }), { force, changes });
126
+ writeFileIfAllowed(path.join(initiativeDir, 'status.md'), buildInitiativeStatus({
127
+ initiativeId,
128
+ title: displayTitle,
129
+ }), { force, changes });
130
+ writeFileIfAllowed(path.join(workPackagesDir, 'README.md'), buildWorkPackagesReadme(), { force, changes });
131
+ return {
132
+ initiativeId,
133
+ initiativeDir,
134
+ changes,
135
+ };
136
+ }
137
+
138
+ export function addWorkPackage({
139
+ projectRoot = process.cwd(),
140
+ initiativeId,
141
+ workPackageId,
142
+ title,
143
+ goal,
144
+ mode = 'standard_work_package',
145
+ force = false,
146
+ } = {}) {
147
+ assertInitiativeId(initiativeId);
148
+ assertWorkPackageId(workPackageId);
149
+ const context = resolveProjectContext({ cwd: projectRoot });
150
+ const initiativeDir = path.join(context.initiativesRoot, initiativeId);
151
+ if (!fs.existsSync(initiativeDir)) {
152
+ throw new Error(`Initiative not found: ${initiativeId}. Run initiative-create first.`);
153
+ }
154
+ const workPackageDir = path.join(initiativeDir, 'work-packages', workPackageId);
155
+ const changes = [];
156
+ ensureDirectory(workPackageDir, changes);
157
+ writeFileIfAllowed(path.join(workPackageDir, 'work-package.md'), buildWorkPackageMarkdown({
158
+ initiativeId,
159
+ workPackageId,
160
+ title: title || humanizeSlug(workPackageId.replace(/^WP-\d{3}-/, '')),
161
+ goal: goal || 'Define work-package outcome before materializing a task.',
162
+ mode,
163
+ }), { force, changes });
164
+ return {
165
+ initiativeId,
166
+ workPackageId,
167
+ workPackageDir,
168
+ changes,
169
+ };
170
+ }
171
+
172
+ export function initiativeStatus({ projectRoot = process.cwd(), initiativeId } = {}) {
173
+ const initiative = readInitiative({ projectRoot, initiativeId });
174
+ const workPackages = listWorkPackages(initiative.initiativeDir);
175
+ return {
176
+ ...initiative,
177
+ workPackages,
178
+ counts: countBy(workPackages, (wp) => wp.status || 'pending'),
179
+ };
180
+ }
181
+
182
+ export function initiativeNext({
183
+ projectRoot = process.cwd(),
184
+ initiativeId,
185
+ materializeTask = false,
186
+ taskId = null,
187
+ force = false,
188
+ } = {}) {
189
+ const status = initiativeStatus({ projectRoot, initiativeId });
190
+ const next = status.workPackages.find((wp) => ['pending', 'ready'].includes(wp.status));
191
+ if (!next) {
192
+ return {
193
+ initiativeId,
194
+ next: null,
195
+ materializedTask: null,
196
+ };
197
+ }
198
+ let materializedTask = null;
199
+ if (materializeTask) {
200
+ materializedTask = materializeWorkPackageTask({
201
+ projectRoot,
202
+ initiativeId,
203
+ workPackage: next,
204
+ taskId: taskId || taskIdForWorkPackage(next.id),
205
+ force,
206
+ });
207
+ updateWorkPackageStatus({
208
+ workPackagePath: next.path,
209
+ status: 'in_progress',
210
+ taskId: materializedTask.taskId,
211
+ });
212
+ }
213
+ return {
214
+ initiativeId,
215
+ next,
216
+ materializedTask,
217
+ };
218
+ }
219
+
220
+ export function initiativeIntake({
221
+ projectRoot = process.cwd(),
222
+ initiativeId,
223
+ docsDir,
224
+ force = false,
225
+ } = {}) {
226
+ const initiative = readInitiative({ projectRoot, initiativeId });
227
+ if (!docsDir) {
228
+ throw new Error('Usage: ops-agent initiative-intake <initiative> <docs-dir>');
229
+ }
230
+ const absoluteDocsDir = path.resolve(projectRoot, docsDir);
231
+ if (!fs.existsSync(absoluteDocsDir) || !fs.statSync(absoluteDocsDir).isDirectory()) {
232
+ throw new Error(`Docs directory not found: ${docsDir}`);
233
+ }
234
+ const intakeDir = path.join(initiative.initiativeDir, 'intake');
235
+ const changes = [];
236
+ ensureDirectory(intakeDir, changes);
237
+ const sources = collectSourceDocuments({ projectRoot, docsDir: absoluteDocsDir });
238
+ const candidates = collectRequirementCandidates({ projectRoot, sources });
239
+ writeFileIfAllowed(path.join(intakeDir, 'source-index.json'), JSON.stringify({
240
+ schemaVersion: 1,
241
+ generatedAt: new Date().toISOString(),
242
+ docsDir: path.relative(projectRoot, absoluteDocsDir),
243
+ sources,
244
+ }, null, 2), { force: true, changes });
245
+ writeFileIfAllowed(path.join(intakeDir, 'sources.md'), renderSourcesMarkdown({ docsDir: absoluteDocsDir, projectRoot, sources }), { force, changes });
246
+ writeFileIfAllowed(path.join(intakeDir, 'extracted-requirements.md'), renderRequirementCandidatesMarkdown(candidates), { force: true, changes });
247
+ writeFileIfAllowed(path.join(intakeDir, 'open-questions.md'), buildOpenQuestionsMarkdown(), { force, changes });
248
+ writeFileIfAllowed(path.join(intakeDir, 'assumptions.md'), buildAssumptionsMarkdown(), { force, changes });
249
+ return {
250
+ initiativeId,
251
+ intakeDir,
252
+ sources,
253
+ candidates,
254
+ changes,
255
+ };
256
+ }
257
+
258
+ export function initiativeRequirements({
259
+ projectRoot = process.cwd(),
260
+ initiativeId,
261
+ force = false,
262
+ } = {}) {
263
+ const initiative = readInitiative({ projectRoot, initiativeId });
264
+ const candidatesPath = path.join(initiative.initiativeDir, 'intake', 'extracted-requirements.md');
265
+ if (!fs.existsSync(candidatesPath)) {
266
+ throw new Error('Missing intake/extracted-requirements.md. Run initiative-intake first.');
267
+ }
268
+ const candidates = parseRequirementCandidates(fs.readFileSync(candidatesPath, 'utf8'));
269
+ const requirements = candidates.map((candidate, index) => ({
270
+ id: `REQ-${String(index + 1).padStart(3, '0')}`,
271
+ status: 'candidate',
272
+ title: summarizeRequirementTitle(candidate.text),
273
+ source: candidate.source,
274
+ sourceHash: candidate.sourceHash,
275
+ text: candidate.text,
276
+ workPackage: '',
277
+ acceptance: '',
278
+ }));
279
+ const changes = [];
280
+ writeFileIfAllowed(path.join(initiative.initiativeDir, 'requirements-map.md'), renderRequirementsMap(requirements), { force: true, changes });
281
+ writeFileIfAllowed(path.join(initiative.initiativeDir, 'coverage.md'), renderCoverage({ requirements }), { force, changes });
282
+ return {
283
+ initiativeId,
284
+ requirements,
285
+ changes,
286
+ };
287
+ }
288
+
289
+ function materializeWorkPackageTask({ projectRoot, initiativeId, workPackage, taskId, force }) {
290
+ const task = createTask({
291
+ projectRoot,
292
+ taskId,
293
+ title: workPackage.title,
294
+ owner: 'Supervisor',
295
+ force,
296
+ });
297
+ const briefPath = path.join(task.taskDir, 'brief.md');
298
+ const planPath = path.join(task.taskDir, 'plan.md');
299
+ const brief = fs.readFileSync(briefPath, 'utf8');
300
+ const plan = fs.readFileSync(planPath, 'utf8');
301
+ fs.writeFileSync(briefPath, updateMarkdownSection(brief, 'Initiative Context', [
302
+ `- Initiative: \`${initiativeId}\``,
303
+ `- Work package: \`${workPackage.id}\``,
304
+ `- Work package mode: \`${workPackage.mode}\``,
305
+ `- Goal: ${workPackage.goal}`,
306
+ '',
307
+ 'This task is a work-package task. Do not create separate tasks for planned slices unless scope/risk escalation requires it.',
308
+ ].join('\n')));
309
+ fs.writeFileSync(planPath, updateMarkdownSection(plan, 'Execution Slices', renderExecutionSlices(workPackage)));
310
+ return task;
311
+ }
312
+
313
+ function collectSourceDocuments({ projectRoot, docsDir }) {
314
+ const files = [];
315
+ walkDocs(docsDir, files);
316
+ return files
317
+ .sort()
318
+ .map((filePath) => {
319
+ const content = fs.readFileSync(filePath, 'utf8');
320
+ return {
321
+ path: path.relative(projectRoot, filePath),
322
+ bytes: Buffer.byteLength(content),
323
+ sha256: sha256(content),
324
+ title: inferSourceTitle(content, filePath),
325
+ };
326
+ });
327
+ }
328
+
329
+ function walkDocs(directory, files) {
330
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
331
+ if (entry.name.startsWith('.') || entry.name === 'node_modules') {
332
+ continue;
333
+ }
334
+ const entryPath = path.join(directory, entry.name);
335
+ if (entry.isDirectory()) {
336
+ walkDocs(entryPath, files);
337
+ continue;
338
+ }
339
+ if (entry.isFile() && SUPPORTED_SOURCE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) {
340
+ files.push(entryPath);
341
+ }
342
+ }
343
+ }
344
+
345
+ function collectRequirementCandidates({ projectRoot, sources }) {
346
+ const candidates = [];
347
+ const seen = new Set();
348
+ for (const source of sources) {
349
+ const sourcePath = path.join(projectRoot, source.path);
350
+ const lines = fs.readFileSync(sourcePath, 'utf8').split(/\r?\n/);
351
+ lines.forEach((line, index) => {
352
+ const normalized = normalizeRequirementCandidate(line);
353
+ if (!isRequirementCandidate(normalized)) {
354
+ return;
355
+ }
356
+ const key = sha256(`${source.path}:${normalized}`).slice(0, 16);
357
+ if (seen.has(key)) {
358
+ return;
359
+ }
360
+ seen.add(key);
361
+ candidates.push({
362
+ id: `RC-${String(candidates.length + 1).padStart(3, '0')}`,
363
+ source: `${source.path}:${index + 1}`,
364
+ sourceHash: source.sha256,
365
+ text: normalized,
366
+ });
367
+ });
368
+ }
369
+ return candidates;
370
+ }
371
+
372
+ function normalizeRequirementCandidate(line) {
373
+ return line
374
+ .replace(/^#{1,6}\s+/, '')
375
+ .replace(/^[-*]\s+/, '')
376
+ .replace(/^\d+[.)]\s+/, '')
377
+ .replace(/\s+/g, ' ')
378
+ .trim();
379
+ }
380
+
381
+ function isRequirementCandidate(text) {
382
+ if (text.length < 20 || text.length > 500) {
383
+ return false;
384
+ }
385
+ return /(must|should|required|requirement|user can|user must|system must|needs to|нужно|должен|должна|должны|требован|пользователь может|пользователь должен|система должна|необходимо|важно)/i.test(text);
386
+ }
387
+
388
+ function renderSourcesMarkdown({ docsDir, projectRoot, sources }) {
389
+ return [
390
+ '# Initiative Sources',
391
+ '',
392
+ `Docs directory: \`${path.relative(projectRoot, docsDir)}\``,
393
+ '',
394
+ '| Source | Title | SHA-256 | Bytes |',
395
+ '| --- | --- | --- | --- |',
396
+ ...sources.map((source) => `| \`${source.path}\` | ${escapeTable(source.title)} | \`${source.sha256}\` | ${source.bytes} |`),
397
+ '',
398
+ ].join('\n');
399
+ }
400
+
401
+ function renderRequirementCandidatesMarkdown(candidates) {
402
+ return [
403
+ '# Extracted Requirement Candidates',
404
+ '',
405
+ 'These are deterministic candidates from source docs. Human review is required before planning work packages.',
406
+ '',
407
+ ...candidates.map((candidate) => [
408
+ `## ${candidate.id}`,
409
+ '',
410
+ `- Source: \`${candidate.source}\``,
411
+ `- Source hash: \`${candidate.sourceHash}\``,
412
+ `- Candidate: ${candidate.text}`,
413
+ '',
414
+ ].join('\n')),
415
+ ].join('\n');
416
+ }
417
+
418
+ function parseRequirementCandidates(content) {
419
+ return content.split(/^##\s+/m).slice(1).map((section) => {
420
+ const [rawId, ...bodyLines] = section.split('\n');
421
+ const body = bodyLines.join('\n');
422
+ return {
423
+ id: rawId.trim(),
424
+ source: readMarkdownListField(body, 'Source'),
425
+ sourceHash: readMarkdownListField(body, 'Source hash'),
426
+ text: readMarkdownListField(body, 'Candidate'),
427
+ };
428
+ }).filter((candidate) => candidate.id && candidate.source && candidate.text);
429
+ }
430
+
431
+ function renderRequirementsMap(requirements) {
432
+ return [
433
+ '# Requirements Map',
434
+ '',
435
+ 'Statuses: `candidate | approved | planned | implemented | rejected`.',
436
+ '',
437
+ '| ID | Status | Requirement | Source | Work package | Acceptance |',
438
+ '| --- | --- | --- | --- | --- | --- |',
439
+ ...requirements.map((req) => `| ${req.id} | ${req.status} | ${escapeTable(req.title)} | \`${req.source}\` | ${req.workPackage || ''} | ${req.acceptance || ''} |`),
440
+ '',
441
+ '## Requirement Details',
442
+ '',
443
+ ...requirements.map((req) => [
444
+ `### ${req.id}`,
445
+ '',
446
+ `- Status: \`${req.status}\``,
447
+ `- Source: \`${req.source}\``,
448
+ `- Source hash: \`${req.sourceHash}\``,
449
+ `- Work package: ${req.workPackage || '(unassigned)'}`,
450
+ `- Acceptance: ${req.acceptance || '(fill before implementation)'}`,
451
+ `- Requirement: ${req.text}`,
452
+ '',
453
+ ].join('\n')),
454
+ ].join('\n');
455
+ }
456
+
457
+ function renderCoverage({ requirements }) {
458
+ const counts = countBy(requirements, (req) => req.status || 'candidate');
459
+ return [
460
+ '# Initiative Coverage',
461
+ '',
462
+ `- Total requirements: ${requirements.length}`,
463
+ `- Candidate: ${counts.candidate || 0}`,
464
+ `- Approved: ${counts.approved || 0}`,
465
+ `- Planned: ${counts.planned || 0}`,
466
+ `- Implemented: ${counts.implemented || 0}`,
467
+ `- Rejected: ${counts.rejected || 0}`,
468
+ '',
469
+ '## Unassigned Requirements',
470
+ '',
471
+ ...requirements.filter((req) => !req.workPackage).map((req) => `- ${req.id}: ${req.title}`),
472
+ ...(requirements.some((req) => !req.workPackage) ? [] : ['- None.']),
473
+ '',
474
+ ].join('\n');
475
+ }
476
+
477
+ function buildOpenQuestionsMarkdown() {
478
+ return [
479
+ '# Open Questions',
480
+ '',
481
+ '- Add human decisions discovered during initiative intake.',
482
+ '',
483
+ ].join('\n');
484
+ }
485
+
486
+ function buildAssumptionsMarkdown() {
487
+ return [
488
+ '# Assumptions',
489
+ '',
490
+ '- Add assumptions that were required to interpret source docs.',
491
+ '',
492
+ ].join('\n');
493
+ }
494
+
495
+ function readInitiative({ projectRoot, initiativeId }) {
496
+ assertInitiativeId(initiativeId);
497
+ const context = resolveProjectContext({ cwd: projectRoot });
498
+ const initiativeDir = path.join(context.initiativesRoot, initiativeId);
499
+ if (!fs.existsSync(initiativeDir)) {
500
+ throw new Error(`Initiative not found: ${initiativeId}`);
501
+ }
502
+ const meta = readSimpleYaml(path.join(initiativeDir, 'initiative.yaml'));
503
+ return {
504
+ initiativeId,
505
+ initiativeDir,
506
+ title: meta.title || humanizeSlug(initiativeId),
507
+ mode: meta.mode || 'fast_mvp',
508
+ goal: meta.goal || '',
509
+ };
510
+ }
511
+
512
+ function listWorkPackages(initiativeDir) {
513
+ const root = path.join(initiativeDir, 'work-packages');
514
+ if (!fs.existsSync(root)) {
515
+ return [];
516
+ }
517
+ return fs.readdirSync(root, { withFileTypes: true })
518
+ .filter((entry) => entry.isDirectory() && WORK_PACKAGE_ID_PATTERN.test(entry.name))
519
+ .map((entry) => readWorkPackage(path.join(root, entry.name, 'work-package.md'), entry.name))
520
+ .sort((left, right) => left.id.localeCompare(right.id));
521
+ }
522
+
523
+ function readWorkPackage(filePath, fallbackId) {
524
+ const content = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
525
+ return {
526
+ id: readInlineField(content, 'ID') || fallbackId,
527
+ title: readInlineField(content, 'Title') || humanizeSlug(fallbackId.replace(/^WP-\d{3}-/, '')),
528
+ status: readInlineField(content, 'Status') || 'pending',
529
+ task: readInlineField(content, 'Task') || '',
530
+ mode: readInlineField(content, 'Mode') || 'standard_work_package',
531
+ goal: readSection(content, 'Goal') || '',
532
+ slices: readBulletsFromSection(content, 'Slices'),
533
+ path: filePath,
534
+ };
535
+ }
536
+
537
+ function updateWorkPackageStatus({ workPackagePath, status, taskId }) {
538
+ const content = fs.readFileSync(workPackagePath, 'utf8');
539
+ let updated = content
540
+ .replace(/^Status:.*$/m, `Status: ${status}`)
541
+ .replace(/^Task:.*$/m, `Task: ${taskId}`);
542
+ if (updated === content) {
543
+ updated = `${content.trimEnd()}\n\nStatus: ${status}\nTask: ${taskId}\n`;
544
+ }
545
+ fs.writeFileSync(workPackagePath, updated.endsWith('\n') ? updated : `${updated}\n`);
546
+ }
547
+
548
+ function buildInitiativeYaml({ initiativeId, title, goal, mode }) {
549
+ return [
550
+ 'schemaVersion: 1',
551
+ `id: ${initiativeId}`,
552
+ `title: ${title}`,
553
+ `mode: ${mode}`,
554
+ 'humanAttention: milestone_and_escalations',
555
+ 'riskPolicy: strict_on_high_risk_only',
556
+ `goal: ${goal}`,
557
+ '',
558
+ ].join('\n');
559
+ }
560
+
561
+ function buildInitiativeMarkdown({ initiativeId, title, goal, mode }) {
562
+ return [
563
+ `# ${title}`,
564
+ '',
565
+ `ID: ${initiativeId}`,
566
+ `Mode: ${mode}`,
567
+ 'Human attention: milestone_and_escalations',
568
+ 'Risk policy: strict_on_high_risk_only',
569
+ '',
570
+ '## Goal',
571
+ '',
572
+ goal,
573
+ '',
574
+ '## Phases',
575
+ '',
576
+ '- Phase 1: Foundation',
577
+ '- Phase 2: Core workflows',
578
+ '- Phase 3: UI/API integration',
579
+ '- Phase 4: QA, rollout and closeout',
580
+ '',
581
+ '## Work Package Policy',
582
+ '',
583
+ '- Work package is materialized as a normal task and remains the audit/logging unit.',
584
+ '- Slices live inside the work-package task plan/execution, not as separate task folders.',
585
+ '- Create a separate task only when a slice triggers scope, risk or architecture escalation.',
586
+ '',
587
+ ].join('\n');
588
+ }
589
+
590
+ function buildInitiativeStatus({ initiativeId, title }) {
591
+ return [
592
+ '# Initiative Status',
593
+ '',
594
+ `Initiative: ${initiativeId}`,
595
+ `Title: ${title}`,
596
+ 'Current phase: planning',
597
+ 'Current work package: none',
598
+ 'Human approval needed: yes',
599
+ 'Next step: Add work packages, then run initiative-next.',
600
+ '',
601
+ ].join('\n');
602
+ }
603
+
604
+ function buildWorkPackagesReadme() {
605
+ return [
606
+ '# Work Packages',
607
+ '',
608
+ 'Each `WP-000-slug` is a planned work-package scope.',
609
+ '',
610
+ 'Use `ops-agent initiative-next <initiative> --materialize-task` to turn the next pending work package into a normal task.',
611
+ '',
612
+ ].join('\n');
613
+ }
614
+
615
+ function buildWorkPackageMarkdown({ initiativeId, workPackageId, title, goal, mode }) {
616
+ return [
617
+ '# Work Package',
618
+ '',
619
+ `ID: ${workPackageId}`,
620
+ `Initiative: ${initiativeId}`,
621
+ `Title: ${title}`,
622
+ 'Status: pending',
623
+ 'Task:',
624
+ `Mode: ${mode}`,
625
+ '',
626
+ '## Goal',
627
+ '',
628
+ goal,
629
+ '',
630
+ '## Boundary',
631
+ '',
632
+ '- Included:',
633
+ '- Excluded:',
634
+ '- Escalate to separate task if:',
635
+ '',
636
+ '## Slices',
637
+ '',
638
+ '- Slice 1: Define the first implementation slice.',
639
+ '- Slice 2: Add the next implementation slice.',
640
+ '- Slice 3: Add micro-verify and evidence.',
641
+ '',
642
+ '## Acceptance',
643
+ '',
644
+ '- Work-package task has completed Verify.',
645
+ '- Slice ledger is recorded in execution.md.',
646
+ '- Learning closeout is completed before task closeout.',
647
+ '',
648
+ ].join('\n');
649
+ }
650
+
651
+ function renderExecutionSlices(workPackage) {
652
+ const slices = workPackage.slices.length
653
+ ? workPackage.slices
654
+ : ['Slice 1: Define implementation slice before Execute.'];
655
+ return [
656
+ `Work package: \`${workPackage.id}\``,
657
+ `Mode: \`${workPackage.mode}\``,
658
+ '',
659
+ '| Slice | Intent | Micro-verify | Escalation rule |',
660
+ '| --- | --- | --- | --- |',
661
+ ...slices.map((slice, index) => `| S-${String(index + 1).padStart(2, '0')} | ${escapeTable(slice)} | targeted test/typecheck/evidence | create separate task or return to Plan if scope/risk expands |`),
662
+ '',
663
+ ].join('\n');
664
+ }
665
+
666
+ function readSimpleYaml(filePath) {
667
+ if (!fs.existsSync(filePath)) {
668
+ return {};
669
+ }
670
+ const result = {};
671
+ for (const line of fs.readFileSync(filePath, 'utf8').split(/\r?\n/)) {
672
+ const match = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line);
673
+ if (match) {
674
+ result[match[1]] = match[2].trim();
675
+ }
676
+ }
677
+ return result;
678
+ }
679
+
680
+ function readInlineField(content, field) {
681
+ const match = new RegExp(`^${escapeRegExp(field)}:\\s*(.*)$`, 'm').exec(content);
682
+ return match ? match[1].trim() : '';
683
+ }
684
+
685
+ function readMarkdownListField(content, field) {
686
+ const match = new RegExp(`^- ${escapeRegExp(field)}:\\s*(.*)$`, 'm').exec(content);
687
+ return match ? match[1].replace(/^`|`$/g, '').trim() : '';
688
+ }
689
+
690
+ function readSection(content, heading) {
691
+ const match = new RegExp(`^##\\s+${escapeRegExp(heading)}\\s*\\n([\\s\\S]*?)(?=^##\\s+|\\s*$)`, 'm').exec(content);
692
+ return match ? match[1].trim() : '';
693
+ }
694
+
695
+ function readBulletsFromSection(content, heading) {
696
+ return readSection(content, heading)
697
+ .split(/\r?\n/)
698
+ .map((line) => line.replace(/^[-*]\s+/, '').trim())
699
+ .filter(Boolean);
700
+ }
701
+
702
+ function taskIdForWorkPackage(workPackageId) {
703
+ return workPackageId.replace(/^WP-/, 'TASK-');
704
+ }
705
+
706
+ function countBy(items, readKey) {
707
+ const counts = {};
708
+ for (const item of items) {
709
+ const key = readKey(item);
710
+ counts[key] = (counts[key] || 0) + 1;
711
+ }
712
+ return counts;
713
+ }
714
+
715
+ function ensureDirectory(directory, changes) {
716
+ if (fs.existsSync(directory)) {
717
+ changes.push({ path: directory, kind: 'directory', status: 'existing' });
718
+ return;
719
+ }
720
+ fs.mkdirSync(directory, { recursive: true });
721
+ changes.push({ path: directory, kind: 'directory', status: 'created' });
722
+ }
723
+
724
+ function writeFileIfAllowed(filePath, content, { force, changes }) {
725
+ const exists = fs.existsSync(filePath);
726
+ if (exists && !force) {
727
+ changes.push({ path: filePath, kind: 'file', status: 'existing' });
728
+ return;
729
+ }
730
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
731
+ fs.writeFileSync(filePath, content.endsWith('\n') ? content : `${content}\n`);
732
+ changes.push({ path: filePath, kind: 'file', status: exists ? 'overwritten' : 'created' });
733
+ }
734
+
735
+ function printChangeSummary(title, changes) {
736
+ const summary = summarizeChanges(changes);
737
+ console.log(title);
738
+ console.log(`- created: ${summary.created}`);
739
+ console.log(`- existing: ${summary.existing}`);
740
+ console.log(`- overwritten: ${summary.overwritten}`);
741
+ }
742
+
743
+ function printInitiativeStatus(result) {
744
+ console.log(`Initiative status: ${result.initiativeId}`);
745
+ console.log(`- title: ${result.title}`);
746
+ console.log(`- mode: ${result.mode}`);
747
+ console.log(`- workPackages: ${result.workPackages.length}`);
748
+ for (const [status, count] of Object.entries(result.counts)) {
749
+ console.log(`- ${status}: ${count}`);
750
+ }
751
+ for (const wp of result.workPackages) {
752
+ console.log(` - ${wp.id}: ${wp.status}${wp.task ? ` -> ${wp.task}` : ''}`);
753
+ }
754
+ }
755
+
756
+ function printInitiativeNext(result) {
757
+ console.log(`Initiative next: ${result.initiativeId}`);
758
+ if (!result.next) {
759
+ console.log('- no pending work packages');
760
+ return;
761
+ }
762
+ console.log(`- workPackage: ${result.next.id}`);
763
+ console.log(`- title: ${result.next.title}`);
764
+ console.log(`- mode: ${result.next.mode}`);
765
+ if (result.materializedTask) {
766
+ const summary = summarizeChanges(result.materializedTask.changes);
767
+ console.log(`- task: ${result.materializedTask.taskId}`);
768
+ console.log(`- taskDir: ${result.materializedTask.taskDir}`);
769
+ console.log(`- created: ${summary.created}`);
770
+ console.log(`- existing: ${summary.existing}`);
771
+ }
772
+ }
773
+
774
+ function assertInitiativeId(value) {
775
+ if (!INITIATIVE_ID_PATTERN.test(value || '')) {
776
+ throw new Error(`Initiative id must match ${INITIATIVE_ID_PATTERN}: ${value || '<missing>'}`);
777
+ }
778
+ }
779
+
780
+ function assertWorkPackageId(value) {
781
+ if (!WORK_PACKAGE_ID_PATTERN.test(value || '')) {
782
+ throw new Error(`Work package id must match ${WORK_PACKAGE_ID_PATTERN}: ${value || '<missing>'}`);
783
+ }
784
+ }
785
+
786
+ function humanizeSlug(slug) {
787
+ return slug
788
+ .replace(/^(TASK|WP)-\d{3}-/, '')
789
+ .split('-')
790
+ .filter(Boolean)
791
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
792
+ .join(' ');
793
+ }
794
+
795
+ function escapeTable(value) {
796
+ return String(value).replace(/\|/g, '\\|');
797
+ }
798
+
799
+ function inferSourceTitle(content, filePath) {
800
+ const heading = /^#\s+(.+)$/m.exec(content);
801
+ return heading ? heading[1].trim() : path.basename(filePath);
802
+ }
803
+
804
+ function summarizeRequirementTitle(text) {
805
+ return text.length > 120 ? `${text.slice(0, 117)}...` : text;
806
+ }
807
+
808
+ function sha256(value) {
809
+ return crypto.createHash('sha256').update(value).digest('hex');
810
+ }
811
+
812
+ function escapeRegExp(value) {
813
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
814
+ }
815
+
816
+ function fail(message) {
817
+ console.error(`Error: ${message}`);
818
+ process.exit(1);
819
+ }
820
+
821
+ if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
822
+ main();
823
+ }
@@ -0,0 +1,133 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { describe, expect, it } from 'vitest';
5
+ import {
6
+ addWorkPackage,
7
+ createInitiative,
8
+ initiativeIntake,
9
+ initiativeNext,
10
+ initiativeRequirements,
11
+ initiativeStatus,
12
+ } from './initiative.mjs';
13
+
14
+ describe('initiative framework', () => {
15
+ it('creates an initiative and summarizes work-package status', () => {
16
+ const root = makeProject();
17
+
18
+ createInitiative({
19
+ projectRoot: root,
20
+ initiativeId: 'delivery-os-mvp',
21
+ title: 'Delivery OS MVP',
22
+ goal: 'Build the first MVP train.',
23
+ });
24
+ addWorkPackage({
25
+ projectRoot: root,
26
+ initiativeId: 'delivery-os-mvp',
27
+ workPackageId: 'WP-001-foundation',
28
+ title: 'Foundation',
29
+ goal: 'Create the foundation.',
30
+ });
31
+
32
+ const status = initiativeStatus({ projectRoot: root, initiativeId: 'delivery-os-mvp' });
33
+
34
+ expect(status.mode).toBe('fast_mvp');
35
+ expect(status.workPackages).toHaveLength(1);
36
+ expect(status.workPackages[0]).toMatchObject({
37
+ id: 'WP-001-foundation',
38
+ status: 'pending',
39
+ title: 'Foundation',
40
+ });
41
+ expect(status.counts.pending).toBe(1);
42
+ });
43
+
44
+ it('materializes the next work package as a task with execution slices', () => {
45
+ const root = makeProject();
46
+ createInitiative({
47
+ projectRoot: root,
48
+ initiativeId: 'delivery-os-mvp',
49
+ title: 'Delivery OS MVP',
50
+ });
51
+ addWorkPackage({
52
+ projectRoot: root,
53
+ initiativeId: 'delivery-os-mvp',
54
+ workPackageId: 'WP-001-foundation',
55
+ title: 'Foundation',
56
+ goal: 'Create the foundation.',
57
+ });
58
+
59
+ const result = initiativeNext({
60
+ projectRoot: root,
61
+ initiativeId: 'delivery-os-mvp',
62
+ materializeTask: true,
63
+ });
64
+
65
+ expect(result.materializedTask.taskId).toBe('TASK-001-foundation');
66
+ const taskDir = path.join(root, 'ops', 'agent-pipeline', 'tasks', 'TASK-001-foundation');
67
+ const brief = fs.readFileSync(path.join(taskDir, 'brief.md'), 'utf8');
68
+ const plan = fs.readFileSync(path.join(taskDir, 'plan.md'), 'utf8');
69
+ const workPackage = fs.readFileSync(path.join(root, 'ops', 'agent-pipeline', 'initiatives', 'delivery-os-mvp', 'work-packages', 'WP-001-foundation', 'work-package.md'), 'utf8');
70
+ expect(brief).toContain('## Initiative Context');
71
+ expect(brief).toContain('Work package: `WP-001-foundation`');
72
+ expect(plan).toContain('## Execution Slices');
73
+ expect(plan).toContain('S-01');
74
+ expect(workPackage).toContain('Status: in_progress');
75
+ expect(workPackage).toContain('Task: TASK-001-foundation');
76
+ });
77
+
78
+ it('indexes source docs and builds a requirements map', () => {
79
+ const root = makeProject();
80
+ const docsDir = path.join(root, 'docs', 'delivery-os');
81
+ fs.mkdirSync(docsDir, { recursive: true });
82
+ fs.writeFileSync(path.join(docsDir, 'mvp.md'), [
83
+ '# Delivery OS MVP',
84
+ '',
85
+ '- User must create delivery orders from the admin UI.',
86
+ '- Система должна показывать статус доставки оператору.',
87
+ '- Nice prose for background context only.',
88
+ ].join('\n'));
89
+ createInitiative({
90
+ projectRoot: root,
91
+ initiativeId: 'delivery-os-mvp',
92
+ title: 'Delivery OS MVP',
93
+ });
94
+
95
+ const intake = initiativeIntake({
96
+ projectRoot: root,
97
+ initiativeId: 'delivery-os-mvp',
98
+ docsDir: 'docs/delivery-os',
99
+ });
100
+ const requirements = initiativeRequirements({
101
+ projectRoot: root,
102
+ initiativeId: 'delivery-os-mvp',
103
+ });
104
+
105
+ expect(intake.sources).toHaveLength(1);
106
+ expect(intake.candidates).toHaveLength(2);
107
+ expect(requirements.requirements).toHaveLength(2);
108
+ const initiativeDir = path.join(root, 'ops', 'agent-pipeline', 'initiatives', 'delivery-os-mvp');
109
+ const sourceIndex = JSON.parse(fs.readFileSync(path.join(initiativeDir, 'intake', 'source-index.json'), 'utf8'));
110
+ const requirementsMap = fs.readFileSync(path.join(initiativeDir, 'requirements-map.md'), 'utf8');
111
+ expect(sourceIndex.sources[0].path).toBe('docs/delivery-os/mvp.md');
112
+ expect(requirementsMap).toContain('REQ-001');
113
+ expect(requirementsMap).toContain('User must create delivery orders');
114
+ expect(requirementsMap).toContain('REQ-002');
115
+ });
116
+ });
117
+
118
+ function makeProject() {
119
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ops-initiative-'));
120
+ fs.mkdirSync(path.join(root, 'ops'), { recursive: true });
121
+ fs.writeFileSync(path.join(root, 'ops', 'project.ops.yaml'), [
122
+ 'name: TestProject',
123
+ 'ops:',
124
+ ' legacyPipelineDir: ops/agent-pipeline',
125
+ ' tasksDir: ops/agent-pipeline/tasks',
126
+ ' initiativesDir: ops/agent-pipeline/initiatives',
127
+ ' memoryDir: ops/agent-pipeline/memory',
128
+ ' cacheDir: ops/agent-pipeline/cache',
129
+ ' playbooksDir: ops/agent-pipeline/playbooks',
130
+ '',
131
+ ].join('\n'));
132
+ return root;
133
+ }
@@ -121,6 +121,12 @@ export function buildOpsScripts(packageSpec) {
121
121
  'agent:learning-report': run('learning-report'),
122
122
  'agent:learning-closeout': run('learning-closeout'),
123
123
  'agent:closeout': run('closeout'),
124
+ 'agent:initiative-create': run('initiative-create'),
125
+ 'agent:initiative-add-work-package': run('initiative-add-work-package'),
126
+ 'agent:initiative-status': run('initiative-status'),
127
+ 'agent:initiative-next': run('initiative-next'),
128
+ 'agent:initiative-intake': run('initiative-intake'),
129
+ 'agent:initiative-requirements': run('initiative-requirements'),
124
130
  'agent:test': run('test/self-test'),
125
131
  };
126
132
  }
@@ -290,6 +296,7 @@ function buildProjectConfig({ projectName, opsRoot }) {
290
296
  'ops:',
291
297
  ` legacyPipelineDir: ${opsRoot}/agent-pipeline`,
292
298
  ` tasksDir: ${opsRoot}/agent-pipeline/tasks`,
299
+ ` initiativesDir: ${opsRoot}/agent-pipeline/initiatives`,
293
300
  ` memoryDir: ${opsRoot}/agent-pipeline/memory`,
294
301
  ` cacheDir: ${opsRoot}/agent-pipeline/cache`,
295
302
  ` playbooksDir: ${opsRoot}/agent-pipeline/playbooks`,
@@ -140,6 +140,10 @@ describe('buildOpsScripts', () => {
140
140
  expect(scripts['agent:quality-gates']).toBe('ops-agent quality-gates');
141
141
  expect(scripts['agent:learning-closeout']).toBe('ops-agent learning-closeout');
142
142
  expect(scripts['agent:closeout']).toBe('ops-agent closeout');
143
+ expect(scripts['agent:initiative-create']).toBe('ops-agent initiative-create');
144
+ expect(scripts['agent:initiative-next']).toBe('ops-agent initiative-next');
145
+ expect(scripts['agent:initiative-intake']).toBe('ops-agent initiative-intake');
146
+ expect(scripts['agent:initiative-requirements']).toBe('ops-agent initiative-requirements');
143
147
  expect(scripts['agent:test']).toBe('ops-agent test/self-test');
144
148
  });
145
149
  });
@@ -134,6 +134,7 @@ function buildConfiguredContext({ projectRoot, configPath, config }) {
134
134
  frameworkRoot,
135
135
  pipelineRoot: legacyPipelineRoot,
136
136
  tasksRoot: resolveProjectPath(projectRoot, ops.tasksDir || 'ops/agent-pipeline/tasks'),
137
+ initiativesRoot: resolveProjectPath(projectRoot, ops.initiativesDir || 'ops/agent-pipeline/initiatives'),
137
138
  memoryRoot: resolveProjectPath(projectRoot, ops.memoryDir || 'ops/agent-pipeline/memory'),
138
139
  cacheRoot: resolveProjectPath(projectRoot, ops.cacheDir || 'ops/agent-pipeline/cache'),
139
140
  promptsRoot: path.join(frameworkRoot, 'prompts'),
@@ -163,6 +164,7 @@ function buildLegacyContext({ cwd }) {
163
164
  frameworkRoot,
164
165
  pipelineRoot: legacyPipelineRoot,
165
166
  tasksRoot: path.join(legacyPipelineRoot, 'tasks'),
167
+ initiativesRoot: path.join(legacyPipelineRoot, 'initiatives'),
166
168
  memoryRoot: path.join(legacyPipelineRoot, 'memory'),
167
169
  cacheRoot: path.join(legacyPipelineRoot, 'cache'),
168
170
  promptsRoot: path.join(frameworkRoot, 'prompts'),
package/bin/ops-agent.mjs CHANGED
@@ -34,6 +34,12 @@ const COMMANDS = new Map([
34
34
  ['learning-report', 'learning-loop.mjs'],
35
35
  ['learning-closeout', 'learning-loop.mjs'],
36
36
  ['closeout', 'closeout.mjs'],
37
+ ['initiative-create', 'initiative.mjs'],
38
+ ['initiative-add-work-package', 'initiative.mjs'],
39
+ ['initiative-status', 'initiative.mjs'],
40
+ ['initiative-next', 'initiative.mjs'],
41
+ ['initiative-intake', 'initiative.mjs'],
42
+ ['initiative-requirements', 'initiative.mjs'],
37
43
  ['test/self-test', null],
38
44
  ]);
39
45
 
@@ -58,7 +64,7 @@ function main() {
58
64
  }
59
65
 
60
66
  const script = path.join(binRoot, COMMANDS.get(command));
61
- const scriptArgs = COMMANDS.get(command) === 'learning-loop.mjs' ? [command, ...args] : args;
67
+ const scriptArgs = ['learning-loop.mjs', 'initiative.mjs'].includes(COMMANDS.get(command)) ? [command, ...args] : args;
62
68
  const result = spawnSync(process.execPath, [script, ...scriptArgs], {
63
69
  cwd: process.cwd(),
64
70
  env: process.env,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@besales/ops-framework",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "ops-agent": "bin/ops-agent.mjs"