@besales/ops-framework 0.1.16 → 0.1.18

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,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.18
4
+
5
+ - Added work-package dependency metadata (`Depends on`, `Unblocks`, `Can run parallel with`, `Sequencing notes`) to new initiative work packages.
6
+ - Made `initiative-next` dependency-aware and prevented materialization when pending work packages are blocked by unmet dependencies.
7
+ - Added a work-package readiness gate that blocks materialization when a work package covers `REQ-*` items but has only process-level acceptance.
8
+
9
+ ## 0.1.17
10
+
11
+ - Broadened the schema compatibility gate so it blocks any schema/foundation requirement that omits fields required by approved dependent requirements, even when the schema text is not written as an explicit closed list.
12
+ - Expanded the schema gate action to call out `person_aliases` or alias storage plus required `meetings.*` fields for track classification and speaker resolution.
13
+
3
14
  ## 0.1.16
4
15
 
5
16
  - Added a schema compatibility readiness gate that blocks planning when a closed/minimal schema requirement does not support dependent approved requirements such as speaker resolution, aliases and meeting track classification.
package/README.md CHANGED
@@ -249,6 +249,8 @@ Initiative
249
249
 
250
250
  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.
251
251
 
252
+ Work packages can declare `Depends on`, `Unblocks`, `Can run parallel with` and `Sequencing notes`. `initiative-next` uses `Depends on` to avoid materializing a package before required packages are completed. If a work package lists `REQ-*` coverage, its `Acceptance` must include requirement-level done criteria or explicit `REQ-*` acceptance references; process-only acceptance blocks materialization.
253
+
252
254
  For source docs, use intake before work-package planning:
253
255
 
