@besales/ops-framework 0.1.5 → 0.1.6

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,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.6
4
+
5
+ - Added `ops-agent closeout <TASK>` as the Supervisor entrypoint before final task closure.
6
+ - Closeout now validates Verify and Retrospective readiness, automatically runs learning closeout, pauses for unresolved learning decisions and can apply approved learning with `--apply-approved`.
7
+ - Added generated project script support for `agent:closeout`.
8
+
3
9
  ## 0.1.5
4
10
 
5
11
  - Added `ops-agent learning-closeout <TASK>` as the default post-retrospective learning checkpoint.
package/README.md CHANGED
@@ -179,6 +179,7 @@ Do not commit that `file:` dependency to production projects. It is only for pac
179
179
  - `learning-audit`
180
180
  - `learning-report`
181
181
  - `learning-closeout`
182
+ - `closeout`
182
183
  - `test/self-test`
183
184
 
184
185
  ## Learning Loop
@@ -203,6 +204,7 @@ ops-agent learning-audit
203
204
  ops-agent update-memory --apply-approved
204
205
  ops-agent learning-report
205
206
  ops-agent learning-closeout TASK-001-example
207
+ ops-agent closeout TASK-001-example
206
208
  ```
207
209
 
208
210
  `memory-candidates` creates structured learning cards with source, reason hash, learning layer, confidence, problem, lesson, repeat risk, proposed wording and suggested target. `learning-index` turns those cards into human-reviewable decisions. `update-memory` only writes approved entries when `--apply-approved` is passed.
@@ -213,6 +215,8 @@ ops-agent learning-closeout TASK-001-example
213
215
 
214
216
  `learning-closeout <TASK>` is the default closeout checkpoint after retrospective. It collects candidates from that task, refreshes `learning-index.json`, writes `learning-review.md`, writes `learning-report.md`, creates `learning-closeout.md` inside the task and updates `status.md` so human approval is visible. The review must show every learning candidate individually with summary, target, source artifact, reason, proposed change, scope, confidence, promotion risk and the current decision.
215
217
 
218
+ `closeout <TASK>` is the Supervisor entrypoint before any final closure. It validates `verify.result.json` and `retrospective.md`, automatically runs `learning-closeout <TASK>`, pauses when learning decisions are still `pending` or `rewrite`, and after `--apply-approved` applies approved learning, refreshes the report and runs the final guard unless `--skip-guard` is passed.
219
+
216
220
  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.
217
221
 
218
222
  ## Feedback Intake
@@ -0,0 +1,224 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { spawnSync } from 'node:child_process';
4
+ import { fileURLToPath } from 'node:url';
5
+ import {
6
+ appendOrchestrationLog,
7
+ parseCliArgs,
8
+ projectContext,
9
+ readJsonFile,
10
+ readTaskFile,
11
+ repoRoot,
12
+ resolveTaskDir,
13
+ updateStatus,
14
+ } from './lib/check-context-utils.mjs';
15
+ import {
16
+ INDEX_FILE,
17
+ REPORT_FILE,
18
+ updateMemory,
19
+ writeLearningCloseout,
20
+ writeLearningReport,
21
+ } from './learning-loop.mjs';
22
+
23
+ export function main() {
24
+ const args = parseCliArgs(process.argv.slice(2));
25
+ const taskArg = args.positional[0] || args.flags.get('task');
26
+ if (!taskArg) {
27
+ fail('Usage: ops-agent closeout <TASK-id-or-task-path> [--apply-approved] [--skip-guard]');
28
+ }
29
+
30
+ try {
31
+ const result = closeoutTask({
32
+ taskArg,
33
+ applyApproved: args.flags.has('apply-approved'),
34
+ runGuard: !args.flags.has('skip-guard'),
35
+ });
36
+ printCloseoutResult(result);
37
+ if (!result.ok) {
38
+ process.exit(1);
39
+ }
40
+ } catch (error) {
41
+ fail(error.message);
42
+ }
43
+ }
44
+
45
+ export function closeoutTask({ taskArg, applyApproved = false, runGuard = true } = {}) {
46
+ const taskDir = resolveTaskDir(taskArg);
47
+ const taskId = path.basename(taskDir);
48
+ const prerequisiteIssues = validateCloseoutPrerequisites(taskDir);
49
+ if (prerequisiteIssues.length) {
50
+ updateStatus(taskDir, {
51
+ stage: 'Closeout Blocked',
52
+ supervisorAction: 'Closeout orchestration blocked by missing prerequisites.',
53
+ nextStep: prerequisiteIssues.join(' '),
54
+ humanApproval: 'no',
55
+ });
56
+ appendOrchestrationLog(taskDir, `closeout blocked; issues=${prerequisiteIssues.length}`);
57
+ return {
58
+ ok: false,
59
+ taskId,
60
+ status: 'blocked_prerequisites',
61
+ issues: prerequisiteIssues,
62
+ };
63
+ }
64
+
65
+ writeLearningCloseout({ taskArg });
66
+ const beforeApply = readLearningDecisionSummary();
67
+ if (applyApproved) {
68
+ updateMemory({ applyApproved: true });
69
+ writeLearningReport();
70
+ }
71
+ const afterApply = readLearningDecisionSummary();
72
+ const unresolved = afterApply.pending + afterApply.rewrite;
73
+
74
+ if (unresolved > 0) {
75
+ updateStatus(taskDir, {
76
+ stage: 'Retrospective / Learning Review',
77
+ supervisorAction: 'Closeout paused for human learning decisions.',
78
+ nextStep: `Review ${relativeProjectPath(path.join(projectContext.memoryRoot, 'learning-review.md'))}; set pending/rewrite entries in ${relativeProjectPath(path.join(projectContext.memoryRoot, INDEX_FILE))} to promote, defer or reject; rerun agent:closeout --apply-approved.`,
79
+ humanApproval: 'yes',
80
+ });
81
+ appendOrchestrationLog(taskDir, `closeout paused for learning decisions; pending=${afterApply.pending}; rewrite=${afterApply.rewrite}`);
82
+ return {
83
+ ok: false,
84
+ taskId,
85
+ status: 'learning_review_required',
86
+ beforeApply,
87
+ afterApply,
88
+ issues: [`Learning decisions unresolved: pending=${afterApply.pending}, rewrite=${afterApply.rewrite}.`],
89
+ };
90
+ }
91
+
92
+ updateStatus(taskDir, {
93
+ stage: 'Closeout Audit',
94
+ supervisorAction: 'Closeout orchestration completed learning loop; ready for final guard.',
95
+ nextStep: 'Review learning-report.md and guard-task result before marking the task Closed, Accepted or Ready To Pause.',
96
+ humanApproval: 'yes',
97
+ });
98
+ appendOrchestrationLog(taskDir, `closeout learning loop resolved; promoted=${afterApply.promote}; deferred=${afterApply.defer}; rejected=${afterApply.reject}`);
99
+
100
+ const guard = runGuard ? runGuardTask(taskArg) : { ok: true, skipped: true, output: 'guard skipped by --skip-guard' };
101
+ if (!guard.ok) {
102
+ return {
103
+ ok: false,
104
+ taskId,
105
+ status: 'guard_failed',
106
+ beforeApply,
107
+ afterApply,
108
+ guard,
109
+ issues: ['Closeout guard failed. Fix listed issues before final closure.'],
110
+ };
111
+ }
112
+
113
+ return {
114
+ ok: true,
115
+ taskId,
116
+ status: 'ready_for_human_closeout',
117
+ beforeApply,
118
+ afterApply,
119
+ guard,
120
+ issues: [],
121
+ };
122
+ }
123
+
124
+ export function validateCloseoutPrerequisites(taskDir) {
125
+ const issues = [];
126
+ const verifyPath = path.join(taskDir, 'verify.result.json');
127
+ if (!fs.existsSync(verifyPath)) {
128
+ issues.push('verify.result.json is missing; run Verify before closeout.');
129
+ } else {
130
+ try {
131
+ const verify = readJsonFile(verifyPath);
132
+ if (!['pass', 'pass_with_notes'].includes(verify.verdict)) {
133
+ issues.push(`verify.result.json verdict=${verify.verdict}; closeout requires pass or pass_with_notes.`);
134
+ }
135
+ } catch (error) {
136
+ issues.push(`verify.result.json is invalid JSON: ${error.message}`);
137
+ }
138
+ }
139
+
140
+ const retrospective = readTaskFile(taskDir, 'retrospective.md');
141
+ if (!retrospective.trim()) {
142
+ issues.push('retrospective.md is missing or empty; fill Retrospective before closeout.');
143
+ }
144
+ if (/\[fill in\]|`not_started`|Retrospective has not started/i.test(retrospective)) {
145
+ issues.push('retrospective.md still contains placeholders or not_started markers; complete Retrospective before closeout.');
146
+ }
147
+ return issues;
148
+ }
149
+
150
+ function readLearningDecisionSummary() {
151
+ const indexPath = path.join(projectContext.memoryRoot, INDEX_FILE);
152
+ if (!fs.existsSync(indexPath)) {
153
+ return {
154
+ total: 0,
155
+ pending: 0,
156
+ promote: 0,
157
+ defer: 0,
158
+ reject: 0,
159
+ rewrite: 0,
160
+ };
161
+ }
162
+ const index = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
163
+ const summary = {
164
+ total: 0,
165
+ pending: 0,
166
+ promote: 0,
167
+ defer: 0,
168
+ reject: 0,
169
+ rewrite: 0,
170
+ };
171
+ for (const entry of index.entries || []) {
172
+ const decision = entry.decision || 'pending';
173
+ summary.total += 1;
174
+ summary[decision] = (summary[decision] || 0) + 1;
175
+ }
176
+ return summary;
177
+ }
178
+
179
+ function runGuardTask(taskArg) {
180
+ const result = spawnSync(process.execPath, [path.join(path.dirname(fileURLToPath(import.meta.url)), 'guard-task.mjs'), taskArg], {
181
+ cwd: repoRoot,
182
+ encoding: 'utf8',
183
+ });
184
+ return {
185
+ ok: result.status === 0,
186
+ status: result.status,
187
+ output: `${result.stdout || ''}${result.stderr || ''}`.trim(),
188
+ };
189
+ }
190
+
191
+ function printCloseoutResult(result) {
192
+ console.log(`Closeout ${result.status} for ${result.taskId}`);
193
+ for (const issue of result.issues || []) {
194
+ console.log(`- ${issue}`);
195
+ }
196
+ if (result.afterApply) {
197
+ console.log(`- learning total: ${result.afterApply.total}`);
198
+ console.log(`- learning pending: ${result.afterApply.pending}`);
199
+ console.log(`- learning rewrite: ${result.afterApply.rewrite}`);
200
+ console.log(`- learning promoted: ${result.afterApply.promote}`);
201
+ console.log(`- learning deferred: ${result.afterApply.defer}`);
202
+ console.log(`- learning rejected: ${result.afterApply.reject}`);
203
+ console.log(`- learning report: ${relativeProjectPath(path.join(projectContext.memoryRoot, REPORT_FILE))}`);
204
+ }
205
+ if (result.guard) {
206
+ console.log(`- guard: ${result.guard.ok ? 'ok' : 'failed'}`);
207
+ if (result.guard.output) {
208
+ console.log(result.guard.output);
209
+ }
210
+ }
211
+ }
212
+
213
+ function relativeProjectPath(filePath) {
214
+ return path.relative(projectContext.projectRoot, filePath) || path.basename(filePath);
215
+ }
216
+
217
+ function fail(message) {
218
+ console.error(`Error: ${message}`);
219
+ process.exit(1);
220
+ }
221
+
222
+ if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
223
+ main();
224
+ }
@@ -0,0 +1,46 @@
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 { validateCloseoutPrerequisites } from './closeout.mjs';
6
+
7
+ describe('closeout', () => {
8
+ it('blocks closeout when verify result is missing and retrospective still has placeholders', () => {
9
+ const taskDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ops-closeout-blocked-'));
10
+ fs.writeFileSync(path.join(taskDir, 'retrospective.md'), [
11
+ '# Retrospective',
12
+ '',
13
+ '## Статус',
14
+ '',
15
+ '`not_started`',
16
+ '',
17
+ '- `ops-agent closeout <TASK>` запущен как основной closeout entrypoint: `[fill in]`',
18
+ ].join('\n'));
19
+
20
+ const issues = validateCloseoutPrerequisites(taskDir);
21
+
22
+ expect(issues).toContain('verify.result.json is missing; run Verify before closeout.');
23
+ expect(issues).toContain('retrospective.md still contains placeholders or not_started markers; complete Retrospective before closeout.');
24
+ });
25
+
26
+ it('allows closeout prerequisites when verify passed and retrospective is complete', () => {
27
+ const taskDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ops-closeout-ready-'));
28
+ fs.writeFileSync(path.join(taskDir, 'verify.result.json'), JSON.stringify({
29
+ schemaVersion: 1,
30
+ verdict: 'pass_with_notes',
31
+ }, null, 2));
32
+ fs.writeFileSync(path.join(taskDir, 'retrospective.md'), [
33
+ '# Retrospective',
34
+ '',
35
+ '## Статус',
36
+ '',
37
+ '`completed`',
38
+ '',
39
+ '## Переиспользуемые выводы',
40
+ '',
41
+ '- Feedback capture worked and learning review was shown.',
42
+ ].join('\n'));
43
+
44
+ expect(validateCloseoutPrerequisites(taskDir)).toEqual([]);
45
+ });
46
+ });
@@ -120,6 +120,7 @@ export function buildOpsScripts(packageSpec) {
120
120
  'agent:learning-audit': run('learning-audit'),
121
121
  'agent:learning-report': run('learning-report'),
122
122
  'agent:learning-closeout': run('learning-closeout'),
123
+ 'agent:closeout': run('closeout'),
123
124
  'agent:test': run('test/self-test'),
124
125
  };
125
126
  }
@@ -139,6 +139,7 @@ describe('buildOpsScripts', () => {
139
139
  expect(scripts.ops).toBe('ops-agent');
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
+ expect(scripts['agent:closeout']).toBe('ops-agent closeout');
142
143
  expect(scripts['agent:test']).toBe('ops-agent test/self-test');
143
144
  });
144
145
  });
package/bin/ops-agent.mjs CHANGED
@@ -33,6 +33,7 @@ const COMMANDS = new Map([
33
33
  ['learning-audit', 'learning-loop.mjs'],
34
34
  ['learning-report', 'learning-loop.mjs'],
35
35
  ['learning-closeout', 'learning-loop.mjs'],
36
+ ['closeout', 'closeout.mjs'],
36
37
  ['test/self-test', null],
37
38
  ]);
38
39
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@besales/ops-framework",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "ops-agent": "bin/ops-agent.mjs"
@@ -193,7 +193,7 @@ Allowed ref types:
193
193
  - `human_arbitration_required` -> route to `Human Arbitration`;
194
194
  - `verifier_failed` -> остаться в `Verify` до remediation или провайдера/контекста.
195
195
  4. `Retrospective`: `retrospective.md` заполнен и содержит итоговый статус/verdict.
196
- 5. `Learning Loop Audit`: Supervisor запускает `learning-closeout <TASK>` или эквивалентную последовательность `memory-candidates`, `learning-index`, `learning-review`, human decisions, `update-memory --apply-approved` для promoted entries and `learning-report`; human должен видеть каждый learning отдельно: summary, target, source artifact, reason, proposed change, scope, confidence, risk, current decision и куда approved entries записаны.
196
+ 5. `Learning Loop Audit`: Supervisor запускает `closeout <TASK>` как основной вход в закрытие. Эта команда валидирует Verify/Retrospective, автоматически запускает `learning-closeout <TASK>`, останавливает закрытие при pending/rewrite learning decisions, а после human decisions запускается повторно с `--apply-approved`. Human должен видеть каждый learning отдельно: summary, target, source artifact, reason, proposed change, scope, confidence, risk, current decision и куда approved entries записаны.
197
197
  6. `Closeout Audit`: Supervisor проверяет `status.md`, `verify.md`, `verify.result.json`, `retrospective.md`, `orchestration-log.md`, `learning-report.md`, deferred/follow-up фиксацию и `git diff --check`.
198
198
  7. `Human Closeout Gate`: только после audit можно предлагать закрытие/паузу/task switch.
199
199
 
@@ -16,6 +16,7 @@
16
16
 
17
17
  ## Learning Loop Audit
18
18
 
19
+ - `ops-agent closeout <TASK>` запущен как основной closeout entrypoint: `[fill in]`
19
20
  - `ops-agent learning-closeout <TASK>` выполнен после заполнения retrospective: `[fill in]`
20
21
  - `ops-agent memory-candidates` выполнен после заполнения retrospective/check/verify feedback: `[fill in]`
21
22
  - `ops-agent learning-index` создал human-reviewable decisions: `[fill in]`