@besales/ops-framework 0.1.3 → 0.1.5
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 +15 -0
- package/README.md +18 -11
- package/bin/guard-task.mjs +25 -0
- package/bin/learning-loop.mjs +199 -39
- package/bin/learning-loop.test.mjs +23 -0
- package/bin/lib/bootstrap-utils.mjs +23 -2
- package/bin/lib/bootstrap-utils.test.mjs +8 -7
- package/bin/lib/llm-input-pack-utils.mjs +1 -10
- package/bin/lib/llm-input-pack-utils.test.mjs +3 -3
- package/bin/lib/task-manifest-utils.mjs +10 -1
- package/bin/ops-agent.mjs +1 -0
- package/bin/run-check.mjs +165 -2
- package/package.json +1 -1
- package/prompts/supervisor.md +1 -1
- package/templates/retrospective.md +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.5
|
|
4
|
+
|
|
5
|
+
- Added `ops-agent learning-closeout <TASK>` as the default post-retrospective learning checkpoint.
|
|
6
|
+
- Expanded `learning-review.md` into per-learning approval cards with summary, target, source, reason, proposed change, scope, confidence, risk and decision.
|
|
7
|
+
- Preserved existing pending learning index entries when refreshing task-specific learning candidates.
|
|
8
|
+
- Added closeout guard coverage for `learning-closeout.md`, `learning-index.json`, `learning-review.md` and `learning-report.md`.
|
|
9
|
+
- Added generated project script support for `agent:learning-closeout`.
|
|
10
|
+
|
|
11
|
+
## 0.1.4
|
|
12
|
+
|
|
13
|
+
- Switched generated project scripts from repeated `yarn dlx` execution to installed `ops-agent` package scripts.
|
|
14
|
+
- Added deterministic Check preflight to skip external checker calls when manifest/context quality gates already block Human Gate.
|
|
15
|
+
- Changed Check context policy to standard-first with strict escalation only when needed.
|
|
16
|
+
- Added Check timing telemetry in `task-manifest.json`.
|
|
17
|
+
|
|
3
18
|
## 0.1.0
|
|
4
19
|
|
|
5
20
|
- Created the first local shared Ops Framework package boundary.
|
package/README.md
CHANGED
|
@@ -49,16 +49,19 @@ The recommended setup is:
|
|
|
49
49
|
corepack yarn dlx @besales/ops-framework@latest init --project-name ExampleProject --install-scripts
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
-
This creates the project-owned `ops/**` structure, including `ops/project.ops.yaml`, task roots, cache, memory files and project-local agent config. With `--install-scripts`, it also writes package-manager scripts that call the
|
|
52
|
+
This creates the project-owned `ops/**` structure, including `ops/project.ops.yaml`, task roots, cache, memory files and project-local agent config. With `--install-scripts`, it also writes package-manager scripts that call the installed `ops-agent` bin and pins the framework package in `devDependencies`.
|
|
53
53
|
|
|
54
54
|
The generated scripts look like:
|
|
55
55
|
|
|
56
56
|
```json
|
|
57
57
|
{
|
|
58
58
|
"scripts": {
|
|
59
|
-
"ops": "
|
|
60
|
-
"agent:run-check": "
|
|
61
|
-
"agent:run-verify": "
|
|
59
|
+
"ops": "ops-agent",
|
|
60
|
+
"agent:run-check": "ops-agent run-check",
|
|
61
|
+
"agent:run-verify": "ops-agent run-verify"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@besales/ops-framework": "0.1.0"
|
|
62
65
|
}
|
|
63
66
|
}
|
|
64
67
|
```
|
|
@@ -119,13 +122,13 @@ The framework blocks obvious inefficiency early, but speculative or broad optimi
|
|
|
119
122
|
From a project root, run commands through the package entry point:
|
|
120
123
|
|
|
121
124
|
```bash
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
125
|
+
ops-agent init --project-name ExampleProject --install-scripts
|
|
126
|
+
ops-agent new-task TASK-001-example --title "Example task"
|
|
127
|
+
ops-agent manifest TASK-001-example
|
|
128
|
+
ops-agent preflight TASK-001-example --target execute
|
|
129
|
+
ops-agent build-check-context TASK-001-example
|
|
130
|
+
ops-agent run-check TASK-001-example
|
|
131
|
+
ops-agent run-verify TASK-001-example
|
|
129
132
|
```
|
|
130
133
|
|
|
131
134
|
When installed as a package, the intended command is:
|
|
@@ -175,6 +178,7 @@ Do not commit that `file:` dependency to production projects. It is only for pac
|
|
|
175
178
|
- `update-memory`
|
|
176
179
|
- `learning-audit`
|
|
177
180
|
- `learning-report`
|
|
181
|
+
- `learning-closeout`
|
|
178
182
|
- `test/self-test`
|
|
179
183
|
|
|
180
184
|
## Learning Loop
|
|
@@ -198,6 +202,7 @@ ops-agent learning-review
|
|
|
198
202
|
ops-agent learning-audit
|
|
199
203
|
ops-agent update-memory --apply-approved
|
|
200
204
|
ops-agent learning-report
|
|
205
|
+
ops-agent learning-closeout TASK-001-example
|
|
201
206
|
```
|
|
202
207
|
|
|
203
208
|
`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.
|
|
@@ -206,6 +211,8 @@ ops-agent learning-report
|
|
|
206
211
|
|
|
207
212
|
`learning-report` writes `ops/agent-pipeline/memory/learning-report.md`. Show the review pack before approval and the report during closeout so the human can see what the framework learned, what is still pending, and which approved entries were written to project memory or project playbooks.
|
|
208
213
|
|
|
214
|
+
`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
|
+
|
|
209
216
|
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.
|
|
210
217
|
|
|
211
218
|
## Feedback Intake
|
package/bin/guard-task.mjs
CHANGED
|
@@ -33,6 +33,7 @@ function main() {
|
|
|
33
33
|
validateHumanGateSummary(taskDir, errors);
|
|
34
34
|
validateFeedback(taskDir, errors);
|
|
35
35
|
validateExecutionEvidence(taskDir, errors);
|
|
36
|
+
validateLearningCloseout(taskDir, errors);
|
|
36
37
|
validateStatusSync(taskDir, errors);
|
|
37
38
|
|
|
38
39
|
if (errors.length > 0) {
|
|
@@ -49,6 +50,30 @@ function main() {
|
|
|
49
50
|
}
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
function validateLearningCloseout(taskDir, errors) {
|
|
54
|
+
const stage = readStatusStage(taskDir);
|
|
55
|
+
const stagesRequiringLearningCloseout = new Set([
|
|
56
|
+
'Closeout Audit',
|
|
57
|
+
'Human Closeout Gate',
|
|
58
|
+
'Closed',
|
|
59
|
+
'Accepted',
|
|
60
|
+
'Ready To Pause',
|
|
61
|
+
]);
|
|
62
|
+
if (!stagesRequiringLearningCloseout.has(stage)) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const taskLearningCloseoutPath = path.join(taskDir, 'learning-closeout.md');
|
|
66
|
+
if (!fs.existsSync(taskLearningCloseoutPath)) {
|
|
67
|
+
errors.push('learning-closeout.md is missing before closeout. Run ops-agent learning-closeout <TASK> and show learning-review.md to human.');
|
|
68
|
+
}
|
|
69
|
+
for (const fileName of ['learning-index.json', 'learning-review.md', 'learning-report.md']) {
|
|
70
|
+
const filePath = path.join(projectContext.memoryRoot, fileName);
|
|
71
|
+
if (!fs.existsSync(filePath)) {
|
|
72
|
+
errors.push(`${fileName} is missing before closeout. Run ops-agent learning-closeout <TASK>.`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
52
77
|
function validateHumanGateSummary(taskDir, errors) {
|
|
53
78
|
const stage = readStatusStage(taskDir);
|
|
54
79
|
if (stage !== 'Human Gate') {
|
package/bin/learning-loop.mjs
CHANGED
|
@@ -3,9 +3,12 @@ import fs from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import {
|
|
6
|
+
appendOrchestrationLog,
|
|
6
7
|
getFlag,
|
|
7
8
|
parseCliArgs,
|
|
8
9
|
projectContext,
|
|
10
|
+
resolveTaskDir,
|
|
11
|
+
updateStatus,
|
|
9
12
|
} from './lib/check-context-utils.mjs';
|
|
10
13
|
|
|
11
14
|
export const CANDIDATES_FILE = 'learning-candidates.md';
|
|
@@ -13,6 +16,7 @@ export const INDEX_FILE = 'learning-index.json';
|
|
|
13
16
|
export const APPROVED_FILE = 'approved-learning.md';
|
|
14
17
|
export const REPORT_FILE = 'learning-report.md';
|
|
15
18
|
export const REVIEW_FILE = 'learning-review.md';
|
|
19
|
+
export const CLOSEOUT_FILE = 'learning-closeout.md';
|
|
16
20
|
const PROJECT_PLAYBOOK_TARGET_PREFIX = 'project-playbook/';
|
|
17
21
|
const MEMORY_TARGET_PREFIX = 'memory/';
|
|
18
22
|
|
|
@@ -44,15 +48,22 @@ export function main() {
|
|
|
44
48
|
writeLearningReport();
|
|
45
49
|
return;
|
|
46
50
|
}
|
|
47
|
-
|
|
51
|
+
if (command === 'learning-closeout') {
|
|
52
|
+
writeLearningCloseout({
|
|
53
|
+
taskArg: args.positional[0] || getFlag(args, 'task'),
|
|
54
|
+
limit: Number(getFlag(args, 'limit', 20)),
|
|
55
|
+
});
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
fail('Usage: ops-agent memory-candidates|learning-index|learning-review|update-memory|learning-audit|learning-report|learning-closeout');
|
|
48
59
|
} catch (error) {
|
|
49
60
|
fail(error.message);
|
|
50
61
|
}
|
|
51
62
|
}
|
|
52
63
|
|
|
53
|
-
export function writeMemoryCandidates({ limit }) {
|
|
64
|
+
export function writeMemoryCandidates({ limit, taskDir = null } = {}) {
|
|
54
65
|
ensureMemoryRoot();
|
|
55
|
-
const candidates = collectLearningCandidates({ limit });
|
|
66
|
+
const candidates = collectLearningCandidates({ limit, taskDir });
|
|
56
67
|
const content = [
|
|
57
68
|
'# Learning Candidates',
|
|
58
69
|
'',
|
|
@@ -81,32 +92,21 @@ export function writeMemoryCandidates({ limit }) {
|
|
|
81
92
|
fs.writeFileSync(path.join(projectContext.memoryRoot, CANDIDATES_FILE), content.endsWith('\n') ? content : `${content}\n`);
|
|
82
93
|
console.log(`Learning candidates written: ${path.join(projectContext.memoryRoot, CANDIDATES_FILE)}`);
|
|
83
94
|
console.log(`- candidates: ${candidates.length}`);
|
|
95
|
+
return candidates;
|
|
84
96
|
}
|
|
85
97
|
|
|
86
|
-
export function writeLearningIndex() {
|
|
98
|
+
export function writeLearningIndex({ preserveExisting = true } = {}) {
|
|
87
99
|
ensureMemoryRoot();
|
|
88
100
|
const candidatesPath = path.join(projectContext.memoryRoot, CANDIDATES_FILE);
|
|
89
101
|
if (!fs.existsSync(candidatesPath)) {
|
|
90
102
|
throw new Error(`Missing ${CANDIDATES_FILE}. Run ops-agent memory-candidates first.`);
|
|
91
103
|
}
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
kind: candidate.kind,
|
|
99
|
-
learningLayer: candidate.learningLayer,
|
|
100
|
-
confidence: candidate.confidence,
|
|
101
|
-
problem: candidate.problem,
|
|
102
|
-
lesson: candidate.lesson,
|
|
103
|
-
repeatRisk: candidate.repeatRisk,
|
|
104
|
-
candidate: candidate.text,
|
|
105
|
-
proposedWording: candidate.proposedWording,
|
|
106
|
-
decision: 'pending',
|
|
107
|
-
target: candidate.suggestedTarget,
|
|
108
|
-
notes: '',
|
|
109
|
-
}));
|
|
104
|
+
const existingEntries = preserveExisting ? readExistingLearningEntries() : [];
|
|
105
|
+
const entries = mergeLearningIndexEntries({
|
|
106
|
+
candidateEntries: parseLearningCandidatesMarkdown(fs.readFileSync(candidatesPath, 'utf8'))
|
|
107
|
+
.map((candidate) => buildLearningIndexEntry(candidate, existingEntries)),
|
|
108
|
+
existingEntries,
|
|
109
|
+
});
|
|
110
110
|
const index = {
|
|
111
111
|
schemaVersion: 1,
|
|
112
112
|
generatedAt: new Date().toISOString(),
|
|
@@ -116,6 +116,7 @@ export function writeLearningIndex() {
|
|
|
116
116
|
fs.writeFileSync(path.join(projectContext.memoryRoot, INDEX_FILE), `${JSON.stringify(index, null, 2)}\n`);
|
|
117
117
|
console.log(`Learning index written: ${path.join(projectContext.memoryRoot, INDEX_FILE)}`);
|
|
118
118
|
console.log(`- entries: ${entries.length}`);
|
|
119
|
+
return index;
|
|
119
120
|
}
|
|
120
121
|
|
|
121
122
|
export function writeLearningReview({ memoryRoot = projectContext.memoryRoot, projectRoot = projectContext.projectRoot } = {}) {
|
|
@@ -173,6 +174,45 @@ export function writeLearningReview({ memoryRoot = projectContext.memoryRoot, pr
|
|
|
173
174
|
fs.writeFileSync(path.join(memoryRoot, REVIEW_FILE), content.endsWith('\n') ? content : `${content}\n`);
|
|
174
175
|
console.log(`Learning review written: ${path.join(memoryRoot, REVIEW_FILE)}`);
|
|
175
176
|
console.log(`- pending: ${pending.length}`);
|
|
177
|
+
return {
|
|
178
|
+
entries,
|
|
179
|
+
pending,
|
|
180
|
+
reviewPath: path.join(memoryRoot, REVIEW_FILE),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function writeLearningCloseout({ taskArg, limit = 20 } = {}) {
|
|
185
|
+
if (!taskArg) {
|
|
186
|
+
throw new Error('Usage: ops-agent learning-closeout <TASK-id-or-task-path> [--limit 20]');
|
|
187
|
+
}
|
|
188
|
+
const taskDir = resolveTaskDir(taskArg);
|
|
189
|
+
const taskId = path.basename(taskDir);
|
|
190
|
+
const candidates = writeMemoryCandidates({ limit, taskDir });
|
|
191
|
+
const index = writeLearningIndex({ preserveExisting: true });
|
|
192
|
+
const review = writeLearningReview();
|
|
193
|
+
writeLearningReport();
|
|
194
|
+
const closeoutPath = writeLearningCloseoutSummary({
|
|
195
|
+
taskDir,
|
|
196
|
+
taskId,
|
|
197
|
+
candidates,
|
|
198
|
+
entries: index.entries || [],
|
|
199
|
+
reviewPath: review.reviewPath,
|
|
200
|
+
});
|
|
201
|
+
updateStatus(taskDir, {
|
|
202
|
+
stage: 'Retrospective / Learning Review',
|
|
203
|
+
supervisorAction: 'Generated learning closeout candidates, review pack and report.',
|
|
204
|
+
nextStep: 'Show learning-review.md to human; set each learning-index.json decision to promote, defer, reject or rewrite; then run update-memory --apply-approved and learning-report.',
|
|
205
|
+
humanApproval: 'yes',
|
|
206
|
+
});
|
|
207
|
+
appendOrchestrationLog(taskDir, `learning closeout generated; candidates=${candidates.length}; review=${relativeProjectPath(review.reviewPath)}; summary=${path.basename(closeoutPath)}`);
|
|
208
|
+
console.log(`Learning closeout ready for ${taskId}`);
|
|
209
|
+
console.log(`- candidates: ${candidates.length}`);
|
|
210
|
+
console.log(`- review: ${relativeProjectPath(review.reviewPath)}`);
|
|
211
|
+
console.log(`- report: ${relativeProjectPath(path.join(projectContext.memoryRoot, REPORT_FILE))}`);
|
|
212
|
+
console.log(`- task summary: ${relativeProjectPath(closeoutPath)}`);
|
|
213
|
+
for (const line of renderConsoleLearningCards(index.entries || [])) {
|
|
214
|
+
console.log(line);
|
|
215
|
+
}
|
|
176
216
|
}
|
|
177
217
|
|
|
178
218
|
export function updateMemory({ applyApproved }) {
|
|
@@ -279,20 +319,26 @@ export function writeLearningReport({ memoryRoot = projectContext.memoryRoot, pr
|
|
|
279
319
|
console.log(`Learning report written: ${path.join(memoryRoot, REPORT_FILE)}`);
|
|
280
320
|
}
|
|
281
321
|
|
|
282
|
-
export function collectLearningCandidates({ limit, tasksRoot = projectContext.tasksRoot } = {}) {
|
|
322
|
+
export function collectLearningCandidates({ limit, tasksRoot = projectContext.tasksRoot, taskDir = null } = {}) {
|
|
283
323
|
const candidates = [];
|
|
284
324
|
const seen = new Set();
|
|
285
|
-
|
|
325
|
+
const taskDirs = taskDir
|
|
326
|
+
? [taskDir]
|
|
327
|
+
: fs.existsSync(tasksRoot)
|
|
328
|
+
? fs.readdirSync(tasksRoot)
|
|
329
|
+
.filter((name) => name.startsWith('TASK-'))
|
|
330
|
+
.sort()
|
|
331
|
+
.reverse()
|
|
332
|
+
.map((name) => path.join(tasksRoot, name))
|
|
333
|
+
: [];
|
|
334
|
+
if (!taskDirs.length) {
|
|
286
335
|
return candidates;
|
|
287
336
|
}
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
.sort()
|
|
291
|
-
.reverse();
|
|
292
|
-
for (const taskName of taskDirs) {
|
|
337
|
+
for (const taskPath of taskDirs) {
|
|
338
|
+
const taskName = path.basename(taskPath);
|
|
293
339
|
for (const fileName of ['retrospective.md', 'feedback.md', 'execution-feedback.md', 'verify.md', 'check.md']) {
|
|
294
340
|
const sourceArtifact = `${taskName}/${fileName}`;
|
|
295
|
-
const filePath = path.join(
|
|
341
|
+
const filePath = path.join(taskPath, fileName);
|
|
296
342
|
if (!fs.existsSync(filePath)) {
|
|
297
343
|
continue;
|
|
298
344
|
}
|
|
@@ -510,6 +556,78 @@ function suggestLearningTarget(text) {
|
|
|
510
556
|
return `${MEMORY_TARGET_PREFIX}${APPROVED_FILE}`;
|
|
511
557
|
}
|
|
512
558
|
|
|
559
|
+
function buildLearningIndexEntry(candidate, existingEntries) {
|
|
560
|
+
const previous = existingEntries.find((entry) => entry.reasonHash === candidate.reasonHash)
|
|
561
|
+
|| existingEntries.find((entry) => entry.id === candidate.id);
|
|
562
|
+
return {
|
|
563
|
+
id: previous?.id || candidate.id,
|
|
564
|
+
source: candidate.source,
|
|
565
|
+
sourceArtifact: candidate.sourceArtifact,
|
|
566
|
+
reasonHash: candidate.reasonHash,
|
|
567
|
+
kind: candidate.kind,
|
|
568
|
+
learningLayer: candidate.learningLayer,
|
|
569
|
+
summary: candidate.lesson,
|
|
570
|
+
confidence: candidate.confidence,
|
|
571
|
+
problem: candidate.problem,
|
|
572
|
+
lesson: candidate.lesson,
|
|
573
|
+
repeatRisk: candidate.repeatRisk,
|
|
574
|
+
scope: scopeForTarget(previous?.target || candidate.suggestedTarget),
|
|
575
|
+
risk: riskForCandidate(candidate),
|
|
576
|
+
candidate: candidate.text,
|
|
577
|
+
proposedChange: previous?.proposedChange || candidate.proposedWording,
|
|
578
|
+
proposedWording: previous?.proposedWording || candidate.proposedWording,
|
|
579
|
+
decision: previous?.decision || 'pending',
|
|
580
|
+
target: previous?.target || candidate.suggestedTarget,
|
|
581
|
+
notes: previous?.notes || '',
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function mergeLearningIndexEntries({ candidateEntries, existingEntries }) {
|
|
586
|
+
const seen = new Set(candidateEntries.map((entry) => entry.reasonHash || entry.id));
|
|
587
|
+
const preserved = existingEntries.filter((entry) => {
|
|
588
|
+
const key = entry.reasonHash || entry.id;
|
|
589
|
+
if (!key || seen.has(key)) {
|
|
590
|
+
return false;
|
|
591
|
+
}
|
|
592
|
+
seen.add(key);
|
|
593
|
+
return true;
|
|
594
|
+
});
|
|
595
|
+
return [...candidateEntries, ...preserved];
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function readExistingLearningEntries() {
|
|
599
|
+
const indexPath = path.join(projectContext.memoryRoot, INDEX_FILE);
|
|
600
|
+
if (!fs.existsSync(indexPath)) {
|
|
601
|
+
return [];
|
|
602
|
+
}
|
|
603
|
+
try {
|
|
604
|
+
const index = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
|
|
605
|
+
return Array.isArray(index.entries) ? index.entries : [];
|
|
606
|
+
} catch {
|
|
607
|
+
return [];
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function scopeForTarget(target) {
|
|
612
|
+
if (target?.startsWith('shared-playbook/')) {
|
|
613
|
+
return 'cross-project candidate; manual shared-framework review required before promotion.';
|
|
614
|
+
}
|
|
615
|
+
if (target?.startsWith(PROJECT_PLAYBOOK_TARGET_PREFIX)) {
|
|
616
|
+
return 'current project playbook overlay; project-specific procedures, routes, commands or runtime quirks.';
|
|
617
|
+
}
|
|
618
|
+
return 'current project memory; durable architecture, product or process knowledge.';
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function riskForCandidate(candidate) {
|
|
622
|
+
if (candidate.suggestedTarget?.startsWith('shared-playbook/')) {
|
|
623
|
+
return 'High if promoted without validation: a task-specific observation could become cross-project guidance.';
|
|
624
|
+
}
|
|
625
|
+
if (candidate.confidence === 'low') {
|
|
626
|
+
return 'Medium: wording may be too vague or one-off; prefer defer/rewrite unless confirmed.';
|
|
627
|
+
}
|
|
628
|
+
return 'Low if reviewed: promotion is human-approved and source-linked.';
|
|
629
|
+
}
|
|
630
|
+
|
|
513
631
|
function readMarkdownField(body, fieldName) {
|
|
514
632
|
const pattern = new RegExp(`^- ${escapeRegExp(fieldName)}: (.*)$`, 'm');
|
|
515
633
|
const match = pattern.exec(body);
|
|
@@ -622,24 +740,66 @@ function renderReviewEntries(entries) {
|
|
|
622
740
|
return ['- None.'];
|
|
623
741
|
}
|
|
624
742
|
return entries.flatMap((entry) => [
|
|
625
|
-
`### ${entry.id}`,
|
|
743
|
+
`### ${entry.id}: ${entry.summary || entry.lesson || entry.candidate || 'Learning candidate'}`,
|
|
626
744
|
'',
|
|
627
|
-
`-
|
|
628
|
-
`- Current decision: \`${entry.decision || 'pending'}\``,
|
|
745
|
+
`- Summary: ${entry.summary || entry.lesson || 'Not specified.'}`,
|
|
629
746
|
`- Suggested target: \`${entry.target || 'memory/approved-learning.md'}\``,
|
|
630
|
-
`-
|
|
631
|
-
`-
|
|
747
|
+
`- Source artifact: \`${entry.sourceArtifact || entry.source}\``,
|
|
748
|
+
`- Reason hash: \`${entry.reasonHash || 'missing'}\``,
|
|
749
|
+
`- Reason: ${entry.problem || 'Not specified.'}`,
|
|
750
|
+
`- Proposed change: ${entry.proposedChange || entry.proposedWording || entry.candidate}`,
|
|
751
|
+
`- Scope: ${entry.scope || scopeForTarget(entry.target)}`,
|
|
632
752
|
`- Confidence: \`${entry.confidence || 'unknown'}\``,
|
|
633
|
-
`-
|
|
634
|
-
`-
|
|
635
|
-
`-
|
|
636
|
-
`-
|
|
753
|
+
`- Risk if promoted: ${entry.risk || 'Not specified.'}`,
|
|
754
|
+
`- Repeat risk if ignored: ${entry.repeatRisk || 'Not specified.'}`,
|
|
755
|
+
`- Current decision: \`${entry.decision || 'pending'}\``,
|
|
756
|
+
`- Human notes: ${entry.notes || '(empty)'}`,
|
|
637
757
|
'',
|
|
638
758
|
'Decision to set in `learning-index.json`: `promote | defer | reject | rewrite`',
|
|
639
759
|
'',
|
|
640
760
|
]);
|
|
641
761
|
}
|
|
642
762
|
|
|
763
|
+
function renderConsoleLearningCards(entries) {
|
|
764
|
+
const pending = entries.filter((entry) => !entry.decision || entry.decision === 'pending');
|
|
765
|
+
if (!pending.length) {
|
|
766
|
+
return ['- pending learning cards: none'];
|
|
767
|
+
}
|
|
768
|
+
return [
|
|
769
|
+
'- pending learning cards:',
|
|
770
|
+
...pending.map((entry) => ` - ${entry.id}: ${entry.summary || entry.lesson || entry.candidate} -> ${entry.target || 'memory/approved-learning.md'}`),
|
|
771
|
+
];
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function writeLearningCloseoutSummary({ taskDir, taskId, candidates, entries, reviewPath }) {
|
|
775
|
+
const pending = entries.filter((entry) => !entry.decision || entry.decision === 'pending');
|
|
776
|
+
const content = [
|
|
777
|
+
'# Learning Closeout',
|
|
778
|
+
'',
|
|
779
|
+
`Task: \`${taskId}\``,
|
|
780
|
+
`Generated at: \`${new Date().toISOString()}\``,
|
|
781
|
+
'',
|
|
782
|
+
'## Human Action Required',
|
|
783
|
+
'',
|
|
784
|
+
`Review \`${relativeProjectPath(reviewPath)}\` and set every pending entry in \`${relativeProjectPath(path.join(projectContext.memoryRoot, INDEX_FILE))}\` to \`promote\`, \`defer\`, \`reject\` or \`rewrite\`.`,
|
|
785
|
+
'',
|
|
786
|
+
'## Summary',
|
|
787
|
+
'',
|
|
788
|
+
`- Candidates collected from this task: ${candidates.length}`,
|
|
789
|
+
`- Pending learning decisions: ${pending.length}`,
|
|
790
|
+
`- Learning review: \`${relativeProjectPath(reviewPath)}\``,
|
|
791
|
+
`- Learning report: \`${relativeProjectPath(path.join(projectContext.memoryRoot, REPORT_FILE))}\``,
|
|
792
|
+
'',
|
|
793
|
+
'## Pending Cards',
|
|
794
|
+
'',
|
|
795
|
+
...renderReviewEntries(pending),
|
|
796
|
+
'',
|
|
797
|
+
].join('\n');
|
|
798
|
+
const closeoutPath = path.join(taskDir, CLOSEOUT_FILE);
|
|
799
|
+
fs.writeFileSync(closeoutPath, content.endsWith('\n') ? content : `${content}\n`);
|
|
800
|
+
return closeoutPath;
|
|
801
|
+
}
|
|
802
|
+
|
|
643
803
|
function relativeProjectPath(filePath, projectRoot = projectContext.projectRoot) {
|
|
644
804
|
return path.relative(projectRoot, filePath) || path.basename(filePath);
|
|
645
805
|
}
|
|
@@ -170,6 +170,29 @@ describe('learning loop', () => {
|
|
|
170
170
|
const review = fs.readFileSync(path.join(memoryRoot, 'learning-review.md'), 'utf8');
|
|
171
171
|
expect(review).toContain('## Human Approval Contract');
|
|
172
172
|
expect(review).toContain('Decision to set in `learning-index.json`: `promote | defer | reject | rewrite`');
|
|
173
|
+
expect(review).toContain('### LC-001: Capture feedback at every stage.');
|
|
174
|
+
expect(review).toContain('- Summary: Capture feedback at every stage.');
|
|
175
|
+
expect(review).toContain('- Source artifact: `TASK-001/feedback.md`');
|
|
176
|
+
expect(review).toContain('- Reason hash: `missing`');
|
|
177
|
+
expect(review).toContain('- Proposed change: Project memory note: capture feedback at every stage.');
|
|
178
|
+
expect(review).toContain('- Scope: current project memory; durable architecture, product or process knowledge.');
|
|
179
|
+
expect(review).toContain('- Risk if promoted: Not specified.');
|
|
173
180
|
expect(review).toContain('Project memory note: capture feedback at every stage.');
|
|
174
181
|
});
|
|
182
|
+
|
|
183
|
+
it('collects closeout candidates from one task when taskDir is provided', () => {
|
|
184
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ops-learning-task-filter-'));
|
|
185
|
+
const taskA = path.join(root, 'TASK-001-current');
|
|
186
|
+
const taskB = path.join(root, 'TASK-002-other');
|
|
187
|
+
fs.mkdirSync(taskA, { recursive: true });
|
|
188
|
+
fs.mkdirSync(taskB, { recursive: true });
|
|
189
|
+
fs.writeFileSync(path.join(taskA, 'retrospective.md'), '- UI acceptance playbook should include current route.\n');
|
|
190
|
+
fs.writeFileSync(path.join(taskB, 'retrospective.md'), '- UI acceptance playbook should include other route.\n');
|
|
191
|
+
|
|
192
|
+
const candidates = collectLearningCandidates({ taskDir: taskA, tasksRoot: root, limit: 10 });
|
|
193
|
+
|
|
194
|
+
expect(candidates).toHaveLength(1);
|
|
195
|
+
expect(candidates[0].sourceArtifact).toBe('TASK-001-current/retrospective.md');
|
|
196
|
+
expect(candidates[0].text).toContain('current route');
|
|
197
|
+
});
|
|
175
198
|
});
|
|
@@ -93,9 +93,9 @@ export function buildFrameworkPackageSpec({ frameworkPackage = null, frameworkVe
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
export function buildOpsScripts(packageSpec) {
|
|
96
|
-
const run = (command) => `
|
|
96
|
+
const run = (command) => `ops-agent ${command}`;
|
|
97
97
|
return {
|
|
98
|
-
ops:
|
|
98
|
+
ops: 'ops-agent',
|
|
99
99
|
'agent:build-check-context': run('build-check-context'),
|
|
100
100
|
'agent:validate-check-artifacts': run('validate-check-artifacts'),
|
|
101
101
|
'agent:manifest': run('manifest'),
|
|
@@ -119,6 +119,7 @@ export function buildOpsScripts(packageSpec) {
|
|
|
119
119
|
'agent:update-memory': run('update-memory'),
|
|
120
120
|
'agent:learning-audit': run('learning-audit'),
|
|
121
121
|
'agent:learning-report': run('learning-report'),
|
|
122
|
+
'agent:learning-closeout': run('learning-closeout'),
|
|
122
123
|
'agent:test': run('test/self-test'),
|
|
123
124
|
};
|
|
124
125
|
}
|
|
@@ -139,10 +140,30 @@ function upsertPackageScripts({ packageJsonPath, packageSpec, changes }) {
|
|
|
139
140
|
...(packageJson.scripts || {}),
|
|
140
141
|
...buildOpsScripts(packageSpec),
|
|
141
142
|
};
|
|
143
|
+
const dependency = buildFrameworkPackageDependency(packageSpec);
|
|
144
|
+
packageJson.devDependencies = {
|
|
145
|
+
...(packageJson.devDependencies || {}),
|
|
146
|
+
[dependency.name]: dependency.version,
|
|
147
|
+
};
|
|
142
148
|
fs.writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`);
|
|
143
149
|
changes.push({ path: packageJsonPath, kind: 'file', status: 'overwritten' });
|
|
144
150
|
}
|
|
145
151
|
|
|
152
|
+
function buildFrameworkPackageDependency(packageSpec) {
|
|
153
|
+
const spec = String(packageSpec || '@besales/ops-framework').trim();
|
|
154
|
+
const atIndex = spec.startsWith('@') ? spec.indexOf('@', 1) : spec.indexOf('@');
|
|
155
|
+
if (atIndex > 0) {
|
|
156
|
+
return {
|
|
157
|
+
name: spec.slice(0, atIndex),
|
|
158
|
+
version: spec.slice(atIndex + 1) || 'latest',
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
name: spec,
|
|
163
|
+
version: 'latest',
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
146
167
|
export function createTask({
|
|
147
168
|
projectRoot = process.cwd(),
|
|
148
169
|
taskId,
|
|
@@ -56,7 +56,7 @@ describe('bootstrap utilities', () => {
|
|
|
56
56
|
expect(() => assertPortableGeneratedConfig({ projectConfigContent, agentsConfigContent })).not.toThrow();
|
|
57
57
|
});
|
|
58
58
|
|
|
59
|
-
it('can install
|
|
59
|
+
it('can install local ops-agent scripts with a pinned package dependency', () => {
|
|
60
60
|
const root = makeTempProject();
|
|
61
61
|
fs.writeFileSync(path.join(root, 'package.json'), `${JSON.stringify({
|
|
62
62
|
name: 'fixture-project',
|
|
@@ -79,10 +79,10 @@ describe('bootstrap utilities', () => {
|
|
|
79
79
|
|
|
80
80
|
const packageJson = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
81
81
|
expect(packageJson.scripts.test).toBe('echo ok');
|
|
82
|
-
expect(packageJson.scripts.ops).toBe('
|
|
83
|
-
expect(packageJson.scripts['agent:run-verify']).toBe('
|
|
82
|
+
expect(packageJson.scripts.ops).toBe('ops-agent');
|
|
83
|
+
expect(packageJson.scripts['agent:run-verify']).toBe('ops-agent run-verify');
|
|
84
84
|
expect(packageJson.dependencies.leftpad).toBe('1.0.0');
|
|
85
|
-
expect(packageJson.devDependencies?.['@besales/ops-framework']).
|
|
85
|
+
expect(packageJson.devDependencies?.['@besales/ops-framework']).toBe('0.1.0');
|
|
86
86
|
});
|
|
87
87
|
|
|
88
88
|
it('creates a minimal task artifact set', () => {
|
|
@@ -136,9 +136,10 @@ describe('buildOpsScripts', () => {
|
|
|
136
136
|
it('generates the standard project script surface', () => {
|
|
137
137
|
const scripts = buildOpsScripts('@besales/ops-framework@0.1.0');
|
|
138
138
|
|
|
139
|
-
expect(scripts.ops).toBe('
|
|
140
|
-
expect(scripts['agent:quality-gates']).toBe('
|
|
141
|
-
expect(scripts['agent:
|
|
139
|
+
expect(scripts.ops).toBe('ops-agent');
|
|
140
|
+
expect(scripts['agent:quality-gates']).toBe('ops-agent quality-gates');
|
|
141
|
+
expect(scripts['agent:learning-closeout']).toBe('ops-agent learning-closeout');
|
|
142
|
+
expect(scripts['agent:test']).toBe('ops-agent test/self-test');
|
|
142
143
|
});
|
|
143
144
|
});
|
|
144
145
|
|
|
@@ -23,13 +23,6 @@ const MEMORY_MAX_CHARS = {
|
|
|
23
23
|
strict: Infinity,
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
-
const STRICT_TRIGGER_PATTERNS = [
|
|
27
|
-
'auth-security',
|
|
28
|
-
'prisma-schema',
|
|
29
|
-
'production-runtime',
|
|
30
|
-
'source-sync-provider',
|
|
31
|
-
];
|
|
32
|
-
|
|
33
26
|
const CHECK_RELEVANT_SECTIONS = [
|
|
34
27
|
'цель',
|
|
35
28
|
'goal',
|
|
@@ -157,9 +150,7 @@ export function resolveLlmContextMode({ requestedMode, riskTriggers = [] } = {})
|
|
|
157
150
|
if (requested) {
|
|
158
151
|
return requested;
|
|
159
152
|
}
|
|
160
|
-
return
|
|
161
|
-
? 'strict'
|
|
162
|
-
: 'standard';
|
|
153
|
+
return 'standard';
|
|
163
154
|
}
|
|
164
155
|
|
|
165
156
|
export function normalizeLlmContextMode(value) {
|
|
@@ -202,9 +202,9 @@ describe('llm input pack utilities', () => {
|
|
|
202
202
|
expect(pack.estimatedTokens).toBeGreaterThan(Math.ceil(value.length / 2.3));
|
|
203
203
|
});
|
|
204
204
|
|
|
205
|
-
it('defaults
|
|
206
|
-
expect(resolveLlmContextMode({ riskTriggers: ['production-runtime'] })).toBe('
|
|
207
|
-
expect(resolveLlmContextMode({ riskTriggers: ['source-sync-provider'] })).toBe('
|
|
205
|
+
it('defaults to standard context and escalates only after context_insufficient', () => {
|
|
206
|
+
expect(resolveLlmContextMode({ riskTriggers: ['production-runtime'] })).toBe('standard');
|
|
207
|
+
expect(resolveLlmContextMode({ riskTriggers: ['source-sync-provider'] })).toBe('standard');
|
|
208
208
|
expect(resolveLlmContextMode({ riskTriggers: ['panel-ui'] })).toBe('standard');
|
|
209
209
|
expect(resolveLlmContextMode({ requestedMode: 'fast', riskTriggers: ['production-runtime'] })).toBe('fast');
|
|
210
210
|
});
|
|
@@ -234,7 +234,15 @@ export function writeTaskManifest(taskDir, manifest) {
|
|
|
234
234
|
fs.writeFileSync(path.join(taskDir, TASK_MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
-
export function recordLlmInputUsage({
|
|
237
|
+
export function recordLlmInputUsage({
|
|
238
|
+
taskDir,
|
|
239
|
+
stage,
|
|
240
|
+
packMeta,
|
|
241
|
+
attempts = [],
|
|
242
|
+
rerunCount = 0,
|
|
243
|
+
now = new Date().toISOString(),
|
|
244
|
+
timing = null,
|
|
245
|
+
}) {
|
|
238
246
|
const existing = readExistingManifest(taskDir);
|
|
239
247
|
if (!existing) {
|
|
240
248
|
return null;
|
|
@@ -263,6 +271,7 @@ export function recordLlmInputUsage({ taskDir, stage, packMeta, attempts = [], r
|
|
|
263
271
|
rerunCount,
|
|
264
272
|
attempts: normalizedAttempts,
|
|
265
273
|
cumulativeEstimatedTokens: normalizedAttempts.reduce((sum, attempt) => sum + (attempt.estimatedTokens || 0), 0),
|
|
274
|
+
timing: timing || undefined,
|
|
266
275
|
updatedAt: now,
|
|
267
276
|
},
|
|
268
277
|
},
|
package/bin/ops-agent.mjs
CHANGED
package/bin/run-check.mjs
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
resolveConfigValue,
|
|
25
25
|
resolveTaskDir,
|
|
26
26
|
sha256Json,
|
|
27
|
+
updateStatus,
|
|
27
28
|
writeTaskFile,
|
|
28
29
|
} from './lib/check-context-utils.mjs';
|
|
29
30
|
import {
|
|
@@ -38,7 +39,12 @@ import {
|
|
|
38
39
|
resolveLlmContextMode,
|
|
39
40
|
summarizePackForConsole,
|
|
40
41
|
} from './lib/llm-input-pack-utils.mjs';
|
|
41
|
-
import {
|
|
42
|
+
import {
|
|
43
|
+
buildTaskManifest,
|
|
44
|
+
recordLlmInputUsage,
|
|
45
|
+
validateManifest,
|
|
46
|
+
writeTaskManifest,
|
|
47
|
+
} from './lib/task-manifest-utils.mjs';
|
|
42
48
|
|
|
43
49
|
function main() {
|
|
44
50
|
runMain().catch((error) => {
|
|
@@ -59,8 +65,25 @@ async function runMain() {
|
|
|
59
65
|
const dryRun = getFlag(args, 'dry-run', false) === true;
|
|
60
66
|
const noCache = getFlag(args, 'no-cache', false) === true;
|
|
61
67
|
const checkerConfig = resolveCheckerConfig(args);
|
|
68
|
+
const runStartedAt = new Date();
|
|
62
69
|
|
|
63
|
-
|
|
70
|
+
let checkContext = ensureFreshCheckContext(taskDir, taskId);
|
|
71
|
+
const deterministicPrecheck = syncManifestAndStatusBeforeCheck({ taskDir, taskId, checkContext });
|
|
72
|
+
checkContext = deterministicPrecheck.checkContext;
|
|
73
|
+
if (!deterministicPrecheck.ok) {
|
|
74
|
+
writeDeterministicPrecheckReturn({
|
|
75
|
+
taskDir,
|
|
76
|
+
taskId,
|
|
77
|
+
checkContext,
|
|
78
|
+
checkerConfig,
|
|
79
|
+
issues: deterministicPrecheck.issues,
|
|
80
|
+
startedAt: runStartedAt,
|
|
81
|
+
});
|
|
82
|
+
runValidator(taskArg);
|
|
83
|
+
console.log(`Checker preflight blocked ${taskId}: return_to_plan`);
|
|
84
|
+
console.log(`- deterministicIssues: ${deterministicPrecheck.issues.length}`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
64
87
|
const initialContextMode = resolveLlmContextMode({
|
|
65
88
|
requestedMode: getFlag(args, 'context-mode') || getFlag(args, 'llm-context-mode'),
|
|
66
89
|
riskTriggers: checkContext.riskTriggers,
|
|
@@ -152,6 +175,7 @@ async function runMain() {
|
|
|
152
175
|
packMeta: promptPayload.pack.meta,
|
|
153
176
|
attempts: llmInputAttempts,
|
|
154
177
|
rerunCount,
|
|
178
|
+
timing: buildTiming(runStartedAt),
|
|
155
179
|
});
|
|
156
180
|
runValidator(taskArg);
|
|
157
181
|
console.log(`Checker blocked for ${taskId}: strict context pack over cap`);
|
|
@@ -166,6 +190,7 @@ async function runMain() {
|
|
|
166
190
|
packMeta: promptPayload.pack.meta,
|
|
167
191
|
attempts: llmInputAttempts,
|
|
168
192
|
rerunCount,
|
|
193
|
+
timing: buildTiming(runStartedAt),
|
|
169
194
|
});
|
|
170
195
|
console.log(`Checker cache hit for ${taskId}: ${cacheKeySha}`);
|
|
171
196
|
return;
|
|
@@ -197,6 +222,7 @@ async function runMain() {
|
|
|
197
222
|
packMeta: promptPayload.pack.meta,
|
|
198
223
|
attempts: llmInputAttempts,
|
|
199
224
|
rerunCount,
|
|
225
|
+
timing: buildTiming(runStartedAt),
|
|
200
226
|
});
|
|
201
227
|
runValidator(taskArg);
|
|
202
228
|
throw new Error(`Checker provider failed with ${failureReason}: ${error.message}`);
|
|
@@ -229,6 +255,7 @@ async function runMain() {
|
|
|
229
255
|
packMeta: promptPayload.pack.meta,
|
|
230
256
|
attempts: llmInputAttempts,
|
|
231
257
|
rerunCount,
|
|
258
|
+
timing: buildTiming(runStartedAt),
|
|
232
259
|
});
|
|
233
260
|
runValidator(taskArg);
|
|
234
261
|
console.log(`Checker run completed for ${taskId}: ${providerOutput.checkResultJson?.verdict}`);
|
|
@@ -247,6 +274,142 @@ function buildAttemptRecord(packMeta, outcome) {
|
|
|
247
274
|
};
|
|
248
275
|
}
|
|
249
276
|
|
|
277
|
+
function buildTiming(startedAt, completedAt = new Date()) {
|
|
278
|
+
return {
|
|
279
|
+
startedAt: startedAt.toISOString(),
|
|
280
|
+
completedAt: completedAt.toISOString(),
|
|
281
|
+
durationMs: Math.max(0, completedAt.getTime() - startedAt.getTime()),
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function syncManifestAndStatusBeforeCheck({ taskDir, taskId, checkContext }) {
|
|
286
|
+
updateStatus(taskDir, {
|
|
287
|
+
planSha: `\`${checkContext.planSha}\``,
|
|
288
|
+
memorySha: `\`${checkContext.memorySha}\``,
|
|
289
|
+
checkResult: '- `check.result.json`: pending_current_check',
|
|
290
|
+
supervisorAction: 'Synchronized manifest/status before Check.',
|
|
291
|
+
nextStep: 'Run Check or fix deterministic plan quality gates before Human Gate.',
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
const syncedCheckContext = ensureFreshCheckContext(taskDir, taskId);
|
|
295
|
+
const manifest = buildTaskManifest({ taskDir });
|
|
296
|
+
writeTaskManifest(taskDir, manifest);
|
|
297
|
+
|
|
298
|
+
const issues = validateManifest(manifest).map((message) => ({
|
|
299
|
+
category: 'manifest_validation',
|
|
300
|
+
message,
|
|
301
|
+
}));
|
|
302
|
+
if (!manifest.context.checkContextCurrent) {
|
|
303
|
+
issues.push({
|
|
304
|
+
category: 'stale_check_context',
|
|
305
|
+
message: 'check-context.json/checker-context-pack.md is stale after synchronization.',
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
for (const signal of manifest.qualitySignals || []) {
|
|
309
|
+
issues.push({
|
|
310
|
+
category: 'plan_quality_gate',
|
|
311
|
+
message: signal,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
ok: issues.length === 0,
|
|
317
|
+
manifest,
|
|
318
|
+
issues,
|
|
319
|
+
checkContext: syncedCheckContext,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function writeDeterministicPrecheckReturn({
|
|
324
|
+
taskDir,
|
|
325
|
+
taskId,
|
|
326
|
+
checkContext,
|
|
327
|
+
checkerConfig,
|
|
328
|
+
issues,
|
|
329
|
+
startedAt,
|
|
330
|
+
}) {
|
|
331
|
+
const findings = issues.map((issue, index) => ({
|
|
332
|
+
id: `F-${String(index + 1).padStart(3, '0')}`,
|
|
333
|
+
severity: 'blocking',
|
|
334
|
+
claimCategory: issue.category === 'plan_quality_gate' ? 'risk_unaddressed' : 'missing_evidence',
|
|
335
|
+
claim: issue.message,
|
|
336
|
+
evidenceRefs: [
|
|
337
|
+
{
|
|
338
|
+
type: 'file',
|
|
339
|
+
ref: issue.category === 'plan_quality_gate' ? 'plan.md' : 'task-manifest.json',
|
|
340
|
+
},
|
|
341
|
+
],
|
|
342
|
+
affectedPlanSections: ['Plan/Check'],
|
|
343
|
+
expectedCorrection: expectedCorrectionForPrecheckIssue(issue),
|
|
344
|
+
}));
|
|
345
|
+
const result = {
|
|
346
|
+
taskId,
|
|
347
|
+
stage: 'Check',
|
|
348
|
+
checkerProvider: 'deterministic-precheck',
|
|
349
|
+
checkerModel: 'none',
|
|
350
|
+
planSha: checkContext.planSha,
|
|
351
|
+
memorySha: checkContext.memorySha,
|
|
352
|
+
riskProfile: checkContext.riskProfile,
|
|
353
|
+
verdict: 'return_to_plan',
|
|
354
|
+
failureReason: null,
|
|
355
|
+
blockingFindings: findings.length,
|
|
356
|
+
nonBlockingFindings: 0,
|
|
357
|
+
humanQuestions: 0,
|
|
358
|
+
findings,
|
|
359
|
+
readyForHumanGate: false,
|
|
360
|
+
createdAt: new Date().toISOString(),
|
|
361
|
+
};
|
|
362
|
+
const markdown = [
|
|
363
|
+
'# Check',
|
|
364
|
+
'',
|
|
365
|
+
'## итоговая оценка',
|
|
366
|
+
'',
|
|
367
|
+
'`return_to_plan` from deterministic pre-check.',
|
|
368
|
+
'',
|
|
369
|
+
'External checker was not invoked because machine-readable plan/context gates already found blocking issues.',
|
|
370
|
+
'',
|
|
371
|
+
'## structured findings',
|
|
372
|
+
'',
|
|
373
|
+
'| ID | Severity | Category | Claim | Expected correction |',
|
|
374
|
+
'| --- | --- | --- | --- | --- |',
|
|
375
|
+
...findings.map((finding) => `| ${finding.id} | ${finding.severity} | ${finding.claimCategory} | ${escapeTableCell(finding.claim)} | ${escapeTableCell(finding.expectedCorrection)} |`),
|
|
376
|
+
'',
|
|
377
|
+
'## рекомендация supervisor',
|
|
378
|
+
'',
|
|
379
|
+
'`return_to_plan`',
|
|
380
|
+
'',
|
|
381
|
+
'## Timing',
|
|
382
|
+
'',
|
|
383
|
+
`- Duration: ${buildTiming(startedAt).durationMs}ms`,
|
|
384
|
+
].join('\n');
|
|
385
|
+
|
|
386
|
+
writeTaskFile(taskDir, 'check.md', markdown);
|
|
387
|
+
writeTaskFile(taskDir, 'check.result.json', JSON.stringify(result, null, 2));
|
|
388
|
+
updateStatus(taskDir, {
|
|
389
|
+
checkVerdict: '`return_to_plan`',
|
|
390
|
+
checkResult: '- `check.result.json`: current',
|
|
391
|
+
supervisorAction: 'Deterministic Check preflight blocked external checker invocation.',
|
|
392
|
+
nextStep: 'Fix deterministic Check findings, then rerun Check.',
|
|
393
|
+
humanApproval: 'no',
|
|
394
|
+
});
|
|
395
|
+
ensureFreshCheckContext(taskDir, taskId);
|
|
396
|
+
appendOrchestrationLog(taskDir, `deterministic Check preflight returned return_to_plan; findings=${findings.length}; external checker skipped`);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function expectedCorrectionForPrecheckIssue(issue) {
|
|
400
|
+
if (issue.category === 'plan_quality_gate') {
|
|
401
|
+
return 'Update plan.md so deterministic quality gates are complete before external Check.';
|
|
402
|
+
}
|
|
403
|
+
if (issue.category === 'stale_check_context') {
|
|
404
|
+
return 'Regenerate check context and task manifest before rerunning Check.';
|
|
405
|
+
}
|
|
406
|
+
return 'Fix task-manifest/check-context consistency before external Check.';
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function escapeTableCell(value) {
|
|
410
|
+
return String(value || '').replace(/\|/g, '\\|').replace(/\n/g, ' ').trim();
|
|
411
|
+
}
|
|
412
|
+
|
|
250
413
|
function buildCheckerCacheKey({
|
|
251
414
|
taskId,
|
|
252
415
|
checkContext,
|
package/package.json
CHANGED
package/prompts/supervisor.md
CHANGED
|
@@ -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
|
|
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 записаны.
|
|
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,9 +16,11 @@
|
|
|
16
16
|
|
|
17
17
|
## Learning Loop Audit
|
|
18
18
|
|
|
19
|
+
- `ops-agent learning-closeout <TASK>` выполнен после заполнения retrospective: `[fill in]`
|
|
19
20
|
- `ops-agent memory-candidates` выполнен после заполнения retrospective/check/verify feedback: `[fill in]`
|
|
20
21
|
- `ops-agent learning-index` создал human-reviewable decisions: `[fill in]`
|
|
21
22
|
- `ops-agent learning-review` создал human approval pack и был показан human: `[fill in]`
|
|
23
|
+
- Каждый learning показан human отдельной карточкой с summary/target/source/reason/proposed change/scope/confidence/risk/decision: `[fill in]`
|
|
22
24
|
- `learning-index.json` reviewed human/supervisor: `[fill in]`
|
|
23
25
|
- Promoted to project memory: `[fill in]`
|
|
24
26
|
- Promoted to project playbooks: `[fill in]`
|