254
256
  ```text
@@ -202,12 +202,14 @@ export function initiativeNext({
202
202
  force = false,
203
203
  } = {}) {
204
204
  const status = initiativeStatus({ projectRoot, initiativeId });
205
- const next = status.workPackages.find((wp) => ['pending', 'ready'].includes(wp.status));
205
+ const readiness = buildWorkPackageReadiness(status.workPackages);
206
+ const next = readiness.find((item) => item.ready)?.workPackage || null;
206
207
  if (!next) {
207
208
  return {
208
209
  initiativeId,
209
210
  next: null,
210
211
  materializedTask: null,
212
+ readiness,
211
213
  };
212
214
  }
213
215
  let materializedTask = null;
@@ -229,6 +231,7 @@ export function initiativeNext({
229
231
  initiativeId,
230
232
  next,
231
233
  materializedTask,
234
+ readiness,
232
235
  };
233
236
  }
234
237
 
@@ -1126,14 +1129,15 @@ function addSchemaCompatibilityGate({ gates, sourceText, requirements }) {
1126
1129
  if (!schemaSupportsMeetingOperationalFields) {
1127
1130
  missing.push('meeting operational fields mentioned by sources, such as `series_id` and `asr_quality`');
1128
1131
  }
1129
- if (missing.length && schemaLooksClosed) {
1132
+ if (missing.length) {
1133
+ const schemaShape = schemaLooksClosed ? 'closed/minimal schema list' : 'schema/foundation contract';
1130
1134
  gates.push({
1131
1135
  id: 'schema-supports-derived-requirements',
1132
1136
  severity: 'critical',
1133
1137
  blocking: true,
1134
1138
  title: 'Schema requirement does not support dependent requirements',
1135
- issue: `Schema requirement ${schemaReq.id} is written as a closed/minimal schema list but omits ${missing.join(', ')}.`,
1136
- action: 'Update the schema requirement before WP planning: add the missing schema support, or explicitly state the alternative Phase 1 storage contract and defer the dependent behavior.',
1139
+ issue: `Schema requirement ${schemaReq.id} is written as a ${schemaShape} but omits ${missing.join(', ')}.`,
1140
+ action: 'Update the schema requirement before WP planning: add the missing schema support, or explicitly state the alternative Phase 1 storage contract and defer the dependent behavior. For meeting extraction this usually means `person_aliases` or documented alias storage plus `meetings.meeting_track`, `meetings.series_id`, `meetings.asr_quality`, `meetings.speaker_resolution_status` and `meetings.raw_speaker_labels` when those behaviors are approved.',
1137
1141
  });
1138
1142
  }
1139
1143
  }
@@ -1326,6 +1330,7 @@ function listWorkPackages(initiativeDir) {
1326
1330
 
1327
1331
  function readWorkPackage(filePath, fallbackId) {
1328
1332
  const content = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
1333
+ const dependencies = readWorkPackageDependencies(content);
1329
1334
  return {
1330
1335
  id: readInlineField(content, 'ID') || fallbackId,
1331
1336
  title: readInlineField(content, 'Title') || humanizeSlug(fallbackId.replace(/^WP-\d{3}-/, '')),
@@ -1333,11 +1338,89 @@ function readWorkPackage(filePath, fallbackId) {
1333
1338
  task: readInlineField(content, 'Task') || '',
1334
1339
  mode: readInlineField(content, 'Mode') || 'standard_work_package',
1335
1340
  goal: readSection(content, 'Goal') || '',
1341
+ requirementsCoverage: readBulletsFromSection(content, 'Requirements Coverage'),
1336
1342
  slices: readBulletsFromSection(content, 'Slices'),
1343
+ acceptance: readBulletsFromSection(content, 'Acceptance'),
1344
+ dependencies,
1337
1345
  path: filePath,
1338
1346
  };
1339
1347
  }
1340
1348
 
1349
+ function readWorkPackageDependencies(content) {
1350
+ const section = readSection(content, 'Dependencies');
1351
+ return {
1352
+ dependsOn: readDependencyField(section, 'Depends on'),
1353
+ unblocks: readDependencyField(section, 'Unblocks'),
1354
+ parallelWith: readDependencyField(section, 'Can run parallel with'),
1355
+ sequencingNotes: readDependencyField(section, 'Sequencing notes'),
1356
+ };
1357
+ }
1358
+
1359
+ function readDependencyField(section, field) {
1360
+ if (!section) {
1361
+ return [];
1362
+ }
1363
+ const match = new RegExp(`^-\\s+${escapeRegExp(field)}:\\s*(.*)$`, 'im').exec(section);
1364
+ if (!match) {
1365
+ return [];
1366
+ }
1367
+ return splitDependencyRefs(match[1]);
1368
+ }
1369
+
1370
+ function splitDependencyRefs(value) {
1371
+ return String(value || '')
1372
+ .split(/[,;]/)
1373
+ .map((item) => item.trim())
1374
+ .filter((item) => item && !/^\(?none\)?$/i.test(item) && !/^\[?fill in\]?$/i.test(item));
1375
+ }
1376
+
1377
+ function buildWorkPackageReadiness(workPackages) {
1378
+ const byId = new Map(workPackages.map((wp) => [wp.id, wp]));
1379
+ return workPackages
1380
+ .filter((wp) => ['pending', 'ready'].includes(wp.status))
1381
+ .map((wp) => {
1382
+ const blockers = [];
1383
+ for (const dependencyId of wp.dependencies.dependsOn) {
1384
+ const dependency = byId.get(dependencyId);
1385
+ if (!dependency) {
1386
+ blockers.push(`Dependency ${dependencyId} is missing.`);
1387
+ continue;
1388
+ }
1389
+ if (!workPackageDependencySatisfied(dependency)) {
1390
+ blockers.push(`Dependency ${dependencyId} is ${dependency.status || 'unknown'}, not completed.`);
1391
+ }
1392
+ }
1393
+ if (workPackageAcceptanceIsProcessOnly(wp)) {
1394
+ blockers.push('Acceptance is process-only while Requirements Coverage is present; add requirement-level done criteria before materialize.');
1395
+ }
1396
+ return {
1397
+ workPackage: wp,
1398
+ ready: blockers.length === 0,
1399
+ blockers,
1400
+ };
1401
+ });
1402
+ }
1403
+
1404
+ function workPackageDependencySatisfied(workPackage) {
1405
+ return /^(done|complete|completed|verified|closed)$/i.test(workPackage.status || '');
1406
+ }
1407
+
1408
+ function workPackageAcceptanceIsProcessOnly(workPackage) {
1409
+ const coverageText = workPackage.requirementsCoverage.join('\n');
1410
+ if (!/\bREQ-\d{3}\b/.test(coverageText)) {
1411
+ return false;
1412
+ }
1413
+ const acceptance = workPackage.acceptance;
1414
+ if (!acceptance.length) {
1415
+ return true;
1416
+ }
1417
+ const acceptanceText = acceptance.join('\n');
1418
+ if (/\bREQ-\d{3}\b/.test(acceptanceText)) {
1419
+ return false;
1420
+ }
1421
+ return acceptance.every((item) => /(verify completed|completed verify|slice ledger|learning closeout|task closeout|execution\.md)/i.test(item));
1422
+ }
1423
+
1341
1424
  function updateWorkPackageStatus({ workPackagePath, status, taskId }) {
1342
1425
  const content = fs.readFileSync(workPackagePath, 'utf8');
1343
1426
  let updated = content
@@ -1437,6 +1520,17 @@ function buildWorkPackageMarkdown({ initiativeId, workPackageId, title, goal, mo
1437
1520
  '- Excluded:',
1438
1521
  '- Escalate to separate task if:',
1439
1522
  '',
1523
+ '## Requirements Coverage',
1524
+ '',
1525
+ '- REQ-000: Add covered requirement and keep acceptance aligned with requirement-level done criteria.',
1526
+ '',
1527
+ '## Dependencies',
1528
+ '',
1529
+ '- Depends on: (none)',
1530
+ '- Unblocks: (none)',
1531
+ '- Can run parallel with: (none)',
1532
+ '- Sequencing notes: (none)',
1533
+ '',
1440
1534
  '## Slices',
1441
1535
  '',
1442
1536
  '- Slice 1: Define the first implementation slice.',
@@ -1445,6 +1539,7 @@ function buildWorkPackageMarkdown({ initiativeId, workPackageId, title, goal, mo
1445
1539
  '',
1446
1540
  '## Acceptance',
1447
1541
  '',
1542
+ '- Covered REQ-* acceptance criteria are satisfied or explicitly deferred with human approval.',
1448
1543
  '- Work-package task has completed Verify.',
1449
1544
  '- Slice ledger is recorded in execution.md.',
1450
1545
  '- Learning closeout is completed before task closeout.',
@@ -1561,6 +1656,13 @@ function printInitiativeNext(result) {
1561
1656
  console.log(`Initiative next: ${result.initiativeId}`);
1562
1657
  if (!result.next) {
1563
1658
  console.log('- no pending work packages');
1659
+ const blocked = (result.readiness || []).filter((item) => !item.ready);
1660
+ for (const item of blocked) {
1661
+ console.log(`- blocked: ${item.workPackage.id}`);
1662
+ for (const blocker of item.blockers) {
1663
+ console.log(` - ${blocker}`);
1664
+ }
1665
+ }
1564
1666
  return;
1565
1667
  }
1566
1668
  console.log(`- workPackage: ${result.next.id}`);
@@ -76,6 +76,110 @@ describe('initiative framework', () => {
76
76
  expect(workPackage).toContain('Task: TASK-001-foundation');
77
77
  });
78
78
 
79
+ it('selects the next work package with dependency readiness', () => {
80
+ const root = makeProject();
81
+ createInitiative({
82
+ projectRoot: root,
83
+ initiativeId: 'delivery-os-mvp',
84
+ title: 'Delivery OS MVP',
85
+ });
86
+ addWorkPackage({
87
+ projectRoot: root,
88
+ initiativeId: 'delivery-os-mvp',
89
+ workPackageId: 'WP-001-foundation',
90
+ title: 'Foundation',
91
+ });
92
+ addWorkPackage({
93
+ projectRoot: root,
94
+ initiativeId: 'delivery-os-mvp',
95
+ workPackageId: 'WP-003-process-meeting',
96
+ title: 'Process Meeting',
97
+ });
98
+ const wp3Path = path.join(root, 'ops', 'agent-pipeline', 'initiatives', 'delivery-os-mvp', 'work-packages', 'WP-003-process-meeting', 'work-package.md');
99
+ fs.writeFileSync(wp3Path, fs.readFileSync(wp3Path, 'utf8').replace(
100
+ '- Depends on: (none)',
101
+ '- Depends on: WP-001-foundation',
102
+ ));
103
+
104
+ let next = initiativeNext({
105
+ projectRoot: root,
106
+ initiativeId: 'delivery-os-mvp',
107
+ });
108
+
109
+ expect(next.next.id).toBe('WP-001-foundation');
110
+ expect(next.readiness.find((item) => item.workPackage.id === 'WP-003-process-meeting').blockers[0]).toContain('Dependency WP-001-foundation');
111
+
112
+ const wp1Path = path.join(root, 'ops', 'agent-pipeline', 'initiatives', 'delivery-os-mvp', 'work-packages', 'WP-001-foundation', 'work-package.md');
113
+ fs.writeFileSync(wp1Path, fs.readFileSync(wp1Path, 'utf8').replace('Status: pending', 'Status: completed'));
114
+
115
+ next = initiativeNext({
116
+ projectRoot: root,
117
+ initiativeId: 'delivery-os-mvp',
118
+ });
119
+
120
+ expect(next.next.id).toBe('WP-003-process-meeting');
121
+ });
122
+
123
+ it('blocks materialization when covered requirements have process-only acceptance', () => {
124
+ const root = makeProject();
125
+ createInitiative({
126
+ projectRoot: root,
127
+ initiativeId: 'delivery-os-mvp',
128
+ title: 'Delivery OS MVP',
129
+ });
130
+ addWorkPackage({
131
+ projectRoot: root,
132
+ initiativeId: 'delivery-os-mvp',
133
+ workPackageId: 'WP-001-foundation',
134
+ title: 'Foundation',
135
+ });
136
+ const wpPath = path.join(root, 'ops', 'agent-pipeline', 'initiatives', 'delivery-os-mvp', 'work-packages', 'WP-001-foundation', 'work-package.md');
137
+ fs.writeFileSync(wpPath, [
138
+ '# Work Package',
139
+ '',
140
+ 'ID: WP-001-foundation',
141
+ 'Initiative: delivery-os-mvp',
142
+ 'Title: Foundation',
143
+ 'Status: pending',
144
+ 'Task:',
145
+ 'Mode: standard_work_package',
146
+ '',
147
+ '## Goal',
148
+ '',
149
+ 'Create foundation.',
150
+ '',
151
+ '## Requirements Coverage',
152
+ '',
153
+ '- REQ-002',
154
+ '',
155
+ '## Dependencies',
156
+ '',
157
+ '- Depends on: (none)',
158
+ '',
159
+ '## Slices',
160
+ '',
161
+ '- Slice 1: Build foundation.',
162
+ '',
163
+ '## Acceptance',
164
+ '',
165
+ '- Work-package task has completed Verify.',
166
+ '- Slice ledger is recorded in execution.md.',
167
+ '- Learning closeout is completed before task closeout.',
168
+ '',
169
+ ].join('\n'));
170
+
171
+ const next = initiativeNext({
172
+ projectRoot: root,
173
+ initiativeId: 'delivery-os-mvp',
174
+ materializeTask: true,
175
+ });
176
+
177
+ expect(next.next).toBeNull();
178
+ expect(next.materializedTask).toBeNull();
179
+ expect(next.readiness[0].blockers[0]).toContain('Acceptance is process-only');
180
+ expect(fs.existsSync(path.join(root, 'ops', 'agent-pipeline', 'tasks', 'TASK-001-foundation'))).toBe(false);
181
+ });
182
+
79
183
  it('indexes source docs and builds a requirements map', () => {
80
184
  const root = makeProject();
81
185
  const docsDir = path.join(root, 'docs', 'delivery-os');
@@ -593,6 +697,92 @@ describe('initiative framework', () => {
593
697
  });
594
698
  expect(synthesis.rewriteRequired).toBe(true);
595
699
  });
700
+
701
+ it('blocks planning when a non-closed schema foundation omits speaker and meeting fields', () => {
702
+ const root = makeProject();
703
+ const docsDir = path.join(root, 'docs', 'delivery-os');
704
+ fs.mkdirSync(docsDir, { recursive: true });
705
+ fs.writeFileSync(path.join(docsDir, '06-roadmap.md'), [
706
+ '# Phase 1',
707
+ '',
708
+ '- Migration 001b: person_aliases. Meeting fields: meeting_track, series_id, asr_quality, speaker_resolution_status, raw_speaker_labels.',
709
+ ].join('\n'));
710
+ createInitiative({
711
+ projectRoot: root,
712
+ initiativeId: 'delivery-os-mvp',
713
+ title: 'Delivery OS MVP',
714
+ });
715
+ initiativeIntake({
716
+ projectRoot: root,
717
+ initiativeId: 'delivery-os-mvp',
718
+ docsDir: 'docs/delivery-os',
719
+ phase: 'phase-1',
720
+ });
721
+ const initiativeDir = path.join(root, 'ops', 'agent-pipeline', 'initiatives', 'delivery-os-mvp');
722
+ fs.writeFileSync(path.join(initiativeDir, 'requirements-map.md'), [
723
+ '# Requirements Map',
724
+ '',
725
+ '## Requirement Details',
726
+ '',
727
+ '### REQ-002',
728
+ '',
729
+ '- Status: `approved`',
730
+ '- Source: `docs/delivery-os/04-data-model.md:1206`',
731
+ '- Source hash: `sha`',
732
+ '- Phase: `phase-1`',
733
+ '- Quality: `complete`',
734
+ '- Quality issue: (none)',
735
+ '- Work package: WP-001',
736
+ '- Acceptance: Schema includes approved Phase 1 entities plus attribution/access foundations: created_by, app.current_user_id, owner_id, entity_history, enforcement-deferred.',
737
+ '- Decision: `approve`',
738
+ '- Notes: Human-approved attribution update.',
739
+ '- Requirement: Minimal Phase 1 Schema And Attribution Foundations.',
740
+ '',
741
+ '### REQ-003',
742
+ '',
743
+ '- Status: `approved`',
744
+ '- Source: `docs/delivery-os/06-roadmap.md:10`',
745
+ '- Source hash: `sha`',
746
+ '- Phase: `phase-1`',
747
+ '- Quality: `complete`',
748
+ '- Quality issue: (none)',
749
+ '- Work package: WP-003',
750
+ '- Acceptance: Meetings are classified with meeting_track.',
751
+ '- Decision: `approve`',
752
+ '- Notes: (empty)',
753
+ '- Requirement: Phase 1 classification uses meeting_track for client/internal/tracker/advisor_network/investor/grant_funding.',
754
+ '',
755
+ '### REQ-004',
756
+ '',
757
+ '- Status: `approved`',
758
+ '- Source: `docs/delivery-os/13-meeting-extraction-profiles.md:240`',
759
+ '- Source hash: `sha`',
760
+ '- Phase: `phase-1`',
761
+ '- Quality: `complete`',
762
+ '- Quality issue: (none)',
763
+ '- Work package: WP-001, WP-003',
764
+ '- Acceptance: Speaker resolution stores raw speaker labels/aliases and speaker_resolution_status.',
765
+ '- Decision: `approve`',
766
+ '- Notes: (empty)',
767
+ '- Requirement: Phase 1 supports speaker identity resolution and alias storage.',
768
+ '',
769
+ ].join('\n'));
770
+
771
+ const synthesis = initiativeRequirementsSynthesis({
772
+ projectRoot: root,
773
+ initiativeId: 'delivery-os-mvp',
774
+ });
775
+
776
+ const schemaGate = synthesis.gates.find((gate) => gate.id === 'schema-supports-derived-requirements');
777
+ expect(schemaGate).toMatchObject({
778
+ severity: 'critical',
779
+ blocking: true,
780
+ });
781
+ expect(schemaGate.issue).toContain('schema/foundation contract');
782
+ expect(schemaGate.issue).toContain('person_aliases');
783
+ expect(schemaGate.issue).toContain('meeting_track');
784
+ expect(schemaGate.action).toContain('meetings.raw_speaker_labels');
785
+ });
596
786
  });
597
787
 
598
788
  function makeProject() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@besales/ops-framework",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "ops-agent": "bin/ops-agent.mjs"