@besales/ops-framework 0.1.0

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.
Files changed (70) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +328 -0
  3. package/bin/build-check-context.mjs +67 -0
  4. package/bin/build-execution-ledger.mjs +54 -0
  5. package/bin/estimate-llm-input.mjs +160 -0
  6. package/bin/guard-task.mjs +384 -0
  7. package/bin/hash-task-artifacts.mjs +44 -0
  8. package/bin/init-project.mjs +49 -0
  9. package/bin/intake-execution-feedback.mjs +207 -0
  10. package/bin/intake-feedback.test.mjs +73 -0
  11. package/bin/learning-loop.mjs +658 -0
  12. package/bin/learning-loop.test.mjs +175 -0
  13. package/bin/lib/bootstrap-utils.mjs +542 -0
  14. package/bin/lib/bootstrap-utils.test.mjs +156 -0
  15. package/bin/lib/check-context-utils.mjs +1448 -0
  16. package/bin/lib/check-context-utils.test.mjs +497 -0
  17. package/bin/lib/execution-ledger-utils.mjs +162 -0
  18. package/bin/lib/execution-ledger-utils.test.mjs +74 -0
  19. package/bin/lib/llm-input-pack-utils.mjs +663 -0
  20. package/bin/lib/llm-input-pack-utils.test.mjs +262 -0
  21. package/bin/lib/project-config.mjs +229 -0
  22. package/bin/lib/project-config.test.mjs +102 -0
  23. package/bin/lib/task-manifest-utils.mjs +512 -0
  24. package/bin/lib/task-manifest-utils.test.mjs +218 -0
  25. package/bin/lib/task-metrics-utils.mjs +63 -0
  26. package/bin/lib/task-metrics-utils.test.mjs +40 -0
  27. package/bin/lib/test-setup.mjs +37 -0
  28. package/bin/new-task.mjs +42 -0
  29. package/bin/ops-agent.mjs +81 -0
  30. package/bin/preflight.mjs +56 -0
  31. package/bin/providers/external-cli-checker.mjs +190 -0
  32. package/bin/providers/openai-checker.mjs +62 -0
  33. package/bin/quality-gates.mjs +92 -0
  34. package/bin/run-check.mjs +559 -0
  35. package/bin/run-plan-check-loop.mjs +392 -0
  36. package/bin/run-verify.mjs +627 -0
  37. package/bin/self-lint.mjs +88 -0
  38. package/bin/supervisor-turn.mjs +146 -0
  39. package/bin/supervisor-turn.test.mjs +72 -0
  40. package/bin/task-manifest.mjs +57 -0
  41. package/bin/task-metrics.mjs +48 -0
  42. package/bin/transition.mjs +94 -0
  43. package/bin/validate-check-artifacts.mjs +418 -0
  44. package/config/default-agents.json +100 -0
  45. package/package.json +28 -0
  46. package/playbooks/checker-context.md +9 -0
  47. package/playbooks/complexity-performance.md +13 -0
  48. package/playbooks/production-rollout.md +9 -0
  49. package/playbooks/source-sync-provider.md +9 -0
  50. package/playbooks/ui-acceptance.md +9 -0
  51. package/prompts/checker.md +170 -0
  52. package/prompts/executor.md +54 -0
  53. package/prompts/planner.md +128 -0
  54. package/prompts/researcher.md +44 -0
  55. package/prompts/supervisor.md +337 -0
  56. package/prompts/verifier.md +128 -0
  57. package/templates/brief.md +15 -0
  58. package/templates/check-resolution.md +69 -0
  59. package/templates/check-result.json +32 -0
  60. package/templates/check.md +46 -0
  61. package/templates/execution-feedback.md +25 -0
  62. package/templates/execution.md +101 -0
  63. package/templates/human-gate-summary.md +49 -0
  64. package/templates/orchestration-log.md +8 -0
  65. package/templates/plan.md +86 -0
  66. package/templates/research.md +13 -0
  67. package/templates/retrospective.md +48 -0
  68. package/templates/status.md +53 -0
  69. package/templates/verify-result.json +19 -0
  70. package/templates/verify.md +41 -0
