@besales/ops-framework 0.1.17 → 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 +6 -0
- package/README.md +2 -0
- package/bin/initiative.mjs +102 -1
- package/bin/initiative.test.mjs +104 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
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
|
+
|
|
3
9
|
## 0.1.17
|
|
4
10
|
|
|
5
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.
|
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
|
package/bin/initiative.mjs
CHANGED
|
@@ -202,12 +202,14 @@ export function initiativeNext({
|
|
|
202
202
|
force = false,
|
|
203
203
|
} = {}) {
|
|
204
204
|
const status = initiativeStatus({ projectRoot, initiativeId });
|
|
205
|
-
const
|
|
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
|
|
|
@@ -1327,6 +1330,7 @@ function listWorkPackages(initiativeDir) {
|
|
|
1327
1330
|
|
|
1328
1331
|
function readWorkPackage(filePath, fallbackId) {
|
|
1329
1332
|
const content = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
|
|
1333
|
+
const dependencies = readWorkPackageDependencies(content);
|
|
1330
1334
|
return {
|
|
1331
1335
|
id: readInlineField(content, 'ID') || fallbackId,
|
|
1332
1336
|
title: readInlineField(content, 'Title') || humanizeSlug(fallbackId.replace(/^WP-\d{3}-/, '')),
|
|
@@ -1334,11 +1338,89 @@ function readWorkPackage(filePath, fallbackId) {
|
|
|
1334
1338
|
task: readInlineField(content, 'Task') || '',
|
|
1335
1339
|
mode: readInlineField(content, 'Mode') || 'standard_work_package',
|
|
1336
1340
|
goal: readSection(content, 'Goal') || '',
|
|
1341
|
+
requirementsCoverage: readBulletsFromSection(content, 'Requirements Coverage'),
|
|
1337
1342
|
slices: readBulletsFromSection(content, 'Slices'),
|
|
1343
|
+
acceptance: readBulletsFromSection(content, 'Acceptance'),
|
|
1344
|
+
dependencies,
|
|
1338
1345
|
path: filePath,
|
|
1339
1346
|
};
|
|
1340
1347
|
}
|
|
1341
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
|
+
|
|
1342
1424
|
function updateWorkPackageStatus({ workPackagePath, status, taskId }) {
|
|
1343
1425
|
const content = fs.readFileSync(workPackagePath, 'utf8');
|
|
1344
1426
|
let updated = content
|
|
@@ -1438,6 +1520,17 @@ function buildWorkPackageMarkdown({ initiativeId, workPackageId, title, goal, mo
|
|
|
1438
1520
|
'- Excluded:',
|
|
1439
1521
|
'- Escalate to separate task if:',
|
|
1440
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
|
+
'',
|
|
1441
1534
|
'## Slices',
|
|
1442
1535
|
'',
|
|
1443
1536
|
'- Slice 1: Define the first implementation slice.',
|
|
@@ -1446,6 +1539,7 @@ function buildWorkPackageMarkdown({ initiativeId, workPackageId, title, goal, mo
|
|
|
1446
1539
|
'',
|
|
1447
1540
|
'## Acceptance',
|
|
1448
1541
|
'',
|
|
1542
|
+
'- Covered REQ-* acceptance criteria are satisfied or explicitly deferred with human approval.',
|
|
1449
1543
|
'- Work-package task has completed Verify.',
|
|
1450
1544
|
'- Slice ledger is recorded in execution.md.',
|
|
1451
1545
|
'- Learning closeout is completed before task closeout.',
|
|
@@ -1562,6 +1656,13 @@ function printInitiativeNext(result) {
|
|
|
1562
1656
|
console.log(`Initiative next: ${result.initiativeId}`);
|
|
1563
1657
|
if (!result.next) {
|
|
1564
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
|
+
}
|
|
1565
1666
|
return;
|
|
1566
1667
|
}
|
|
1567
1668
|
console.log(`- workPackage: ${result.next.id}`);
|
package/bin/initiative.test.mjs
CHANGED
|
@@ -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');
|