@@ -0,0 +1,146 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import {
5
+ appendOrchestrationLog,
6
+ getFlag,
7
+ parseCliArgs,
8
+ readStatusStage,
9
+ resolveTaskDir,
10
+ } from './lib/check-context-utils.mjs';
11
+ import {
12
+ classifyFeedback,
13
+ recordFeedback,
14
+ } from './intake-execution-feedback.mjs';
15
+
16
+ function main() {
17
+ const args = parseCliArgs(process.argv.slice(2));
18
+ const taskArg = args.positional[0];
19
+ if (!taskArg) {
20
+ fail('Usage: ops-agent supervisor-turn <TASK-id-or-task-path> <message> [--from-file path] [--source user|assistant|tool] [--no-intake]');
21
+ }
22
+
23
+ try {
24
+ const taskDir = resolveTaskDir(taskArg);
25
+ const taskId = path.basename(taskDir);
26
+ const source = getFlag(args, 'source', 'user');
27
+ const message = readMessage(args);
28
+ const shouldIntake = !args.flags.has('no-intake') && message.trim().length > 0;
29
+ const classification = shouldIntake
30
+ ? recordFeedback({ taskDir, taskId, rawFeedback: message, source })
31
+ : classifyFeedback(message || 'status check');
32
+
33
+ appendOrchestrationLog(taskDir, {
34
+ event: 'supervisor_turn',
35
+ taskId,
36
+ source,
37
+ intakeRecorded: shouldIntake,
38
+ classification: classification.classification,
39
+ supervisorDecision: classification.supervisorDecision,
40
+ });
41
+
42
+ const stage = readStatusStage(taskDir) || 'unknown';
43
+ const contract = buildTurnContract({ taskId, stage, classification, intakeRecorded: shouldIntake });
44
+ if (args.flags.has('json')) {
45
+ console.log(JSON.stringify(contract, null, 2));
46
+ return;
47
+ }
48
+ console.log(renderTurnContract(contract));
49
+ } catch (error) {
50
+ fail(error.message);
51
+ }
52
+ }
53
+
54
+ function readMessage(args) {
55
+ const fromFile = getFlag(args, 'from-file');
56
+ if (fromFile) {
57
+ return fs.readFileSync(path.resolve(fromFile), 'utf8');
58
+ }
59
+ return args.positional.slice(1).join(' ');
60
+ }
61
+
62
+ export function buildTurnContract({ taskId, stage, classification, intakeRecorded }) {
63
+ return {
64
+ task: taskId,
65
+ stage,
66
+ supervisor: 'active',
67
+ intakeRecorded,
68
+ classification: classification.classification,
69
+ supervisorDecision: classification.supervisorDecision,
70
+ allowedNow: allowedNowForClassification(classification),
71
+ notAllowedNow: notAllowedNowForClassification(classification),
72
+ nextStep: nextStepForClassification(classification),
73
+ };
74
+ }
75
+
76
+ export function renderTurnContract(contract) {
77
+ return [
78
+ `TASK: \`${contract.task}\``,
79
+ `STAGE: \`${contract.stage}\``,
80
+ `SUPERVISOR: ${contract.supervisor}`,
81
+ `FEEDBACK INTAKE: ${contract.intakeRecorded ? `recorded (${contract.classification})` : 'not_recorded'}`,
82
+ `ALLOWED NOW: ${contract.allowedNow}`,
83
+ `NOT ALLOWED NOW: ${contract.notAllowedNow}`,
84
+ '',
85
+ `Следующий шаг: ${contract.nextStep}`,
86
+ ].join('\n');
87
+ }
88
+
89
+ function allowedNowForClassification(classification) {
90
+ switch (classification.supervisorDecision) {
91
+ case 'stay_current':
92
+ return 'continue current stage while keeping feedback available for retrospective/learning';
93
+ case 'stay_execute':
94
+ return 'continue Execute within approved scope';
95
+ case 'return_plan':
96
+ return 'update plan.md and run fresh Check before implementation continues';
97
+ case 'return_research':
98
+ return 'refresh research.md before updating plan/check';
99
+ case 'return_brief':
100
+ return 'reset task brief/scope before research/plan/check';
101
+ case 'human_arbitration':
102
+ return 'ask human to resolve the tradeoff before continuing';
103
+ default:
104
+ return 'pause for human triage';
105
+ }
106
+ }
107
+
108
+ function notAllowedNowForClassification(classification) {
109
+ if (classification.requiresFreshCheck) {
110
+ return 'continue implementation as if the previous Check/Human Gate still applies';
111
+ }
112
+ if (classification.supervisorDecision === 'human_triage') {
113
+ return 'guess intent or mutate task artifacts beyond feedback intake';
114
+ }
115
+ return 'drop the feedback on the floor or answer without task header/footer';
116
+ }
117
+
118
+ function nextStepForClassification(classification) {
119
+ switch (classification.classification) {
120
+ case 'acknowledgement':
121
+ return 'proceed with the previously proposed supervisor action.';
122
+ case 'learning_capture':
123
+ return 'continue current stage and include the event in retrospective and learning candidates.';
124
+ case 'within_approved_scope':
125
+ return 'continue Execute within the approved plan.';
126
+ case 'plan_patch_required':
127
+ return 'return to Plan, update plan.md, run fresh Check and request a new Human Gate if needed.';
128
+ case 'research_required':
129
+ return 'return to Research before Plan/Check.';
130
+ case 'brief_reset_required':
131
+ return 'return to Brief before Research/Plan/Check.';
132
+ case 'human_arbitration_required':
133
+ return 'ask human to resolve the decision explicitly.';
134
+ default:
135
+ return 'ask human to classify the message before continuing.';
136
+ }
137
+ }
138
+
139
+ function fail(message) {
140
+ console.error(`Error: ${message}`);
141
+ process.exit(1);
142
+ }
143
+
144
+ if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
145
+ main();
146
+ }
@@ -0,0 +1,72 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { describe, expect, it } from 'vitest';
6
+ import {
7
+ buildTurnContract,
8
+ renderTurnContract,
9
+ } from './supervisor-turn.mjs';
10
+
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = path.dirname(__filename);
13
+ const opsAgentBin = path.join(__dirname, 'ops-agent.mjs');
14
+
15
+ describe('supervisor turn router', () => {
16
+ it('records a task message and prints the mandatory turn contract', () => {
17
+ const taskId = 'TASK-999-supervisor-turn';
18
+ const taskDir = path.join(process.cwd(), 'ops', 'agent-pipeline', 'tasks', taskId);
19
+ fs.mkdirSync(taskDir, { recursive: true });
20
+ fs.writeFileSync(path.join(taskDir, 'brief.md'), '# Brief\n\nTest.\n');
21
+ fs.writeFileSync(path.join(taskDir, 'research.md'), '# Research\n\n## Findings\n\n- `docs/example.md`\n');
22
+ fs.writeFileSync(path.join(taskDir, 'plan.md'), '# Plan\n\n## Затронутые модули и файлы\n\n- `docs/example.md`\n');
23
+ fs.writeFileSync(path.join(taskDir, 'status.md'), [
24
+ '# Status',
25
+ '',
26
+ '## Текущий этап',
27
+ '',
28
+ 'Execute',
29
+ '',
30
+ '## Feedback intake',
31
+ '',
32
+ '- `feedback.md`: no_events',
33
+ ].join('\n'));
34
+ fs.writeFileSync(path.join(taskDir, 'orchestration-log.md'), '# Orchestration Log\n\n## Entries\n');
35
+
36
+ const result = spawnSync(process.execPath, [
37
+ opsAgentBin,
38
+ 'supervisor-turn',
39
+ taskId,
40
+ 'Да, давай.',
41
+ ], {
42
+ cwd: process.cwd(),
43
+ encoding: 'utf8',
44
+ });
45
+
46
+ expect(result.status).toBe(0);
47
+ expect(result.stdout).toContain(`TASK: \`${taskId}\``);
48
+ expect(result.stdout).toContain('SUPERVISOR: active');
49
+ expect(result.stdout).toContain('FEEDBACK INTAKE: recorded (acknowledgement)');
50
+ expect(result.stdout).toContain('Следующий шаг: proceed with the previously proposed supervisor action.');
51
+
52
+ const feedback = fs.readFileSync(path.join(taskDir, 'feedback.md'), 'utf8');
53
+ expect(feedback).toContain('- Classification: acknowledgement');
54
+ const log = fs.readFileSync(path.join(taskDir, 'orchestration-log.md'), 'utf8');
55
+ expect(log).toContain('supervisor_turn');
56
+ });
57
+
58
+ it('renders a contract from classification metadata', () => {
59
+ const contract = buildTurnContract({
60
+ taskId: 'TASK-999-contract',
61
+ stage: 'Plan',
62
+ intakeRecorded: true,
63
+ classification: {
64
+ classification: 'plan_patch_required',
65
+ supervisorDecision: 'return_plan',
66
+ requiresFreshCheck: true,
67
+ },
68
+ });
69
+
70
+ expect(renderTurnContract(contract)).toContain('NOT ALLOWED NOW: continue implementation as if the previous Check/Human Gate still applies');
71
+ });
72
+ });
@@ -0,0 +1,57 @@
1
+ import path from 'node:path';
2
+ import {
3
+ appendOrchestrationLog,
4
+ parseCliArgs,
5
+ resolveTaskDir,
6
+ } from './lib/check-context-utils.mjs';
7
+ import {
8
+ buildTaskManifest,
9
+ validateManifest,
10
+ writeTaskManifest,
11
+ } from './lib/task-manifest-utils.mjs';
12
+
13
+ function main() {
14
+ const args = parseCliArgs(process.argv.slice(2));
15
+ const taskArg = args.positional[0];
16
+ if (!taskArg) {
17
+ fail('Usage: node ops/agent-pipeline/bin/task-manifest.mjs <TASK-id-or-task-path> [--check] [--json]');
18
+ }
19
+
20
+ try {
21
+ const taskDir = resolveTaskDir(taskArg);
22
+ const taskId = path.basename(taskDir);
23
+ const manifest = buildTaskManifest({ taskDir });
24
+ const errors = validateManifest(manifest);
25
+
26
+ if (!args.flags.has('check')) {
27
+ writeTaskManifest(taskDir, manifest);
28
+ appendOrchestrationLog(taskDir, `task manifest updated; phase=${manifest.phase}; mode=${manifest.mode}; gatesOk=${errors.length === 0}`);
29
+ }
30
+
31
+ if (args.flags.has('json')) {
32
+ console.log(JSON.stringify({ ok: errors.length === 0, taskId, manifest, errors }, null, 2));
33
+ } else {
34
+ console.log(`Task manifest for ${taskId}: ${errors.length === 0 ? 'ok' : 'needs attention'}`);
35
+ console.log(`- mode: ${manifest.mode}`);
36
+ console.log(`- phase: ${manifest.phase}`);
37
+ console.log(`- risk: ${manifest.context.riskProfile} (${manifest.context.riskTriggers.join(', ') || 'none'})`);
38
+ if (errors.length > 0) {
39
+ console.log('Issues:');
40
+ for (const error of errors) {
41
+ console.log(`- ${error}`);
42
+ }
43
+ }
44
+ }
45
+
46
+ process.exit(errors.length === 0 ? 0 : 1);
47
+ } catch (error) {
48
+ fail(error.message);
49
+ }
50
+ }
51
+
52
+ function fail(message) {
53
+ console.error(`Error: ${message}`);
54
+ process.exit(1);
55
+ }
56
+
57
+ main();
@@ -0,0 +1,48 @@
1
+ import path from 'node:path';
2
+ import {
3
+ parseCliArgs,
4
+ readTaskFile,
5
+ resolveTaskDir,
6
+ } from './lib/check-context-utils.mjs';
7
+ import { computeTaskMetricsFromLog } from './lib/task-metrics-utils.mjs';
8
+
9
+ function main() {
10
+ const args = parseCliArgs(process.argv.slice(2));
11
+ const taskArg = args.positional[0];
12
+ if (!taskArg) {
13
+ fail('Usage: node ops/agent-pipeline/bin/task-metrics.mjs <TASK-id-or-task-path> [--json]');
14
+ }
15
+
16
+ try {
17
+ const taskDir = resolveTaskDir(taskArg);
18
+ const taskId = path.basename(taskDir);
19
+ const metrics = {
20
+ taskId,
21
+ ...computeTaskMetricsFromLog(readTaskFile(taskDir, 'orchestration-log.md')),
22
+ };
23
+
24
+ if (args.flags.has('json')) {
25
+ console.log(JSON.stringify(metrics, null, 2));
26
+ return;
27
+ }
28
+
29
+ console.log(`Task metrics for ${taskId}`);
30
+ console.log(`- events: ${metrics.events}`);
31
+ console.log(`- spanHours: ${metrics.spanHours}`);
32
+ console.log(`- externalVerifierRuns: ${metrics.externalVerifierRuns}`);
33
+ console.log(`- returnToPlan: ${metrics.returnToPlan}`);
34
+ console.log(`- returnToExecute: ${metrics.returnToExecute}`);
35
+ console.log(`- deterministicPreverifyReturns: ${metrics.deterministicPreverifyReturns}`);
36
+ console.log(`- processBreaks: ${metrics.processBreaks}`);
37
+ console.log(`- loopSignals: ${metrics.loopSignals}`);
38
+ } catch (error) {
39
+ fail(error.message);
40
+ }
41
+ }
42
+
43
+ function fail(message) {
44
+ console.error(`Error: ${message}`);
45
+ process.exit(1);
46
+ }
47
+
48
+ main();
@@ -0,0 +1,94 @@
1
+ import path from 'node:path';
2
+ import {
3
+ appendOrchestrationLog,
4
+ parseCliArgs,
5
+ resolveTaskDir,
6
+ updateStatus,
7
+ } from './lib/check-context-utils.mjs';
8
+ import {
9
+ normalizePhase,
10
+ transitionTaskManifest,
11
+ writeTaskManifest,
12
+ } from './lib/task-manifest-utils.mjs';
13
+
14
+ function main() {
15
+ const args = parseCliArgs(process.argv.slice(2));
16
+ const taskArg = args.positional[0];
17
+ const phaseArg = args.positional[1] || args.flags.get('to') || args.flags.get('phase');
18
+ if (!taskArg || !phaseArg) {
19
+ fail('Usage: node ops/agent-pipeline/bin/transition.mjs <TASK-id-or-task-path> <plan|check|execute|verify|human_gate|retrospective> [--json]');
20
+ }
21
+
22
+ try {
23
+ const taskDir = resolveTaskDir(taskArg);
24
+ const taskId = path.basename(taskDir);
25
+ const targetPhase = normalizePhase(phaseArg);
26
+ const result = transitionTaskManifest({ taskDir, targetPhase });
27
+
28
+ if (!result.ok) {
29
+ if (args.flags.has('json')) {
30
+ console.log(JSON.stringify(result, null, 2));
31
+ } else {
32
+ console.log(`Transition blocked for ${taskId} -> ${targetPhase}`);
33
+ for (const issue of result.issues) {
34
+ console.log(`- ${issue.category}: ${issue.message}`);
35
+ }
36
+ }
37
+ process.exit(1);
38
+ }
39
+
40
+ writeTaskManifest(taskDir, result.manifest);
41
+ updateStatus(taskDir, {
42
+ stage: displayPhase(targetPhase),
43
+ supervisorAction: `Transitioned to ${targetPhase} via agent:transition.`,
44
+ nextStep: nextStepForPhase(targetPhase),
45
+ humanApproval: targetPhase === 'human_gate' ? 'yes' : 'no',
46
+ });
47
+ appendOrchestrationLog(taskDir, `transitioned to ${targetPhase}; mode=${result.manifest.mode}; manifest updated`);
48
+
49
+ if (args.flags.has('json')) {
50
+ console.log(JSON.stringify(result, null, 2));
51
+ } else {
52
+ console.log(`Transitioned ${taskId} -> ${targetPhase}`);
53
+ console.log(`- mode: ${result.manifest.mode}`);
54
+ console.log(`- risk: ${result.manifest.context.riskProfile}`);
55
+ }
56
+ } catch (error) {
57
+ fail(error.message);
58
+ }
59
+ }
60
+
61
+ function nextStepForPhase(phase) {
62
+ switch (phase) {
63
+ case 'check':
64
+ return 'Run independent check or refresh check artifacts.';
65
+ case 'execute':
66
+ return 'Implement the checked plan and record execution evidence.';
67
+ case 'verify':
68
+ return 'Run Verify after execution evidence is complete.';
69
+ case 'human_gate':
70
+ return 'Ask human to review final gate artifacts and approve closeout.';
71
+ case 'retrospective':
72
+ return 'Capture retrospective lessons and delivery metrics.';
73
+ default:
74
+ return 'Continue planning until gates are ready.';
75
+ }
76
+ }
77
+
78
+ function displayPhase(phase) {
79
+ return {
80
+ plan: 'Plan',
81
+ check: 'Check',
82
+ execute: 'Execute',
83
+ verify: 'Verify',
84
+ human_gate: 'Human Gate',
85
+ retrospective: 'Retrospective',
86
+ }[phase] || phase;
87
+ }
88
+
89
+ function fail(message) {
90
+ console.error(`Error: ${message}`);
91
+ process.exit(1);
92
+ }
93
+
94
+ main();